diff --git a/.github/workflows/bcc.yml b/.github/workflows/bcc.yml index 016925ef9b2..3fe7bdde2f7 100644 --- a/.github/workflows/bcc.yml +++ b/.github/workflows/bcc.yml @@ -15,7 +15,7 @@ jobs: env: fail-fast: true - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/build-phar.yml b/.github/workflows/build-phar.yml index ba0c5c6d1e9..40da1ef7eb8 100644 --- a/.github/workflows/build-phar.yml +++ b/.github/workflows/build-phar.yml @@ -44,7 +44,7 @@ jobs: env: fail-fast: true - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 # required for composer to automatically detect root package version diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33f2a6b945b..90e3705cf4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: env: fail-fast: true - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get Composer Cache Directories id: composer-cache @@ -63,7 +63,7 @@ jobs: env: fail-fast: true - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get Composer Cache Directories id: composer-cache @@ -148,7 +148,7 @@ jobs: env: fail-fast: true - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get Composer Cache Directories id: composer-cache diff --git a/.github/workflows/shepherd.yml b/.github/workflows/shepherd.yml index 839d98563cc..3440783d936 100644 --- a/.github/workflows/shepherd.yml +++ b/.github/workflows/shepherd.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.2' diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index 35442ed5fcf..7cc615de5da 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -62,7 +62,12 @@ jobs: env: fail-fast: true - - uses: actions/checkout@v3 + - name: PHP Version + run: | + php -v + php -r 'var_dump(PHP_VERSION_ID);' + + - uses: actions/checkout@v4 - name: Get Composer Cache Directories id: composer-cache diff --git a/UPGRADING.md b/UPGRADING.md index 7b1d5d800c1..936f35747dd 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -3,6 +3,8 @@ - The minimum PHP version was raised to PHP 8.1.17. +- [BC] The configuration settings `ignoreInternalFunctionFalseReturn` and `ignoreInternalFunctionNullReturn` are now defaulted to `false` + - [BC] Switched the internal representation of `list` and `non-empty-list` from the TList and TNonEmptyList classes to an unsealed list shape: the TList, TNonEmptyList and TCallableList classes were removed. Nothing will change for users: the `list` and `non-empty-list` syntax will remain supported and its semantics unchanged. Psalm 5 already deprecates the `TList`, `TNonEmptyList` and `TCallableList` classes: use `\Psalm\Type::getListAtomic`, `\Psalm\Type::getNonEmptyListAtomic` and `\Psalm\Type::getCallableListAtomic` to instantiate list atomics, or directly instantiate TKeyedArray objects with `is_list=true` where appropriate. @@ -10,6 +12,9 @@ - [BC] The only optional boolean parameter of `TKeyedArray::getGenericArrayType` was removed, and was replaced with a string parameter with a different meaning. - [BC] The `TDependentListKey` type was removed and replaced with an optional property of the `TIntRange` type. +- [BC] `TCallableArray` and `TCallableList` removed and replaced with `TCallableKeyedArray`. + +- [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] Property `Config::$shepherd_host` was replaced with `Config::$shepherd_endpoint` @@ -33,6 +38,10 @@ - [BC] `strict_types` is now applied to all files of the Psalm codebase. +- [BC] Properties `Psalm\Type\Atomic\TLiteralFloat::$value` and `Psalm\Type\Atomic\TLiteralInt::$value` became typed (`float` and `int` respectively) + +- [BC] Property `Psalm\Storage\EnumCaseStorage::$value` changed from `int|string|null` to `TLiteralInt|TLiteralString|null` + # Upgrading from Psalm 4 to Psalm 5 ## Changed diff --git a/bin/update-property-map.php b/bin/update-property-map.php index 001a64022b2..1c55e609c20 100755 --- a/bin/update-property-map.php +++ b/bin/update-property-map.php @@ -93,9 +93,9 @@ function extractClassesFromStatements(array $statements): array foreach ($files as $file) { $contents = file_get_contents($file); // FIXME: find a way to ignore custom entities, for now we strip them. - $contents = preg_replace('#&[a-zA-Z\d.\-_]+;#', '', $contents); - $contents = preg_replace('#%[a-zA-Z\d.\-_]+;#', '', $contents); - $contents = preg_replace('#]+>#', '', $contents); + $contents = (string) preg_replace('#&[a-zA-Z\d.\-_]+;#', '', $contents); + $contents = (string) preg_replace('#%[a-zA-Z\d.\-_]+;#', '', $contents); + $contents = (string) preg_replace('#]+>#', '', $contents); try { $simple = new SimpleXMLElement($contents); } catch (Throwable $exception) { diff --git a/composer.json b/composer.json index 0adb443c964..0a56c517c06 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "vimeo/psalm", - "type": "library", + "type": "project", "description": "A static analysis tool for finding errors in PHP applications", "keywords": [ "php", @@ -39,6 +39,9 @@ "symfony/console": "^4.1.6 || ^5.0 || ^6.0", "symfony/filesystem": "^5.4 || ^6.0" }, + "conflict": { + "nikic/php-parser": "4.17.0" + }, "provide": { "psalm/psalm": "self.version" }, @@ -74,7 +77,8 @@ }, "extra": { "branch-alias": { - "dev-master": "5.x-dev", + "dev-master": "6.x-dev", + "dev-5.x": "5.x-dev", "dev-4.x": "4.x-dev", "dev-3.x": "3.x-dev", "dev-2.x": "2.x-dev", @@ -131,10 +135,15 @@ "scripts-descriptions": { "cs": "Checks that the code conforms to the coding standard.", "cs-fix": "Automatically correct coding standard violations.", - "lint": "Runs unit tests.", + "lint": "Lint php files.", "phpunit": "Runs unit tests in parallel.", "phpunit-std": "Runs unit tests.", "psalm": "Runs static analysis.", "tests": "Runs all available tests." + }, + "support": { + "docs": "https://psalm.dev/docs", + "issues": "https://github.com/vimeo/psalm/issues", + "source": "https://github.com/vimeo/psalm" } } diff --git a/config.xsd b/config.xsd index 4cf075b6ece..72745ea604f 100644 --- a/config.xsd +++ b/config.xsd @@ -49,8 +49,8 @@ - - + + @@ -230,6 +230,7 @@ + @@ -350,6 +351,7 @@ + @@ -438,12 +440,14 @@ + + diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 7d8c477b1a1..eedbac5dea9 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1307,8 +1307,8 @@ 'date_create_immutable' => ['DateTimeImmutable|false', 'datetime='=>'string', 'timezone='=>'?DateTimeZone'], 'date_create_immutable_from_format' => ['DateTimeImmutable|false', 'format'=>'string', 'datetime'=>'string', 'timezone='=>'?DateTimeZone'], 'date_date_set' => ['DateTime', 'object'=>'DateTime', 'year'=>'int', 'month'=>'int', 'day'=>'int'], -'date_default_timezone_get' => ['string'], -'date_default_timezone_set' => ['bool', 'timezoneId'=>'string'], +'date_default_timezone_get' => ['non-empty-string'], +'date_default_timezone_set' => ['bool', 'timezoneId'=>'non-empty-string'], 'date_diff' => ['DateInterval', 'baseObject'=>'DateTimeInterface', 'targetObject'=>'DateTimeInterface', 'absolute='=>'bool'], 'date_format' => ['string', 'object'=>'DateTimeInterface', 'format'=>'string'], 'date_get_last_errors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}|false'], @@ -1390,11 +1390,11 @@ 'DateTimeInterface::getTimezone' => ['DateTimeZone|false'], 'DateTimeInterface::__serialize' => ['array'], 'DateTimeInterface::__unserialize' => ['void', 'data'=>'array'], -'DateTimeZone::__construct' => ['void', 'timezone'=>'string'], +'DateTimeZone::__construct' => ['void', 'timezone'=>'non-empty-string'], 'DateTimeZone::__set_state' => ['DateTimeZone', 'array'=>'array'], 'DateTimeZone::__wakeup' => ['void'], 'DateTimeZone::getLocation' => ['array|false'], -'DateTimeZone::getName' => ['string'], +'DateTimeZone::getName' => ['non-empty-string'], 'DateTimeZone::getOffset' => ['int', 'datetime'=>'DateTimeInterface'], 'DateTimeZone::getTransitions' => ['list|false', 'timestampBegin='=>'int', 'timestampEnd='=>'int'], 'DateTimeZone::listAbbreviations' => ['array>'], @@ -1679,10 +1679,10 @@ 'DOMDocument::getElementsByTagName' => ['DOMNodeList', 'qualifiedName'=>'string'], 'DOMDocument::getElementsByTagNameNS' => ['DOMNodeList', 'namespace'=>'?string', 'localName'=>'string'], 'DOMDocument::importNode' => ['DOMNode|false', 'node'=>'DOMNode', 'deep='=>'bool'], -'DOMDocument::load' => ['DOMDocument|bool', 'filename'=>'string', 'options='=>'int'], +'DOMDocument::load' => ['bool', 'filename'=>'string', 'options='=>'int'], 'DOMDocument::loadHTML' => ['bool', 'source'=>'non-empty-string', 'options='=>'int'], 'DOMDocument::loadHTMLFile' => ['bool', 'filename'=>'string', 'options='=>'int'], -'DOMDocument::loadXML' => ['DOMDocument|bool', 'source'=>'non-empty-string', 'options='=>'int'], +'DOMDocument::loadXML' => ['bool', 'source'=>'non-empty-string', 'options='=>'int'], 'DOMDocument::normalizeDocument' => ['void'], 'DOMDocument::registerNodeClass' => ['bool', 'baseClass'=>'string', 'extendedClass'=>'?string'], 'DOMDocument::relaxNGValidate' => ['bool', 'filename'=>'string'], @@ -2942,7 +2942,7 @@ 'gc_enable' => ['void'], 'gc_enabled' => ['bool'], 'gc_mem_caches' => ['int'], -'gc_status' => ['array{runs:int,collected:int,threshold:int,roots:int,running:bool,protected:bool,full:bool,buffer_size:int}'], +'gc_status' => ['array{runs:int,collected:int,threshold:int,roots:int,running:bool,protected:bool,full:bool,buffer_size:int,application_time:float,collector_time:float,destructor_time:float,free_time:float}'], 'gd_info' => ['array'], 'gearman_bugreport' => [''], 'gearman_client_add_options' => ['', 'client_object'=>'', 'option'=>''], @@ -7825,10 +7825,10 @@ 'mysqli::begin_transaction' => ['bool', 'flags='=>'int', 'name='=>'?string'], 'mysqli::change_user' => ['bool', 'username'=>'string', 'password'=>'string', 'database'=>'?string'], 'mysqli::character_set_name' => ['string'], -'mysqli::close' => ['bool'], +'mysqli::close' => ['true'], 'mysqli::commit' => ['bool', 'flags='=>'int', 'name='=>'?string'], 'mysqli::connect' => ['bool', 'hostname='=>'string|null', 'username='=>'string|null', 'password='=>'string|null', 'database='=>'string|null', 'port='=>'int|null', 'socket='=>'string|null'], -'mysqli::debug' => ['bool', 'options'=>'string'], +'mysqli::debug' => ['true', 'options'=>'string'], 'mysqli::dump_debug_info' => ['bool'], 'mysqli::escape_string' => ['string', 'string'=>'string'], 'mysqli::execute_query' => ['mysqli_result|bool', 'query'=>'non-empty-string', 'params='=>'list|null'], @@ -7857,7 +7857,7 @@ 'mysqli::select_db' => ['bool', 'database'=>'string'], 'mysqli::set_charset' => ['bool', 'charset'=>'string'], 'mysqli::set_opt' => ['bool', 'option'=>'int', 'value'=>'string|int'], -'mysqli::ssl_set' => ['bool', 'key'=>'?string', 'certificate'=>'?string', 'ca_certificate'=>'?string', 'ca_path'=>'?string', 'cipher_algos'=>'?string'], +'mysqli::ssl_set' => ['true', 'key'=>'?string', 'certificate'=>'?string', 'ca_certificate'=>'?string', 'ca_path'=>'?string', 'cipher_algos'=>'?string'], 'mysqli::stat' => ['string|false'], 'mysqli::stmt_init' => ['mysqli_stmt'], 'mysqli::store_result' => ['mysqli_result|false', 'mode='=>'int'], @@ -7903,7 +7903,7 @@ 'mysqli_fetch_object' => ['object|false|null', 'result'=>'mysqli_result', 'class='=>'string', 'constructor_args='=>'array'], 'mysqli_fetch_row' => ['list|false|null', 'result'=>'mysqli_result'], 'mysqli_field_count' => ['int', 'mysql'=>'mysqli'], -'mysqli_field_seek' => ['bool', 'result'=>'mysqli_result', 'index'=>'int'], +'mysqli_field_seek' => ['true', 'result'=>'mysqli_result', 'index'=>'int'], 'mysqli_field_tell' => ['int', 'result'=>'mysqli_result'], 'mysqli_free_result' => ['void', 'result'=>'mysqli_result'], 'mysqli_get_cache_stats' => ['array|false'], @@ -7957,7 +7957,7 @@ 'mysqli_result::fetch_fields' => ['list'], 'mysqli_result::fetch_object' => ['object|false|null', 'class='=>'string', 'constructor_args='=>'array'], 'mysqli_result::fetch_row' => ['list|false|null'], -'mysqli_result::field_seek' => ['bool', 'index'=>'int'], +'mysqli_result::field_seek' => ['true', 'index'=>'int'], 'mysqli_result::free' => ['void'], 'mysqli_result::free_result' => ['void'], 'mysqli_rollback' => ['bool', 'mysql'=>'mysqli', 'flags='=>'int', 'name='=>'?string'], @@ -7981,7 +7981,7 @@ 'mysqli_stmt::attr_set' => ['bool', 'attribute'=>'int', 'value'=>'int'], 'mysqli_stmt::bind_param' => ['bool', 'types'=>'string', '&var'=>'mixed', '&...vars='=>'mixed'], 'mysqli_stmt::bind_result' => ['bool', '&w_var1'=>'', '&...w_vars='=>''], -'mysqli_stmt::close' => ['bool'], +'mysqli_stmt::close' => ['true'], 'mysqli_stmt::data_seek' => ['void', 'offset'=>'int'], 'mysqli_stmt::execute' => ['bool', 'params='=>'list|null'], 'mysqli_stmt::fetch' => ['bool|null'], @@ -12987,7 +12987,7 @@ 'strpbrk' => ['string|false', 'string'=>'string', 'characters'=>'string'], 'strpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'strptime' => ['array|false', 'timestamp'=>'string', 'format'=>'string'], -'strrchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string'], +'strrchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], 'strrev' => ['string', 'string'=>'string'], 'strripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index 134b79a0ce6..a9cde9ff992 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -100,6 +100,22 @@ 'old' => ['DOMNodeList', 'namespace'=>'string', 'localName'=>'string'], 'new' => ['DOMNodeList', 'namespace'=>'?string', 'localName'=>'string'], ], + 'DOMDocument::load' => [ + 'old' => ['DOMDocument|bool', 'filename'=>'string', 'options='=>'int'], + 'new' => ['bool', 'filename'=>'string', 'options='=>'int'], + ], + 'DOMDocument::loadXML' => [ + 'old' => ['DOMDocument|bool', 'source'=>'non-empty-string', 'options='=>'int'], + 'new' => ['bool', 'source'=>'non-empty-string', 'options='=>'int'], + ], + 'DOMDocument::loadHTML' => [ + 'old' => ['DOMDocument|bool', 'source'=>'non-empty-string', 'options='=>'int'], + 'new' => ['bool', 'source'=>'non-empty-string', 'options='=>'int'], + ], + 'DOMDocument::loadHTMLFile' => [ + 'old' => ['DOMDocument|bool', 'filename'=>'string', 'options='=>'int'], + 'new' => ['bool', 'filename'=>'string', 'options='=>'int'], + ], 'DOMImplementation::createDocument' => [ 'old' => ['DOMDocument|false', 'namespace='=>'string', 'qualifiedName='=>'string', 'doctype='=>'DOMDocumentType'], 'new' => ['DOMDocument|false', 'namespace='=>'?string', 'qualifiedName='=>'string', 'doctype='=>'?DOMDocumentType'], @@ -224,6 +240,14 @@ 'old' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], 'new' => ['string', 'locale'=>'string', 'displayLocale='=>'?string'], ], + 'mysqli_field_seek' => [ + 'old' => ['bool', 'result'=>'mysqli_result', 'index'=>'int'], + 'new' => ['true', 'result'=>'mysqli_result', 'index'=>'int'], + ], + 'mysqli_result::field_seek' => [ + 'old' => ['bool', 'index'=>'int'], + 'new' => ['true', 'index'=>'int'], + ], 'mysqli_stmt::__construct' => [ 'old' => ['void', 'mysql'=>'mysqli', 'query='=>'string'], 'new' => ['void', 'mysql'=>'mysqli', 'query='=>'?string'], diff --git a/dictionaries/CallMap_81_delta.php b/dictionaries/CallMap_81_delta.php index 3bd304ea2f4..18cc1ae4792 100644 --- a/dictionaries/CallMap_81_delta.php +++ b/dictionaries/CallMap_81_delta.php @@ -94,6 +94,10 @@ 'old' => ['int|false', 'fields'=>'array', 'separator='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'new' => ['int|false', 'fields'=>'array', 'separator='=>'string', 'enclosure='=>'string', 'escape='=>'string', 'eol='=>'string'], ], + 'hash_pbkdf2' => [ + 'old' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool'], + 'new' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool', 'options=' => 'array'], + ], 'finfo_buffer' => [ 'old' => ['string|false', 'finfo'=>'resource', 'string'=>'string', 'flags='=>'int', 'context='=>'resource'], 'new' => ['string|false', 'finfo'=>'finfo', 'string'=>'string', 'flags='=>'int', 'context='=>'resource'], diff --git a/dictionaries/CallMap_83_delta.php b/dictionaries/CallMap_83_delta.php index 9bcf76deece..8a4a76077b8 100644 --- a/dictionaries/CallMap_83_delta.php +++ b/dictionaries/CallMap_83_delta.php @@ -23,7 +23,7 @@ 'changed' => [ 'gc_status' => [ 'old' => ['array{runs:int,collected:int,threshold:int,roots:int}'], - 'new' => ['array{runs:int,collected:int,threshold:int,roots:int,running:bool,protected:bool,full:bool,buffer_size:int}'], + 'new' => ['array{runs:int,collected:int,threshold:int,roots:int,running:bool,protected:bool,full:bool,buffer_size:int,application_time:float,collector_time:float,destructor_time:float,free_time:float}'], ], 'srand' => [ 'old' => ['void', 'seed='=>'int', 'mode='=>'int'], @@ -49,10 +49,6 @@ 'old' => ['bool', '&rw_array'=>'array', 'flags='=>'int'], 'new' => ['true', '&rw_array'=>'array', 'flags='=>'int'], ], - 'hash_pbkdf2' => [ - 'old' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool'], - 'new' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool', 'options=' => 'array'], - ], 'imap_setflag_full' => [ 'old' => ['bool', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flag'=>'string', 'options='=>'int'], 'new' => ['true', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flag'=>'string', 'options='=>'int'], @@ -117,6 +113,10 @@ 'old' => ['?bool', 'text'=>'string'], 'new' => ['bool', 'text'=>'string'], ], + 'strrchr' => [ + 'old' => ['string|false', 'haystack'=>'string', 'needle'=>'string'], + 'new' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], + ], ], 'removed' => [ diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index d20efff9f0f..9565bfa203d 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -733,8 +733,8 @@ 'DOMDocument::getElementsByTagNameNS' => ['DOMNodeList', 'namespace'=>'string', 'localName'=>'string'], 'DOMDocument::importNode' => ['DOMNode|false', 'node'=>'DOMNode', 'deep='=>'bool'], 'DOMDocument::load' => ['DOMDocument|bool', 'filename'=>'string', 'options='=>'int'], - 'DOMDocument::loadHTML' => ['bool', 'source'=>'non-empty-string', 'options='=>'int'], - 'DOMDocument::loadHTMLFile' => ['bool', 'filename'=>'string', 'options='=>'int'], + 'DOMDocument::loadHTML' => ['DOMDocument|bool', 'source'=>'non-empty-string', 'options='=>'int'], + 'DOMDocument::loadHTMLFile' => ['DOMDocument|bool', 'filename'=>'string', 'options='=>'int'], 'DOMDocument::loadXML' => ['DOMDocument|bool', 'source'=>'non-empty-string', 'options='=>'int'], 'DOMDocument::normalizeDocument' => ['void'], 'DOMDocument::registerNodeClass' => ['bool', 'baseClass'=>'string', 'extendedClass'=>'?string'], @@ -847,11 +847,11 @@ 'DateTimeInterface::getOffset' => ['int'], 'DateTimeInterface::getTimestamp' => ['int|false'], 'DateTimeInterface::getTimezone' => ['DateTimeZone|false'], - 'DateTimeZone::__construct' => ['void', 'timezone'=>'string'], + 'DateTimeZone::__construct' => ['void', 'timezone'=>'non-empty-string'], 'DateTimeZone::__set_state' => ['DateTimeZone', 'array'=>'array'], 'DateTimeZone::__wakeup' => ['void'], 'DateTimeZone::getLocation' => ['array|false'], - 'DateTimeZone::getName' => ['string'], + 'DateTimeZone::getName' => ['non-empty-string'], 'DateTimeZone::getOffset' => ['int|false', 'datetime'=>'DateTimeInterface'], 'DateTimeZone::getTransitions' => ['list|false', 'timestampBegin='=>'int', 'timestampEnd='=>'int'], 'DateTimeZone::listAbbreviations' => ['array>'], @@ -9820,8 +9820,8 @@ 'date_create_immutable' => ['DateTimeImmutable|false', 'datetime='=>'string', 'timezone='=>'?DateTimeZone'], 'date_create_immutable_from_format' => ['DateTimeImmutable|false', 'format'=>'string', 'datetime'=>'string', 'timezone='=>'?DateTimeZone'], 'date_date_set' => ['DateTime|false', 'object'=>'DateTime', 'year'=>'int', 'month'=>'int', 'day'=>'int'], - 'date_default_timezone_get' => ['string'], - 'date_default_timezone_set' => ['bool', 'timezoneId'=>'string'], + 'date_default_timezone_get' => ['non-empty-string'], + 'date_default_timezone_set' => ['bool', 'timezoneId'=>'non-empty-string'], 'date_diff' => ['DateInterval|false', 'baseObject'=>'DateTimeInterface', 'targetObject'=>'DateTimeInterface', 'absolute='=>'bool'], 'date_format' => ['string|false', 'object'=>'DateTimeInterface', 'format'=>'string'], 'date_get_last_errors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}|false'], @@ -12737,10 +12737,10 @@ 'mysqli::begin_transaction' => ['bool', 'flags='=>'int', 'name='=>'string'], 'mysqli::change_user' => ['bool', 'username'=>'string', 'password'=>'string', 'database'=>'?string'], 'mysqli::character_set_name' => ['string'], - 'mysqli::close' => ['bool'], + 'mysqli::close' => ['true'], 'mysqli::commit' => ['bool', 'flags='=>'int', 'name='=>'string'], 'mysqli::connect' => ['null|false', 'hostname='=>'string', 'username='=>'string', 'password='=>'string', 'database='=>'string', 'port='=>'int', 'socket='=>'string'], - 'mysqli::debug' => ['bool', 'options'=>'string'], + 'mysqli::debug' => ['true', 'options'=>'string'], 'mysqli::dump_debug_info' => ['bool'], 'mysqli::escape_string' => ['string', 'string'=>'string'], 'mysqli::get_charset' => ['object'], @@ -12768,7 +12768,7 @@ 'mysqli::select_db' => ['bool', 'database'=>'string'], 'mysqli::set_charset' => ['bool', 'charset'=>'string'], 'mysqli::set_opt' => ['bool', 'option'=>'int', 'value'=>'string|int'], - 'mysqli::ssl_set' => ['bool', 'key'=>'?string', 'certificate'=>'?string', 'ca_certificate'=>'?string', 'ca_path'=>'?string', 'cipher_algos'=>'?string'], + 'mysqli::ssl_set' => ['true', 'key'=>'?string', 'certificate'=>'?string', 'ca_certificate'=>'?string', 'ca_path'=>'?string', 'cipher_algos'=>'?string'], 'mysqli::stat' => ['string|false'], 'mysqli::stmt_init' => ['mysqli_stmt'], 'mysqli::store_result' => ['mysqli_result|false', 'mode='=>'int'], @@ -12889,7 +12889,7 @@ 'mysqli_stmt::attr_set' => ['bool', 'attribute'=>'int', 'value'=>'int'], 'mysqli_stmt::bind_param' => ['bool', 'types'=>'string', '&var'=>'mixed', '&...vars='=>'mixed'], 'mysqli_stmt::bind_result' => ['bool', '&w_var1'=>'', '&...w_vars='=>''], - 'mysqli_stmt::close' => ['bool'], + 'mysqli_stmt::close' => ['true'], 'mysqli_stmt::data_seek' => ['void', 'offset'=>'int'], 'mysqli_stmt::execute' => ['bool'], 'mysqli_stmt::fetch' => ['bool|null'], diff --git a/dictionaries/ImpureFunctionsList.php b/dictionaries/ImpureFunctionsList.php new file mode 100644 index 00000000000..70b1fad7d92 --- /dev/null +++ b/dictionaries/ImpureFunctionsList.php @@ -0,0 +1,255 @@ + true, + 'chgrp' => true, + 'chmod' => true, + 'chown' => true, + 'chroot' => true, + 'copy' => true, + 'file_get_contents' => true, + 'file_put_contents' => true, + 'opendir' => true, + 'readdir' => true, + 'closedir' => true, + 'rewinddir' => true, + 'scandir' => true, + 'fopen' => true, + 'fread' => true, + 'fwrite' => true, + 'fclose' => true, + 'touch' => true, + 'fpassthru' => true, + 'fputs' => true, + 'fscanf' => true, + 'fseek' => true, + 'flock' => true, + 'ftruncate' => true, + 'fprintf' => true, + 'symlink' => true, + 'mkdir' => true, + 'unlink' => true, + 'rename' => true, + 'rmdir' => true, + 'popen' => true, + 'pclose' => true, + 'fgetcsv' => true, + 'fputcsv' => true, + 'umask' => true, + 'finfo_open' => true, + 'finfo_close' => true, + 'finfo_file' => true, + 'stream_set_timeout' => true, + 'fgets' => true, + 'fflush' => true, + 'move_uploaded_file' => true, + 'file_exists' => true, + 'realpath' => true, + 'glob' => true, + 'is_readable' => true, + 'is_dir' => true, + 'is_file' => true, + // stream/socket io + 'stream_context_set_option' => true, + 'socket_write' => true, + 'stream_set_blocking' => true, + 'socket_close' => true, + 'socket_set_option' => true, + 'stream_set_write_buffer' => true, + 'stream_socket_enable_crypto' => true, + 'stream_copy_to_stream' => true, + 'stream_wrapper_register' => true, + 'socket_connect' => true, + 'socket_bind' => true, + 'socket_set_block' => true, + 'socket_set_nonblock' => true, + 'socket_listen' => true, + // meta calls + 'call_user_func' => true, + 'call_user_func_array' => true, + 'define' => true, + 'create_function' => true, + // http + 'header' => true, + 'header_remove' => true, + 'http_response_code' => true, + 'setcookie' => true, + 'setrawcookie' => true, + // output buffer + 'ob_start' => true, + 'ob_end_clean' => true, + 'ob_get_clean' => true, + 'readfile' => true, + 'printf' => true, + 'var_dump' => true, + 'phpinfo' => true, + 'ob_implicit_flush' => true, + 'vprintf' => true, + // mcrypt + 'mcrypt_generic_init' => true, + 'mcrypt_generic_deinit' => true, + 'mcrypt_module_close' => true, + // internal optimisation + 'opcache_compile_file' => true, + 'clearstatcache' => true, + // process-related + 'pcntl_signal' => true, + 'pcntl_alarm' => true, + 'posix_kill' => true, + 'cli_set_process_title' => true, + 'pcntl_async_signals' => true, + 'proc_close' => true, + 'proc_nice' => true, + 'proc_open' => true, + 'proc_terminate' => true, + // curl + 'curl_setopt' => true, + 'curl_close' => true, + 'curl_multi_add_handle' => true, + 'curl_multi_remove_handle' => true, + 'curl_multi_select' => true, + 'curl_multi_close' => true, + 'curl_setopt_array' => true, + // apc, apcu + 'apc_store' => true, + 'apc_delete' => true, + 'apc_clear_cache' => true, + 'apc_add' => true, + 'apc_inc' => true, + 'apc_dec' => true, + 'apc_cas' => true, + 'apcu_store' => true, + 'apcu_delete' => true, + 'apcu_clear_cache' => true, + 'apcu_add' => true, + 'apcu_inc' => true, + 'apcu_dec' => true, + 'apcu_cas' => true, + // gz + 'gzwrite' => true, + 'gzrewind' => true, + 'gzseek' => true, + 'gzclose' => true, + // newrelic + 'newrelic_start_transaction' => true, + 'newrelic_name_transaction' => true, + 'newrelic_add_custom_parameter' => true, + 'newrelic_add_custom_tracer' => true, + 'newrelic_background_job' => true, + 'newrelic_end_transaction' => true, + 'newrelic_set_appname' => true, + // execution + 'shell_exec' => true, + 'exec' => true, + 'system' => true, + 'passthru' => true, + 'pcntl_exec' => true, + // well-known functions + 'libxml_use_internal_errors' => true, + 'libxml_disable_entity_loader' => true, + 'curl_exec' => true, + 'mt_srand' => true, + 'openssl_pkcs7_sign' => true, + 'openssl_sign' => true, + 'mt_rand' => true, + 'rand' => true, + 'random_int' => true, + 'random_bytes' => true, + 'wincache_ucache_delete' => true, + 'wincache_ucache_set' => true, + 'wincache_ucache_inc' => true, + 'class_alias' => true, + 'class_exists' => true, // impure by virtue of triggering autoloader + 'enum_exists' => true, // impure by virtue of triggering autoloader + // php environment + 'ini_set' => true, + 'sleep' => true, + 'usleep' => true, + 'register_shutdown_function' => true, + 'error_reporting' => true, + 'register_tick_function' => true, + 'unregister_tick_function' => true, + 'set_error_handler' => true, + 'user_error' => true, + 'trigger_error' => true, + 'restore_error_handler' => true, + 'date_default_timezone_set' => true, + 'assert_options' => true, + 'setlocale' => true, + 'set_exception_handler' => true, + 'set_time_limit' => true, + 'putenv' => true, + 'spl_autoload_register' => true, + 'spl_autoload_unregister' => true, + 'microtime' => true, + 'array_rand' => true, + 'set_include_path' => true, + // logging + 'openlog' => true, + 'syslog' => true, + 'error_log' => true, + 'define_syslog_variables' => true, + // session + 'session_id' => true, + 'session_decode' => true, + 'session_name' => true, + 'session_set_cookie_params' => true, + 'session_set_save_handler' => true, + 'session_regenerate_id' => true, + 'mb_internal_encoding' => true, + 'session_start' => true, + 'session_cache_limiter' => true, + // ldap + 'ldap_set_option' => true, + // iterators + 'rewind' => true, + 'iterator_apply' => true, + 'iterator_to_array' => true, + // mysqli + 'mysqli_select_db' => true, + 'mysqli_dump_debug_info' => true, + 'mysqli_kill' => true, + 'mysqli_multi_query' => true, + 'mysqli_next_result' => true, + 'mysqli_options' => true, + 'mysqli_ping' => true, + 'mysqli_query' => true, + 'mysqli_report' => true, + 'mysqli_rollback' => true, + 'mysqli_savepoint' => true, + 'mysqli_set_charset' => true, + 'mysqli_ssl_set' => true, + 'mysqli_close' => true, + // script execution + 'ignore_user_abort' => true, + // ftp + 'ftp_close' => true, + 'ftp_pasv' => true, + // bcmath + 'bcscale' => true, + // json + 'json_last_error' => true, + // opcache + 'opcache_compile_file' => true, + 'opcache_get_configuration' => true, + 'opcache_get_status' => true, + 'opcache_invalidate' => true, + 'opcache_is_script_cached' => true, + 'opcache_reset' => true, + //gettext + 'bindtextdomain' => true, + // hash + 'hash_update' => true, + 'hash_update_file' => true, + 'hash_update_stream' => true, + // unserialize + 'unserialize' => true, + // openssl + 'openssl_csr_export_to_file' => true, + 'openssl_pkcs12_export_to_file' => true, + 'openssl_pkey_export_to_file' => true, + 'openssl_x509_export_to_file' => true, +]; diff --git a/docs/annotating_code/type_syntax/array_types.md b/docs/annotating_code/type_syntax/array_types.md index 2846a9a220d..1021ab1fcab 100644 --- a/docs/annotating_code/type_syntax/array_types.md +++ b/docs/annotating_code/type_syntax/array_types.md @@ -14,7 +14,7 @@ $a = [1, 2, 3, 4, 5]; ```php 'hello', 5 => 'goodbye']; -$b = ['a' => 'AA', 'b' => 'BB', 'c' => 'CC'] +$b = ['a' => 'AA', 'b' => 'BB', 'c' => 'CC']; ``` Makeshift [Structs](https://en.wikipedia.org/wiki/Struct_(C_programming_language)): diff --git a/docs/contributing/adding_issues.md b/docs/contributing/adding_issues.md index b609d872837..4cc2fabb4b9 100644 --- a/docs/contributing/adding_issues.md +++ b/docs/contributing/adding_issues.md @@ -17,8 +17,8 @@ namespace Psalm\Issue; final class MyNewIssue extends CodeIssue { - public const SHORTCODE = 123; public const ERROR_LEVEL = 2; + public const SHORTCODE = 123; } ``` @@ -26,7 +26,7 @@ For `SHORTCODE` value use `$max_shortcode + 1`. To choose appropriate error leve There a number of abstract classes you can extend: -* `CodeIssue` - non specific, default issue. It's a base class for all issues. +* `CodeIssue` - non-specific, default issue. It's a base class for all issues. * `ClassIssue` - issue related to a specific class (also interface, trait, enum). These issues can be suppressed for specific classes in `psalm.xml` by using `referencedClass` attribute * `PropertyIssue` - issue related to a specific property. Can be targeted by using `referencedProperty` in `psalm.xml` * `FunctionIssue` - issue related to a specific function. Can be suppressed with `referencedFunction` attribute. diff --git a/docs/running_psalm/configuration.md b/docs/running_psalm/configuration.md index f4976aaad83..05f65236f0c 100644 --- a/docs/running_psalm/configuration.md +++ b/docs/running_psalm/configuration.md @@ -213,7 +213,7 @@ When `true`, Psalm will check that the developer has caught every exception in g ignoreInternalFunctionFalseReturn="[bool]" > ``` -When `true`, Psalm ignores possibly-false issues stemming from return values of internal functions (like `preg_split`) that may return false, but do so rarely. Defaults to `true`. +When `true`, Psalm ignores possibly-false issues stemming from return values of internal functions (like `preg_split`) that may return false, but do so rarely. Defaults to `false`. #### ignoreInternalFunctionNullReturn @@ -222,7 +222,7 @@ When `true`, Psalm ignores possibly-false issues stemming from return values of ignoreInternalFunctionNullReturn="[bool]" > ``` -When `true`, Psalm ignores possibly-null issues stemming from return values of internal array functions (like `current`) that may return null, but do so rarely. Defaults to `true`. +When `true`, Psalm ignores possibly-null issues stemming from return values of internal array functions (like `current`) that may return null, but do so rarely. Defaults to `false`. #### inferPropertyTypesFromConstructor diff --git a/docs/running_psalm/error_levels.md b/docs/running_psalm/error_levels.md index 55a18b8fa61..9bb001277c3 100644 --- a/docs/running_psalm/error_levels.md +++ b/docs/running_psalm/error_levels.md @@ -29,6 +29,7 @@ Level 5 and above allows a more non-verifiable code, and higher levels are even - [DuplicateFunction](issues/DuplicateFunction.md) - [DuplicateMethod](issues/DuplicateMethod.md) - [DuplicateParam](issues/DuplicateParam.md) + - [DuplicateProperty](issues/DuplicateProperty.md) - [EmptyArrayAccess](issues/EmptyArrayAccess.md) - [ExtensionRequirementViolation](issues/ExtensionRequirementViolation.md) - [ImplementationRequirementViolation](issues/ImplementationRequirementViolation.md) @@ -100,6 +101,7 @@ Level 5 and above allows a more non-verifiable code, and higher levels are even - [ContinueOutsideLoop](issues/ContinueOutsideLoop.md) - [InvalidTypeImport](issues/InvalidTypeImport.md) - [MethodSignatureMismatch](issues/MethodSignatureMismatch.md) +- [NonVariableReferenceReturn](issues/NonVariableReferenceReturn.md) - [OverriddenMethodAccess](issues/OverriddenMethodAccess.md) - [ParamNameMismatch](issues/ParamNameMismatch.md) - [ReservedWord](issues/ReservedWord.md) @@ -292,11 +294,13 @@ Level 5 and above allows a more non-verifiable code, and higher levels are even - [TaintedInput](issues/TaintedInput.md) - [TaintedLdap](issues/TaintedLdap.md) - [TaintedShell](issues/TaintedShell.md) + - [TaintedSleep](issues/TaintedSleep.md) - [TaintedSql](issues/TaintedSql.md) - [TaintedSSRF](issues/TaintedSSRF.md) - [TaintedSystemSecret](issues/TaintedSystemSecret.md) - [TaintedUnserialize](issues/TaintedUnserialize.md) - [TaintedUserSecret](issues/TaintedUserSecret.md) + - [TaintedXpath](issues/TaintedXpath.md) - [UncaughtThrowInGlobalScope](issues/UncaughtThrowInGlobalScope.md) - [UnevaluatedCode](issues/UnevaluatedCode.md) - [UnnecessaryVarAnnotation](issues/UnnecessaryVarAnnotation.md) diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md index d9b3b4f168a..f2655635cf1 100644 --- a/docs/running_psalm/issues.md +++ b/docs/running_psalm/issues.md @@ -31,6 +31,7 @@ - [DuplicateFunction](issues/DuplicateFunction.md) - [DuplicateMethod](issues/DuplicateMethod.md) - [DuplicateParam](issues/DuplicateParam.md) + - [DuplicateProperty](issues/DuplicateProperty.md) - [EmptyArrayAccess](issues/EmptyArrayAccess.md) - [ExtensionRequirementViolation](issues/ExtensionRequirementViolation.md) - [FalsableReturnStatement](issues/FalsableReturnStatement.md) @@ -151,6 +152,7 @@ - [NonInvariantDocblockPropertyType](issues/NonInvariantDocblockPropertyType.md) - [NonInvariantPropertyType](issues/NonInvariantPropertyType.md) - [NonStaticSelfCall](issues/NonStaticSelfCall.md) + - [NonVariableReferenceReturn](issues/NonVariableReferenceReturn.md) - [NoValue](issues/NoValue.md) - [NullableReturnStatement](issues/NullableReturnStatement.md) - [NullArgument](issues/NullArgument.md) @@ -240,12 +242,14 @@ - [TaintedInput](issues/TaintedInput.md) - [TaintedLdap](issues/TaintedLdap.md) - [TaintedShell](issues/TaintedShell.md) + - [TaintedSleep](issues/TaintedSleep.md) - [TaintedSql](issues/TaintedSql.md) - [TaintedSSRF](issues/TaintedSSRF.md) - [TaintedSystemSecret](issues/TaintedSystemSecret.md) - [TaintedTextWithQuotes](issues/TaintedTextWithQuotes.md) - [TaintedUnserialize](issues/TaintedUnserialize.md) - [TaintedUserSecret](issues/TaintedUserSecret.md) + - [TaintedXpath](issues/TaintedXpath.md) - [TooFewArguments](issues/TooFewArguments.md) - [TooManyArguments](issues/TooManyArguments.md) - [TooManyTemplateParams](issues/TooManyTemplateParams.md) diff --git a/docs/running_psalm/issues/DuplicateProperty.md b/docs/running_psalm/issues/DuplicateProperty.md new file mode 100644 index 00000000000..1a7cf0e6e59 --- /dev/null +++ b/docs/running_psalm/issues/DuplicateProperty.md @@ -0,0 +1,19 @@ +# DuplicateProperty + +Emitted when a class property is defined twice + +```php +xpath($expression); +} +``` diff --git a/docs/running_psalm/plugins/plugins_type_system.md b/docs/running_psalm/plugins/plugins_type_system.md index 5cf70ad94e7..99e6fa6b807 100644 --- a/docs/running_psalm/plugins/plugins_type_system.md +++ b/docs/running_psalm/plugins/plugins_type_system.md @@ -183,8 +183,6 @@ $a = []; foreach (range(1,1) as $_) $a[(string)rand(0,1)] = rand(0,1); // array ``` -`TCallableArray` - denotes an array that is _also_ `callable`. - `TCallableKeyedArray` - denotes an object-like array that is _also_ `callable`. `TClassStringMap` - Represents an array where the type of each value is a function of its string key value diff --git a/examples/TemplateChecker.php b/examples/TemplateChecker.php index 55e65d2300f..60d0f822534 100644 --- a/examples/TemplateChecker.php +++ b/examples/TemplateChecker.php @@ -160,7 +160,7 @@ protected function checkWithViewClass(Context $context, array $stmts): void } } - $pseudo_method_name = preg_replace('/[^a-zA-Z0-9_]+/', '_', $this->file_name); + $pseudo_method_name = (string) preg_replace('/[^a-zA-Z0-9_]+/', '_', $this->file_name); $class_method = new VirtualClassMethod($pseudo_method_name, ['stmts' => []]); diff --git a/examples/plugins/StringChecker.php b/examples/plugins/StringChecker.php index 621997dd965..43629bdf65e 100644 --- a/examples/plugins/StringChecker.php +++ b/examples/plugins/StringChecker.php @@ -35,6 +35,7 @@ public static function afterExpressionAnalysis(AfterExpressionAnalysisEvent $eve && strpos($expr->value, 'TestController') === false && preg_match($class_or_class_method, $expr->value) ) { + /** @psalm-suppress PossiblyInvalidArrayAccess */ $absolute_class = preg_split('/[:]/', $expr->value)[0]; IssueBuffer::maybeAdd( new InvalidClass( diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 6d475011ab7..fff3a06b79f 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -12,6 +12,11 @@ $matches[1] + + + $deprecated_element_xml + + @@ -282,6 +287,11 @@ props[0]]]> + + + $buffer + + $config @@ -448,9 +458,6 @@ - classExtendsOrImplements - classExtendsOrImplements - classExtendsOrImplements classOrInterfaceExists classOrInterfaceExists classOrInterfaceExists @@ -571,8 +578,6 @@ $const_name - $type[0] - $type[0][0] diff --git a/src/Psalm/CodeLocation.php b/src/Psalm/CodeLocation.php index 05ca3c388a5..063a6f498f8 100644 --- a/src/Psalm/CodeLocation.php +++ b/src/Psalm/CodeLocation.php @@ -224,7 +224,7 @@ private function calculateRealLocation(): void $indentation = (int)strpos($key_line, '@'); - $key_line = trim(preg_replace('@\**/\s*@', '', mb_strcut($key_line, $indentation))); + $key_line = trim((string) preg_replace('@\**/\s*@', '', mb_strcut($key_line, $indentation))); $this->selection_start = $preview_offset + $indentation + $this->preview_start; $this->selection_end = $this->selection_start + strlen($key_line); diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 64244ab83b3..a2a4f5405f2 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -686,6 +686,8 @@ public function classOrInterfaceExists( /** * Check whether a class/interface exists + * + * @psalm-assert-if-true class-string|interface-string|enum-string $fq_class_name */ public function classOrInterfaceOrEnumExists( string $fq_class_name, @@ -701,6 +703,7 @@ public function classOrInterfaceOrEnumExists( ); } + /** @psalm-mutation-free */ public function classExtendsOrImplements(string $fq_class_name, string $possible_parent): bool { return $this->classlikes->classExtends($fq_class_name, $possible_parent) @@ -969,7 +972,7 @@ public function getMarkupContentForSymbolByReference( //Direct Assignment if (is_numeric($reference->symbol[0])) { return new PHPMarkdownContent( - preg_replace( + (string) preg_replace( '/^[^:]*:/', '', $reference->symbol, @@ -1008,7 +1011,7 @@ public function getMarkupContentForSymbolByReference( //Class Property if (strpos($reference->symbol, '$') !== false) { - $property_id = preg_replace('/^\\\\/', '', $reference->symbol); + $property_id = (string) preg_replace('/^\\\\/', '', $reference->symbol); /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ [$fq_class_name, $property_name] = explode('::$', $property_id); $class_storage = $this->classlikes->getStorageFor($fq_class_name); @@ -1180,7 +1183,7 @@ public function getMarkupContentForSymbolByReference( public function getSymbolLocationByReference(Reference $reference): ?CodeLocation { if (is_numeric($reference->symbol[0])) { - $symbol = preg_replace('/:.*/', '', $reference->symbol); + $symbol = (string) preg_replace('/:.*/', '', $reference->symbol); $symbol_parts = explode('-', $symbol); if (!isset($symbol_parts[0]) || !isset($symbol_parts[1])) { @@ -1788,7 +1791,7 @@ public function getCompletionItemsForPartialSymbol( ) { $file_contents = $this->getFileContents($file_path); - $class_name = preg_replace('/^.*\\\/', '', $fq_class_name, 1); + $class_name = (string) preg_replace('/^.*\\\/', '', $fq_class_name, 1); if ($aliases->uses_end) { $position = self::getPositionFromOffset($aliases->uses_end, $file_contents); diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 8f39ae6771b..3b39ed8f3f0 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -44,7 +44,6 @@ use Psalm\Progress\VoidProgress; use RuntimeException; use SimpleXMLElement; -use SimpleXMLIterator; use Symfony\Component\Filesystem\Path; use Throwable; use UnexpectedValueException; @@ -404,12 +403,12 @@ class Config /** * @var bool */ - public $ignore_internal_falsable_issues = true; + public $ignore_internal_falsable_issues = false; /** * @var bool */ - public $ignore_internal_nullable_issues = true; + public $ignore_internal_nullable_issues = false; /** * @var array @@ -718,8 +717,6 @@ protected function __construct() $this->eventDispatcher = new EventDispatcher(); $this->universal_object_crates = [ strtolower(stdClass::class), - strtolower(SimpleXMLElement::class), - strtolower(SimpleXMLIterator::class), ]; } @@ -845,6 +842,7 @@ private static function loadDomDocument(string $base_dir, string $file_contents) $dom_document->loadXML($file_contents, LIBXML_NONET); $dom_document->xinclude(LIBXML_NOWARNING | LIBXML_NONET); + /** @psalm-suppress PossiblyFalseArgument */ chdir($oldpwd); return $dom_document; } @@ -1020,7 +1018,6 @@ private static function processConfigDeprecations( /** * @param non-empty-string $file_contents - * @psalm-suppress MixedMethodCall * @psalm-suppress MixedAssignment * @psalm-suppress MixedArgument * @psalm-suppress MixedPropertyFetch @@ -1109,7 +1106,9 @@ private static function fromXmlAndPaths( $composer_json = null; if (file_exists($composer_json_path)) { - $composer_json = json_decode(file_get_contents($composer_json_path), true); + $composer_json_contents = file_get_contents($composer_json_path); + assert($composer_json_contents !== false); + $composer_json = json_decode($composer_json_contents, true); if (!is_array($composer_json)) { throw new UnexpectedValueException('Invalid composer.json at ' . $composer_json_path); } @@ -1151,12 +1150,13 @@ private static function fromXmlAndPaths( } if (isset($config_xml['autoloader'])) { - $autoloader_path = $config->base_dir . DIRECTORY_SEPARATOR . $config_xml['autoloader']; + $autoloader = (string) $config_xml['autoloader']; + $autoloader_path = $config->base_dir . DIRECTORY_SEPARATOR . $autoloader; if (!file_exists($autoloader_path)) { // in here for legacy reasons where people put absolute paths but psalm resolved it relative - if ($config_xml['autoloader']->__toString()[0] === '/') { - $autoloader_path = $config_xml['autoloader']->__toString(); + if ($autoloader[0] === '/') { + $autoloader_path = $autoloader; } if (!file_exists($autoloader_path)) { @@ -1164,7 +1164,7 @@ private static function fromXmlAndPaths( } } - $config->autoloader = realpath($autoloader_path); + $config->autoloader = (string) realpath($autoloader_path); } if (isset($config_xml['cacheDirectory'])) { @@ -1294,7 +1294,7 @@ private static function fromXmlAndPaths( ); } - if (isset($config_xml->fileExtensions)) { + if (isset($config_xml->fileExtensions->extension)) { $config->file_extensions = []; $config->loadFileExtensions($config_xml->fileExtensions->extension); @@ -1317,7 +1317,6 @@ private static function fromXmlAndPaths( if (isset($config_xml->ignoreExceptions)) { if (isset($config_xml->ignoreExceptions->class)) { - /** @var SimpleXMLElement $exception_class */ foreach ($config_xml->ignoreExceptions->class as $exception_class) { $exception_name = (string) $exception_class['name']; $global_attribute_text = (string) $exception_class['onlyGlobalScope']; @@ -1328,7 +1327,6 @@ private static function fromXmlAndPaths( } } if (isset($config_xml->ignoreExceptions->classAndDescendants)) { - /** @var SimpleXMLElement $exception_class */ foreach ($config_xml->ignoreExceptions->classAndDescendants as $exception_class) { $exception_name = (string) $exception_class['name']; $global_attribute_text = (string) $exception_class['onlyGlobalScope']; @@ -1382,7 +1380,6 @@ private static function fromXmlAndPaths( // this plugin loading system borrows heavily from etsy/phan if (isset($config_xml->plugins)) { if (isset($config_xml->plugins->plugin)) { - /** @var SimpleXMLElement $plugin */ foreach ($config_xml->plugins->plugin as $plugin) { $plugin_file_name = (string) $plugin['filename']; @@ -1394,7 +1391,6 @@ private static function fromXmlAndPaths( } } if (isset($config_xml->plugins->pluginClass)) { - /** @var SimpleXMLElement $plugin */ foreach ($config_xml->plugins->pluginClass as $plugin) { $plugin_class_name = $plugin['class']; // any child elements are used as plugin configuration @@ -1410,21 +1406,23 @@ private static function fromXmlAndPaths( if (isset($config_xml->issueHandlers)) { foreach ($config_xml->issueHandlers as $issue_handlers) { - /** @var SimpleXMLElement $issue_handler */ - foreach ($issue_handlers->children() as $key => $issue_handler) { - if ($key === 'PluginIssue') { - $custom_class_name = (string) $issue_handler['name']; - /** @var string $key */ - $config->issue_handlers[$custom_class_name] = IssueHandler::loadFromXMLElement( - $issue_handler, - $base_dir, - ); - } else { - /** @var string $key */ - $config->issue_handlers[$key] = IssueHandler::loadFromXMLElement( - $issue_handler, - $base_dir, - ); + $issue_handler_children = $issue_handlers->children(); + if ($issue_handler_children) { + foreach ($issue_handler_children as $key => $issue_handler) { + if ($key === 'PluginIssue') { + $custom_class_name = (string)$issue_handler['name']; + /** @var string $key */ + $config->issue_handlers[$custom_class_name] = IssueHandler::loadFromXMLElement( + $issue_handler, + $base_dir, + ); + } else { + /** @var string $key */ + $config->issue_handlers[$key] = IssueHandler::loadFromXMLElement( + $issue_handler, + $base_dir, + ); + } } } } @@ -1467,19 +1465,36 @@ public function setAdvancedErrorLevel(string $issue_key, array $config, ?string $this->issue_handlers[$issue_key]->setCustomLevels($config, $this->base_dir); } + public function safeSetAdvancedErrorLevel( + string $issue_key, + array $config, + ?string $default_error_level = null + ): void { + if (!isset($this->issue_handlers[$issue_key])) { + $this->setAdvancedErrorLevel($issue_key, $config, $default_error_level); + } + } + public function setCustomErrorLevel(string $issue_key, string $error_level): void { $this->issue_handlers[$issue_key] = new IssueHandler(); $this->issue_handlers[$issue_key]->setErrorLevel($error_level); } + public function safeSetCustomErrorLevel(string $issue_key, string $error_level): void + { + if (!isset($this->issue_handlers[$issue_key])) { + $this->setCustomErrorLevel($issue_key, $error_level); + } + } + /** * @throws ConfigException if a Config file could not be found */ private function loadFileExtensions(SimpleXMLElement $extensions): void { foreach ($extensions as $extension) { - $extension_name = preg_replace('/^\.?/', '', (string)$extension['name'], 1); + $extension_name = (string) preg_replace('/^\.?/', '', (string)$extension['name'], 1); $this->file_extensions[] = $extension_name; if (isset($extension['scanner'])) { @@ -1714,7 +1729,7 @@ private function getPluginClassForPath(Codebase $codebase, string $path, string public function shortenFileName(string $to): string { if (!is_file($to)) { - return preg_replace('/^' . preg_quote($this->base_dir, '/') . '/', '', $to, 1); + return (string) preg_replace('/^' . preg_quote($this->base_dir, '/') . '/', '', $to, 1); } $from = $this->base_dir; @@ -1896,7 +1911,7 @@ public static function getParentIssueType(string $issue_type): ?string } if (strpos($issue_type, 'Possibly') === 0) { - $stripped_issue_type = preg_replace('/^Possibly(False|Null)?/', '', $issue_type, 1); + $stripped_issue_type = (string) preg_replace('/^Possibly(False|Null)?/', '', $issue_type, 1); if (strpos($stripped_issue_type, 'Invalid') === false && strpos($stripped_issue_type, 'Un') !== 0) { $stripped_issue_type = 'Invalid' . $stripped_issue_type; @@ -2038,7 +2053,7 @@ public function getReportingLevelForFunction(string $issue_type, string $functio if ($level === null && $issue_type === 'UndefinedFunction') { // undefined functions trigger global namespace fallback // so we should also check reporting levels for the symbol in global scope - $root_function_id = preg_replace('/.*\\\/', '', $function_id); + $root_function_id = (string) preg_replace('/.*\\\/', '', $function_id); if ($root_function_id !== $function_id) { /** @psalm-suppress PossiblyUndefinedStringArrayOffset https://github.com/vimeo/psalm/issues/7656 */ $level = $this->issue_handlers[$issue_type]->getReportingLevelForFunction($root_function_id); @@ -2244,6 +2259,10 @@ public function visitStubFiles(Codebase $codebase, ?Progress $progress = null): $stubsDir . 'SPL.phpstub', ]; + if ($codebase->analysis_php_version_id >= 7_04_00) { + $this->internal_stubs[] = $stubsDir . 'Php74.phpstub'; + } + if ($codebase->analysis_php_version_id >= 8_00_00) { $this->internal_stubs[] = $stubsDir . 'CoreGenericAttributes.phpstub'; $this->internal_stubs[] = $stubsDir . 'Php80.phpstub'; @@ -2279,9 +2298,10 @@ public function visitStubFiles(Codebase $codebase, ?Progress $progress = null): if (is_file($phpstorm_meta_path)) { $stub_files[] = $phpstorm_meta_path; } elseif (is_dir($phpstorm_meta_path)) { - $phpstorm_meta_path = realpath($phpstorm_meta_path); + $phpstorm_meta_path = (string) realpath($phpstorm_meta_path); + $phpstorm_meta_files = glob($phpstorm_meta_path . '/*.meta.php', GLOB_NOSORT); - foreach (glob($phpstorm_meta_path . '/*.meta.php', GLOB_NOSORT) as $glob) { + foreach ($phpstorm_meta_files ?: [] as $glob) { if (is_file($glob) && realpath(dirname($glob)) === $phpstorm_meta_path) { $stub_files[] = $glob; } @@ -2489,7 +2509,7 @@ public function getPotentialComposerFilePathForClassLike(string $class): ?string && $this->isInProjectDirs($dir . DIRECTORY_SEPARATOR . 'testdummy.php') ) { $maxDepth = $depth; - $candidate_path = realpath($dir) . $pathEnd; + $candidate_path = (string) realpath($dir) . $pathEnd; } } } @@ -2624,7 +2644,9 @@ public function getPHPVersionFromComposerJson(): ?string if (file_exists($composer_json_path)) { try { - $composer_json = json_decode(file_get_contents($composer_json_path), true, 512, JSON_THROW_ON_ERROR); + $composer_json_contents = file_get_contents($composer_json_path); + assert($composer_json_contents !== false); + $composer_json = json_decode($composer_json_contents, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $e) { $composer_json = null; } diff --git a/src/Psalm/Config/Creator.php b/src/Psalm/Config/Creator.php index d24a4cebc54..aa52bee23e1 100644 --- a/src/Psalm/Config/Creator.php +++ b/src/Psalm/Config/Creator.php @@ -17,6 +17,7 @@ use function array_sum; use function array_unique; use function array_values; +use function assert; use function count; use function explode; use function file_exists; @@ -198,8 +199,10 @@ public static function getPaths(string $current_dir, ?string $suggested_dir): ar ); } try { + $composer_json_contents = file_get_contents($composer_json_location); + assert($composer_json_contents !== false); $composer_json = json_decode( - file_get_contents($composer_json_location), + $composer_json_contents, true, 512, JSON_THROW_ON_ERROR, @@ -263,7 +266,7 @@ private static function getPsr4Or0Paths(string $current_dir, array $composer_jso continue; } - $path = preg_replace('@[/\\\]$@', '', $path, 1); + $path = (string) preg_replace('@[/\\\]$@', '', $path, 1); if ($path !== 'tests') { $nodes[] = ''; @@ -287,9 +290,9 @@ private static function guessPhpFileDirs(string $current_dir): array /** @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), + 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) { diff --git a/src/Psalm/Config/FileFilter.php b/src/Psalm/Config/FileFilter.php index dc6bd55f498..b0410026369 100644 --- a/src/Psalm/Config/FileFilter.php +++ b/src/Psalm/Config/FileFilter.php @@ -14,6 +14,7 @@ use function array_map; use function array_merge; use function array_shift; +use function assert; use function count; use function explode; use function glob; @@ -138,7 +139,11 @@ public static function loadFromArray( if (strpos($prospective_directory_path, '*') !== false) { // Strip meaningless trailing recursive wildcard like "path/**/" or "path/**" - $prospective_directory_path = preg_replace('#(\/\*\*)+\/?$#', '/', $prospective_directory_path); + $prospective_directory_path = (string) preg_replace( + '#(\/\*\*)+\/?$#', + '/', + $prospective_directory_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_directory_path); @@ -208,7 +213,7 @@ public static function loadFromArray( while ($iterator->valid()) { if ($iterator->isLink()) { - $linked_path = readlink($iterator->getPathname()); + $linked_path = (string) readlink($iterator->getPathname()); if (stripos($linked_path, $directory_path) !== 0) { if ($ignore_type_stats && $filter instanceof ProjectFileFilter) { @@ -288,7 +293,7 @@ public static function loadFromArray( continue; } - $file_path = realpath($prospective_file_path); + $file_path = (string) realpath($prospective_file_path); if (!$file_path) { if ($allow_missing_files) { @@ -342,7 +347,8 @@ public static function loadFromArray( foreach ($config['referencedFunction'] as $referenced_function) { $function_id = $referenced_function['name'] ?? ''; if (!is_string($function_id) - || (!preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $function_id) + || (!preg_match('/^[a-zA-Z_\x80-\xff](?:[\\\\]?[a-zA-Z0-9_\x80-\xff]+)*$/', $function_id) + && !preg_match('/^[^:]+::[^:]+$/', $function_id) // methods are also allowed && !static::isRegularExpression($function_id))) { throw new ConfigException( 'Invalid referencedFunction ' . ((string) $function_id), @@ -391,7 +397,6 @@ public static function loadFromXMLElement( if ($e->directory) { $config['directory'] = []; - /** @var SimpleXMLElement $directory */ foreach ($e->directory as $directory) { $config['directory'][] = [ 'name' => (string) $directory['name'], @@ -404,7 +409,6 @@ public static function loadFromXMLElement( if ($e->file) { $config['file'] = []; - /** @var SimpleXMLElement $file */ foreach ($e->file as $file) { $config['file'][]['name'] = (string) $file['name']; } @@ -412,7 +416,6 @@ public static function loadFromXMLElement( if ($e->referencedClass) { $config['referencedClass'] = []; - /** @var SimpleXMLElement $referenced_class */ foreach ($e->referencedClass as $referenced_class) { $config['referencedClass'][]['name'] = strtolower((string)$referenced_class['name']); } @@ -420,7 +423,6 @@ public static function loadFromXMLElement( if ($e->referencedMethod) { $config['referencedMethod'] = []; - /** @var SimpleXMLElement $referenced_method */ foreach ($e->referencedMethod as $referenced_method) { $config['referencedMethod'][]['name'] = (string)$referenced_method['name']; } @@ -428,7 +430,6 @@ public static function loadFromXMLElement( if ($e->referencedFunction) { $config['referencedFunction'] = []; - /** @var SimpleXMLElement $referenced_function */ foreach ($e->referencedFunction as $referenced_function) { $config['referencedFunction'][]['name'] = strtolower((string)$referenced_function['name']); } @@ -436,7 +437,6 @@ public static function loadFromXMLElement( if ($e->referencedProperty) { $config['referencedProperty'] = []; - /** @var SimpleXMLElement $referenced_property */ foreach ($e->referencedProperty as $referenced_property) { $config['referencedProperty'][]['name'] = strtolower((string)$referenced_property['name']); } @@ -444,7 +444,6 @@ public static function loadFromXMLElement( if ($e->referencedConstant) { $config['referencedConstant'] = []; - /** @var SimpleXMLElement $referenced_constant */ foreach ($e->referencedConstant as $referenced_constant) { $config['referencedConstant'][]['name'] = strtolower((string)$referenced_constant['name']); } @@ -452,8 +451,6 @@ public static function loadFromXMLElement( if ($e->referencedVariable) { $config['referencedVariable'] = []; - - /** @var SimpleXMLElement $referenced_variable */ foreach ($e->referencedVariable as $referenced_variable) { $config['referencedVariable'][]['name'] = strtolower((string)$referenced_variable['name']); } @@ -503,6 +500,7 @@ private static function recursiveGlob(array $parts, bool $only_dir): array $first_dir = self::slashify($parts[0]); $paths = glob($first_dir . '*', GLOB_ONLYDIR | GLOB_NOSORT); + assert($paths !== false); $result = []; foreach ($paths as $path) { $parts[0] = $path; diff --git a/src/Psalm/Config/IssueHandler.php b/src/Psalm/Config/IssueHandler.php index fe8a2037d02..a5af5aefe4b 100644 --- a/src/Psalm/Config/IssueHandler.php +++ b/src/Psalm/Config/IssueHandler.php @@ -10,6 +10,7 @@ use function array_filter; use function array_map; +use function assert; use function dirname; use function in_array; use function scandir; @@ -40,9 +41,10 @@ public static function loadFromXMLElement(SimpleXMLElement $e, string $base_dir) } } - /** @var SimpleXMLElement $error_level */ - foreach ($e->errorLevel as $error_level) { - $handler->custom_levels[] = ErrorLevelFileFilter::loadFromXMLElement($error_level, $base_dir, true); + if (isset($e->errorLevel)) { + foreach ($e->errorLevel as $error_level) { + $handler->custom_levels[] = ErrorLevelFileFilter::loadFromXMLElement($error_level, $base_dir, true); + } } return $handler; @@ -158,10 +160,12 @@ public function getReportingLevelForVariable(string $var_name): ?string */ public static function getAllIssueTypes(): array { + $scan = scandir(dirname(__DIR__) . '/Issue', SCANDIR_SORT_NONE); + assert($scan !== false); return array_filter( array_map( static fn(string $file_name): string => substr($file_name, 0, -4), - scandir(dirname(__DIR__) . '/Issue', SCANDIR_SORT_NONE), + $scan, ), static fn(string $issue_name): bool => $issue_name !== '' && $issue_name !== 'MethodIssue' diff --git a/src/Psalm/Config/ProjectFileFilter.php b/src/Psalm/Config/ProjectFileFilter.php index c14392189e5..e3ffd4b20f0 100644 --- a/src/Psalm/Config/ProjectFileFilter.php +++ b/src/Psalm/Config/ProjectFileFilter.php @@ -27,7 +27,6 @@ public static function loadFromXMLElement( throw new ConfigException('Cannot nest ignoreFiles inside itself'); } - /** @var SimpleXMLElement $e->ignoreFiles */ $filter->file_filter = static::loadFromXMLElement($e->ignoreFiles, $base_dir, false); } diff --git a/src/Psalm/Context.php b/src/Psalm/Context.php index f19033f4fc2..23824f67fae 100644 --- a/src/Psalm/Context.php +++ b/src/Psalm/Context.php @@ -14,7 +14,8 @@ use Psalm\Internal\Type\AssertionReconciler; use Psalm\Storage\FunctionLikeStorage; use Psalm\Type\Atomic\DependentType; -use Psalm\Type\Atomic\TArray; +use Psalm\Type\Atomic\TIntRange; +use Psalm\Type\Atomic\TNull; use Psalm\Type\Union; use RuntimeException; @@ -845,7 +846,7 @@ public function hasVariable(string $var_name): bool return false; } - $stripped_var = preg_replace('/(->|\[).*$/', '', $var_name, 1); + $stripped_var = (string) preg_replace('/(->|\[).*$/', '', $var_name, 1); if ($stripped_var !== '$this' || $var_name !== $stripped_var) { $this->cond_referenced_var_ids[$var_name] = true; @@ -870,10 +871,19 @@ public function getScopeSummary(): string public function defineGlobals(): void { $globals = [ + // not sure why this is declared here again, see VariableFetchAnalyzer '$argv' => new Union([ - new TArray([Type::getInt(), Type::getString()]), + Type::getNonEmptyListAtomic(Type::getString()), + new TNull(), + ], [ + 'ignore_nullable_issues' => true, + ]), + '$argc' => new Union([ + new TIntRange(1, null), + new TNull(), + ], [ + 'ignore_nullable_issues' => true, ]), - '$argc' => Type::getInt(), ]; $config = Config::getInstance(); diff --git a/src/Psalm/ErrorBaseline.php b/src/Psalm/ErrorBaseline.php index c1374d5a970..ab963a352a1 100644 --- a/src/Psalm/ErrorBaseline.php +++ b/src/Psalm/ErrorBaseline.php @@ -246,7 +246,7 @@ private static function writeToFile( $filesNode->setAttribute('php-version', implode(';' . "\n\t", [...[ ('php:' . PHP_VERSION), ], ...array_map( - static fn(string $extension): string => $extension . ':' . phpversion($extension), + static fn(string $extension): string => $extension . ':' . (string) phpversion($extension), $extensions, )])); } diff --git a/src/Psalm/FileBasedPluginAdapter.php b/src/Psalm/FileBasedPluginAdapter.php index 7d69c9f6343..882e1b7d85b 100644 --- a/src/Psalm/FileBasedPluginAdapter.php +++ b/src/Psalm/FileBasedPluginAdapter.php @@ -13,6 +13,7 @@ use function assert; use function class_exists; +use function count; use function reset; use function str_replace; @@ -65,6 +66,8 @@ private function getPluginClassForPath(string $path): string $declared_classes = ClassLikeAnalyzer::getClassesForFile($codebase, $path); + assert(count($declared_classes) > 0, 'FileBasedPlugin contains a class'); + return reset($declared_classes); } } diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index eb8f40f47b5..a541e15c7b8 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -38,6 +38,7 @@ use Psalm\Issue\ExtensionRequirementViolation; use Psalm\Issue\ImplementationRequirementViolation; use Psalm\Issue\InaccessibleMethod; +use Psalm\Issue\InheritorViolation; use Psalm\Issue\InternalClass; use Psalm\Issue\InvalidEnumCaseValue; use Psalm\Issue\InvalidExtendClass; @@ -76,6 +77,8 @@ use Psalm\Storage\MethodStorage; use Psalm\Type; use Psalm\Type\Atomic\TGenericObject; +use Psalm\Type\Atomic\TLiteralInt; +use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNull; @@ -95,8 +98,6 @@ use function explode; use function implode; use function in_array; -use function is_int; -use function is_string; use function preg_match; use function preg_replace; use function reset; @@ -273,6 +274,22 @@ public function analyze( ); } + $class_union = new Union([new TNamedObject($fq_class_name)]); + foreach ($storage->parent_classes + $storage->direct_class_interfaces as $parent_class) { + $parent_storage = $codebase->classlikes->getStorageFor($parent_class); + if ($parent_storage && $parent_storage->inheritors) { + if (!UnionTypeComparator::isContainedBy($codebase, $class_union, $parent_storage->inheritors)) { + IssueBuffer::maybeAdd( + new InheritorViolation( + 'Class ' . $fq_class_name . ' is not an allowed inheritor of parent class ' . $parent_class, + new CodeLocation($this, $this->class), + ), + $this->getSuppressedIssues(), + ); + } + } + } + if ($storage->template_types) { foreach ($storage->template_types as $param_name => $_) { @@ -2485,8 +2502,8 @@ private function checkEnum(): void ), ); } elseif ($case_storage->value !== null) { - if ((is_int($case_storage->value) && $storage->enum_type === 'string') - || (is_string($case_storage->value) && $storage->enum_type === 'int') + if (($case_storage->value instanceof TLiteralInt && $storage->enum_type === 'string') + || ($case_storage->value instanceof TLiteralString && $storage->enum_type === 'int') ) { IssueBuffer::maybeAdd( new InvalidEnumCaseValue( @@ -2499,7 +2516,7 @@ private function checkEnum(): void } if ($case_storage->value !== null) { - if (in_array($case_storage->value, $seen_values, true)) { + if (in_array($case_storage->value->value, $seen_values, true)) { IssueBuffer::maybeAdd( new DuplicateEnumCaseValue( 'Enum case values should be unique', @@ -2508,7 +2525,7 @@ private function checkEnum(): void ), ); } else { - $seen_values[] = $case_storage->value; + $seen_values[] = $case_storage->value->value; } } } diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index 786cd4fe24d..cf54d72c3ab 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -16,7 +16,6 @@ use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Issue\InaccessibleProperty; -use Psalm\Issue\InheritorViolation; use Psalm\Issue\InvalidClass; use Psalm\Issue\InvalidTemplateParam; use Psalm\Issue\MissingDependency; @@ -31,7 +30,6 @@ use Psalm\StatementsSource; use Psalm\Storage\ClassLikeStorage; use Psalm\Type; -use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; use UnexpectedValueException; @@ -229,7 +227,7 @@ public static function checkFullyQualifiedClassLikeName( return null; } - $fq_class_name = preg_replace('/^\\\/', '', $fq_class_name, 1); + $fq_class_name = (string) preg_replace('/^\\\/', '', $fq_class_name, 1); if (in_array($fq_class_name, ['callable', 'iterable', 'self', 'static', 'parent'], true)) { return true; @@ -334,23 +332,6 @@ public static function checkFullyQualifiedClassLikeName( return null; } - - $classUnion = new Union([new TNamedObject($fq_class_name)]); - foreach ($class_storage->parent_classes + $class_storage->direct_class_interfaces as $parent_class) { - $parent_storage = $codebase->classlikes->getStorageFor($parent_class); - if ($parent_storage && $parent_storage->inheritors) { - if (!UnionTypeComparator::isContainedBy($codebase, $classUnion, $parent_storage->inheritors)) { - IssueBuffer::maybeAdd( - new InheritorViolation( - 'Class ' . $fq_class_name . ' is not an allowed inheritor of parent class ' . $parent_class, - $code_location, - ), - $suppressed_issues, - ); - } - } - } - foreach ($class_storage->invalid_dependencies as $dependency_class_name => $_) { // if the implemented/extended class is stubbed, it may not yet have // been hydrated diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index b1b5323db33..2d4880fd55f 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -154,7 +154,7 @@ public static function arrayToDocblocks( } else { $description = trim(substr($var_line, strlen($line_parts[0]) + 1)); } - $description = preg_replace('/\\n \\*\\s+/um', ' ', $description); + $description = (string) preg_replace('/\\n \\*\\s+/um', ' ', $description); } } @@ -261,8 +261,8 @@ private static function decorateVarDocblockComment( */ public static function sanitizeDocblockType(string $docblock_type): string { - $docblock_type = preg_replace('@^[ \t]*\*@m', '', $docblock_type); - $docblock_type = preg_replace('/,\n\s+}/', '}', $docblock_type); + $docblock_type = (string) preg_replace('@^[ \t]*\*@m', '', $docblock_type); + $docblock_type = (string) preg_replace('/,\n\s+}/', '}', $docblock_type); return str_replace("\n", '', $docblock_type); } @@ -399,7 +399,7 @@ public static function splitDocLine(string $return_block): array continue; } - $remaining = trim(preg_replace('@^[ \t]*\* *@m', ' ', substr($return_block, $i + 1))); + $remaining = trim((string) preg_replace('@^[ \t]*\* *@m', ' ', substr($return_block, $i + 1))); if ($remaining) { return array_merge([rtrim($type)], preg_split('/\s+/', $remaining) ?: []); diff --git a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php index abca24a9dba..0edb40528c4 100644 --- a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php @@ -13,9 +13,13 @@ use Psalm\Internal\Analyzer\Statements\Expression\ClassConstAnalyzer; use Psalm\Internal\FileManipulation\FileManipulationBuffer; use Psalm\Internal\Provider\NodeDataProvider; +use Psalm\Internal\Type\Comparator\UnionTypeComparator; +use Psalm\Issue\InheritorViolation; use Psalm\Issue\ParseError; use Psalm\Issue\UndefinedInterface; use Psalm\IssueBuffer; +use Psalm\Type\Atomic\TNamedObject; +use Psalm\Type\Union; use UnexpectedValueException; use function strtolower; @@ -111,6 +115,23 @@ public function analyze(): void } } + $class_union = new Union([new TNamedObject($fq_interface_name)]); + foreach ($class_storage->direct_interface_parents as $parent_interface) { + $parent_storage = $codebase->classlikes->getStorageFor($parent_interface); + if ($parent_storage && $parent_storage->inheritors) { + if (!UnionTypeComparator::isContainedBy($codebase, $class_union, $parent_storage->inheritors)) { + IssueBuffer::maybeAdd( + new InheritorViolation( + 'Interface ' . $fq_interface_name . ' + is not an allowed inheritor of parent interface ' . $parent_interface, + new CodeLocation($this, $this->class), + ), + $this->getSuppressedIssues(), + ); + } + } + } + $fq_interface_name = $this->getFQCLN(); if (!$fq_interface_name) { diff --git a/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php b/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php index 964e4dc2f54..16afcb9b83b 100644 --- a/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php @@ -212,7 +212,7 @@ public static function isWithinAny(string $calling_identifier, array $identifier */ public static function getNameSpaceRoot(string $fullyQualifiedClassName): string { - $root_namespace = preg_replace('/^([^\\\]+).*/', '$1', $fullyQualifiedClassName, 1); + $root_namespace = (string) preg_replace('/^([^\\\]+).*/', '$1', $fullyQualifiedClassName, 1); if ($root_namespace === "") { throw new InvalidArgumentException("Invalid classname \"$fullyQualifiedClassName\""); } diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index c8ea83edf14..f2464a1c4ad 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -202,6 +202,10 @@ class ProjectAnalyzer UnnecessaryVarAnnotation::class, ]; + private const PHP_VERSION_REGEX = '^(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\..*)?$'; + + private const PHP_SUPPORTED_VERSIONS_REGEX = '^(5\.[456]|7\.[01234]|8\.[0123])(\..*)?$'; + /** * @param array $generated_report_options */ @@ -1181,8 +1185,16 @@ public function refactorCodeAfterCompletion(array $to_refactor): void */ public function setPhpVersion(string $version, string $source): void { - if (!preg_match('/^(5\.[456]|7\.[01234]|8\.[012])(\..*)?$/', $version)) { - throw new UnexpectedValueException('Expecting a version number in the format x.y'); + if (!preg_match('/' . self::PHP_VERSION_REGEX . '/', $version)) { + throw new UnexpectedValueException('Expecting a version number in the format x.y or x.y.z'); + } + + if (!preg_match('/' . self::PHP_SUPPORTED_VERSIONS_REGEX . '/', $version)) { + throw new UnexpectedValueException( + 'Psalm supports PHP version ">=5.4". The specified version ' + . $version + . " is either not supported or doesn't exist.", + ); } [$php_major_version, $php_minor_version] = explode('.', $version); diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php index 8fb30e90d4e..e419a6a3747 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php @@ -751,20 +751,6 @@ public static function handleIterable( $has_valid_iterator = true; - if ($iterator_atomic_type instanceof TNamedObject - && strtolower($iterator_atomic_type->value) === 'simplexmlelement' - ) { - $value_type = Type::combineUnionTypes( - $value_type, - new Union([$iterator_atomic_type]), - ); - - $key_type = Type::combineUnionTypes( - $key_type, - Type::getString(), - ); - } - if ($iterator_atomic_type instanceof TIterable || (strtolower($iterator_atomic_type->value) === 'traversable' || $codebase->classImplements( diff --git a/src/Psalm/Internal/Analyzer/Statements/DeclareAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/DeclareAnalyzer.php new file mode 100644 index 00000000000..fed7eb3e1f1 --- /dev/null +++ b/src/Psalm/Internal/Analyzer/Statements/DeclareAnalyzer.php @@ -0,0 +1,108 @@ +declares as $declaration) { + $declaration_key = (string) $declaration->key; + + if ($declaration_key === 'strict_types') { + if ($stmt->stmts !== null) { + IssueBuffer::maybeAdd( + new UnrecognizedStatement( + 'strict_types declaration must not use block mode', + new CodeLocation($statements_analyzer, $stmt), + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + + self::analyzeStrictTypesDeclaration($statements_analyzer, $declaration, $context); + } elseif ($declaration_key === 'ticks') { + self::analyzeTicksDeclaration($statements_analyzer, $declaration); + } elseif ($declaration_key === 'encoding') { + self::analyzeEncodingDeclaration($statements_analyzer, $declaration); + } else { + IssueBuffer::maybeAdd( + new UnrecognizedStatement( + 'Psalm does not understand the declare statement ' . $declaration->key, + new CodeLocation($statements_analyzer, $declaration), + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + } + } + + private static function analyzeStrictTypesDeclaration( + StatementsAnalyzer $statements_analyzer, + PhpParser\Node\Stmt\DeclareDeclare $declaration, + Context $context + ): void { + if (!$declaration->value instanceof PhpParser\Node\Scalar\LNumber + || !in_array($declaration->value->value, [0, 1], true) + ) { + IssueBuffer::maybeAdd( + new UnrecognizedStatement( + 'strict_types declaration can only have 1 or 0 as a value', + new CodeLocation($statements_analyzer, $declaration), + ), + $statements_analyzer->getSuppressedIssues(), + ); + + return; + } + + if ($declaration->value->value === 1) { + $context->strict_types = true; + } + } + + private static function analyzeTicksDeclaration( + StatementsAnalyzer $statements_analyzer, + PhpParser\Node\Stmt\DeclareDeclare $declaration + ): void { + if (!$declaration->value instanceof PhpParser\Node\Scalar\LNumber) { + IssueBuffer::maybeAdd( + new UnrecognizedStatement( + 'ticks declaration should have integer as a value', + new CodeLocation($statements_analyzer, $declaration), + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + } + + private static function analyzeEncodingDeclaration( + StatementsAnalyzer $statements_analyzer, + PhpParser\Node\Stmt\DeclareDeclare $declaration + ): void { + if (!$declaration->value instanceof PhpParser\Node\Scalar\String_) { + IssueBuffer::maybeAdd( + new UnrecognizedStatement( + 'encoding declaration should have string as a value', + new CodeLocation($statements_analyzer, $declaration), + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + } +} diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php index e37aa729fc4..bbdd5e9f16b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php @@ -532,98 +532,103 @@ private static function analyzeOperands( $has_valid_right_operand = true; } - $result_type = Type::getArray(); - return null; } - $has_valid_right_operand = true; - $has_valid_left_operand = true; + if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Plus) { + $has_valid_right_operand = true; + $has_valid_left_operand = true; - if ($left_type_part instanceof TKeyedArray - && $right_type_part instanceof TKeyedArray - ) { - $definitely_existing_mixed_right_properties = array_diff_key( - $right_type_part->properties, - $left_type_part->properties, - ); + if ($left_type_part instanceof TKeyedArray + && $right_type_part instanceof TKeyedArray + ) { + $definitely_existing_mixed_right_properties = array_diff_key( + $right_type_part->properties, + $left_type_part->properties, + ); - $properties = $left_type_part->properties; - - foreach ($right_type_part->properties as $key => $type) { - if (!isset($properties[$key])) { - $properties[$key] = $type; - } elseif ($properties[$key]->possibly_undefined) { - $properties[$key] = Type::combineUnionTypes( - $properties[$key], - $type, - $codebase, - false, - true, - 500, - $type->possibly_undefined, - ); + $properties = $left_type_part->properties; + + foreach ($right_type_part->properties as $key => $type) { + if (!isset($properties[$key])) { + $properties[$key] = $type; + } elseif ($properties[$key]->possibly_undefined) { + $properties[$key] = Type::combineUnionTypes( + $properties[$key], + $type, + $codebase, + false, + true, + 500, + $type->possibly_undefined, + ); + } } - } - if ($left_type_part->fallback_params !== null) { - foreach ($definitely_existing_mixed_right_properties as $key => $type) { - $properties[$key] = Type::combineUnionTypes(Type::getMixed(), $type); + if ($left_type_part->fallback_params !== null) { + foreach ($definitely_existing_mixed_right_properties as $key => $type) { + $properties[$key] = Type::combineUnionTypes(Type::getMixed(), $type); + } } - } - if ($left_type_part->fallback_params === null - && $right_type_part->fallback_params === null - ) { - $fallback_params = null; - } elseif ($left_type_part->fallback_params !== null - && $right_type_part->fallback_params !== null - ) { - $fallback_params = [ - Type::combineUnionTypes( - $left_type_part->fallback_params[0], - $right_type_part->fallback_params[0], - ), - Type::combineUnionTypes( - $left_type_part->fallback_params[1], - $right_type_part->fallback_params[1], - ), - ]; + if ($left_type_part->fallback_params === null + && $right_type_part->fallback_params === null + ) { + $fallback_params = null; + } elseif ($left_type_part->fallback_params !== null + && $right_type_part->fallback_params !== null + ) { + $fallback_params = [ + Type::combineUnionTypes( + $left_type_part->fallback_params[0], + $right_type_part->fallback_params[0], + ), + Type::combineUnionTypes( + $left_type_part->fallback_params[1], + $right_type_part->fallback_params[1], + ), + ]; + } else { + $fallback_params = $left_type_part->fallback_params ?: $right_type_part->fallback_params; + } + + $new_keyed_array = new TKeyedArray( + $properties, + null, + $fallback_params, + ); + $result_type_member = new Union([$new_keyed_array]); } else { - $fallback_params = $left_type_part->fallback_params ?: $right_type_part->fallback_params; + $result_type_member = TypeCombiner::combine( + [$left_type_part, $right_type_part], + $codebase, + true, + ); } - $new_keyed_array = new TKeyedArray( - $properties, - null, - $fallback_params, - ); - $result_type_member = new Union([$new_keyed_array]); - } else { - $result_type_member = TypeCombiner::combine( - [$left_type_part, $right_type_part], - $codebase, - true, - ); - } + $result_type = Type::combineUnionTypes($result_type_member, $result_type, $codebase, true); - $result_type = Type::combineUnionTypes($result_type_member, $result_type, $codebase, true); + if ($left instanceof PhpParser\Node\Expr\ArrayDimFetch + && $context + && $statements_source instanceof StatementsAnalyzer + ) { + ArrayAssignmentAnalyzer::updateArrayType( + $statements_source, + $left, + $right, + $result_type, + $context, + ); + } - if ($left instanceof PhpParser\Node\Expr\ArrayDimFetch - && $context - && $statements_source instanceof StatementsAnalyzer - ) { - ArrayAssignmentAnalyzer::updateArrayType( - $statements_source, - $left, - $right, - $result_type, - $context, - ); + return null; } - - return null; } + /** + * @var Atomic $left_type_part + * @var Atomic $right_type_part + * // Todo remove this hint reset after fixing #10267 + */ if (($left_type_part instanceof TNamedObject && strtolower($left_type_part->value) === 'gmp') || ($right_type_part instanceof TNamedObject && strtolower($right_type_part->value) === 'gmp') diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 658784e08fb..15053f87ea0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -41,7 +41,6 @@ use Psalm\Type; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; -use Psalm\Type\Atomic\TCallableArray; use Psalm\Type\Atomic\TCallableKeyedArray; use Psalm\Type\Atomic\TClosure; use Psalm\Type\Atomic\TKeyedArray; @@ -1528,9 +1527,7 @@ private static function checkArgCount( foreach ($arg_value_type->getAtomicTypes() as $atomic_arg_type) { $packed_var_definite_args_tmp = []; - if ($atomic_arg_type instanceof TCallableArray || - $atomic_arg_type instanceof TCallableKeyedArray - ) { + if ($atomic_arg_type instanceof TCallableKeyedArray) { $packed_var_definite_args_tmp[] = 2; } elseif ($atomic_arg_type instanceof TKeyedArray) { if ($atomic_arg_type->fallback_params !== null) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php index 71b507ec9d5..ec731bca268 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php @@ -50,9 +50,6 @@ public static function collect( if ($static_class_storage->template_extended_params && $method_name && !empty($non_trait_class_storage->overridden_method_ids[$method_name]) - && isset($class_storage->methods[$method_name]) - && (!isset($non_trait_class_storage->methods[$method_name]->return_type) - || $class_storage->methods[$method_name]->inherited_return_type) ) { foreach ($non_trait_class_storage->overridden_method_ids[$method_name] as $overridden_method_id) { $overridden_storage = $codebase->methods->getStorage($overridden_method_id); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index cfe401b629d..72e17d8a413 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -768,7 +768,7 @@ private static function getAnalyzeNamedExpression( if (strpos($var_type_part->value, '::')) { $parts = explode('::', strtolower($var_type_part->value)); $fq_class_name = $parts[0]; - $fq_class_name = preg_replace('/^\\\/', '', $fq_class_name, 1); + $fq_class_name = (string) preg_replace('/^\\\/', '', $fq_class_name, 1); $potential_method_id = new MethodIdentifier($fq_class_name, $parts[1]); } else { $function_call_info->new_function_name = new VirtualFullyQualified( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index eb4ff708071..415e394f299 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -27,7 +27,6 @@ use Psalm\Type; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; -use Psalm\Type\Atomic\TCallableArray; use Psalm\Type\Atomic\TCallableKeyedArray; use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TClosure; @@ -360,9 +359,7 @@ private static function getReturnTypeFromCallMapWithArgs( if (count($atomic_types) === 1) { if (isset($atomic_types['array'])) { - if ($atomic_types['array'] instanceof TCallableArray - || $atomic_types['array'] instanceof TCallableKeyedArray - ) { + if ($atomic_types['array'] instanceof TCallableKeyedArray) { return Type::getInt(false, 2); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php index 0e479caf902..ff627e40f54 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php @@ -292,8 +292,7 @@ private static function isSupported(FunctionLikeParameter $container_param): boo return false; } - if ($a instanceof Type\Atomic\TCallableArray || - $a instanceof Type\Atomic\TCallableString || + if ($a instanceof Type\Atomic\TCallableString || $a instanceof Type\Atomic\TCallableKeyedArray ) { return false; 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 d2823b626a5..e72f142b39c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -17,6 +17,7 @@ use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Analyzer\TraitAnalyzer; +use Psalm\Internal\Codebase\AssertionsFromInheritanceResolver; use Psalm\Internal\Codebase\InternalCallMapHandler; use Psalm\Internal\FileManipulation\FileManipulationBuffer; use Psalm\Internal\MethodIdentifier; @@ -417,11 +418,14 @@ public static function analyze( } } - if ($method_storage->assertions) { + $assertionsResolver = new AssertionsFromInheritanceResolver($codebase); + $assertions = $assertionsResolver->resolve($method_storage, $class_storage); + + if ($assertions) { self::applyAssertionsToContext( $stmt_name, ExpressionIdentifier::getExtendedVarId($stmt->var, null, $statements_analyzer), - $method_storage->assertions, + $assertions, $args, $template_result, $context, 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 d5592a84bc9..9fee5ed5ac1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php @@ -185,6 +185,7 @@ public static function fetch( $self_fq_class_name, $statements_analyzer, $args, + $template_result, ); if ($return_type_candidate) { @@ -451,7 +452,7 @@ public static function taintMethodCallResult( $stmt_var_type = $context->vars_in_scope[$var_id]->setParentNodes( $var_nodes, ); - + $context->vars_in_scope[$var_id] = $stmt_var_type; } else { $method_call_node = DataFlowNode::getForMethodReturn( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php index 7a6bde5b6ed..6dfd989c581 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php @@ -31,6 +31,7 @@ use Psalm\IssueBuffer; use Psalm\Type; use Psalm\Type\Atomic\TNamedObject; +use Psalm\Type\Atomic\TObject; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; @@ -414,7 +415,7 @@ public static function analyze( $types = $class_type->getAtomicTypes(); foreach ($types as $key => &$type) { - if (!$type instanceof TNamedObject) { + if (!$type instanceof TNamedObject && !$type instanceof TObject) { unset($types[$key]); } else { $type = $type->setFromDocblock(false); 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 2f907c54329..cf292efb25f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -17,6 +17,7 @@ use Psalm\Internal\Analyzer\Statements\Expression\Call\StaticCallAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Codebase\AssertionsFromInheritanceResolver; use Psalm\Internal\FileManipulation\FileManipulationBuffer; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Type\Comparator\UnionTypeComparator; @@ -319,11 +320,17 @@ public static function analyze( } } - if ($method_storage->assertions) { + $assertionsResolver = new AssertionsFromInheritanceResolver($codebase); + $assertions = $assertionsResolver->resolve( + $method_storage, + $class_storage, + ); + + if ($assertions) { CallAnalyzer::applyAssertionsToContext( $stmt_name, null, - $method_storage->assertions, + $assertions, $stmt->getArgs(), $template_result, $context, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index 24655ecfbbe..666ccbc7d8a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -520,7 +520,7 @@ public static function getFunctionIdsFromCallableArg( } if ($callable_arg instanceof PhpParser\Node\Scalar\String_) { - $potential_id = preg_replace('/^\\\/', '', $callable_arg->value, 1); + $potential_id = (string) preg_replace('/^\\\/', '', $callable_arg->value, 1); if (preg_match('/^[A-Za-z0-9_]+(\\\[A-Za-z0-9_]+)*(::[A-Za-z0-9_]+)?$/', $potential_id)) { assert($potential_id !== ''); @@ -614,7 +614,7 @@ public static function checkFunctionExists( if (!$codebase->functions->functionExists($statements_analyzer, $function_id)) { /** @var non-empty-lowercase-string */ - $root_function_id = preg_replace('/.*\\\/', '', $function_id); + $root_function_id = (string) preg_replace('/.*\\\/', '', $function_id); if ($can_be_in_root_scope && $function_id !== $root_function_id diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index 16b37556fc1..79ac2bf19db 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php @@ -481,6 +481,17 @@ public static function getArrayAccessTypeGivenOffset( $key_values = []; + if ($codebase->store_node_types + && !$context->collect_initializations + && !$context->collect_mutations + ) { + $codebase->analyzer->addNodeType( + $statements_analyzer->getFilePath(), + $stmt->var, + $array_type->getId(), + ); + } + if ($stmt->dim instanceof PhpParser\Node\Scalar\String_) { $value_type = Type::getAtomicStringFromLiteral($stmt->dim->value); if ($value_type instanceof TLiteralString) { @@ -1734,8 +1745,12 @@ private static function handleArrayAccessOnNamedObject( ?Union &$array_access_type, bool &$has_array_access, ): void { - if (strtolower($type->value) === 'simplexmlelement') { - $call_array_access_type = new Union([new TNamedObject('SimpleXMLElement')]); + $codebase = $statements_analyzer->getCodebase(); + if (strtolower($type->value) === 'simplexmlelement' + || ($codebase->classExists($type->value) + && $codebase->classExtendsOrImplements($type->value, 'SimpleXMLElement')) + ) { + $call_array_access_type = new Union([new TNull(), new TNamedObject('SimpleXMLElement')]); } elseif (strtolower($type->value) === 'domnodelist' && $stmt->dim) { $old_data_provider = $statements_analyzer->node_data; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index 4fb88b112af..a6004cda383 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -53,7 +53,6 @@ use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TGenericObject; use Psalm\Type\Atomic\TInt; -use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNull; @@ -69,8 +68,6 @@ use function array_search; use function count; use function in_array; -use function is_int; -use function is_string; use function strtolower; use const ARRAY_FILTER_USE_KEY; @@ -230,7 +227,7 @@ public static function analyze( self::handleEnumValue($statements_analyzer, $stmt, $stmt_var_type, $class_storage); } elseif ($prop_name === 'name') { $has_valid_fetch_type = true; - self::handleEnumName($statements_analyzer, $stmt, $lhs_type_part); + self::handleEnumName($statements_analyzer, $stmt, $stmt_var_type, $class_storage); } else { self::handleNonExistentProperty( $statements_analyzer, @@ -981,16 +978,31 @@ public static function processUnspecialTaints( private static function handleEnumName( StatementsAnalyzer $statements_analyzer, PropertyFetch $stmt, - Atomic $lhs_type_part, + Union $stmt_var_type, + ClassLikeStorage $class_storage ): void { - if ($lhs_type_part instanceof TEnumCase) { - $statements_analyzer->node_data->setType( - $stmt, - new Union([Type::getAtomicStringFromLiteral($lhs_type_part->case_name)]), - ); - } else { - $statements_analyzer->node_data->setType($stmt, Type::getNonEmptyString()); + $relevant_enum_cases = array_filter( + $stmt_var_type->getAtomicTypes(), + static fn(Atomic $type): bool => $type instanceof TEnumCase, + ); + $relevant_enum_case_names = array_map( + static fn(TEnumCase $enumCase): string => $enumCase->case_name, + $relevant_enum_cases, + ); + + if (empty($relevant_enum_case_names)) { + $relevant_enum_case_names = array_keys($class_storage->enum_cases); } + + $statements_analyzer->node_data->setType( + $stmt, + empty($relevant_enum_case_names) + ? Type::getNonEmptyString() + : new Union(array_map( + fn(string $name): TString => Type::getAtomicStringFromLiteral($name), + $relevant_enum_case_names, + )), + ); } private static function handleEnumValue( @@ -1021,14 +1033,7 @@ private static function handleEnumValue( $case_values = []; foreach ($enum_cases as $enum_case) { - if (is_string($enum_case->value)) { - $case_values[] = Type::getAtomicStringFromLiteral($enum_case->value); - } elseif (is_int($enum_case->value)) { - $case_values[] = new TLiteralInt($enum_case->value); - } else { - // this should never happen - $case_values[] = new TMixed(); - } + $case_values[] = $enum_case->value ?? new TMixed(); } /** @psalm-suppress ArgumentTypeCoercion */ diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php index ae1800138ea..5677818a7dd 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php @@ -448,12 +448,12 @@ public static function normalizeFilePath(string $path_to_file): string $path_to_file = str_replace('/./', '/', $path_to_file); // first remove unnecessary / duplicates - $path_to_file = preg_replace('/\/[\/]+/', '/', $path_to_file); + $path_to_file = (string) preg_replace('/\/[\/]+/', '/', $path_to_file); $reduce_pattern = '/\/[^\/]+\/\.\.\//'; while (preg_match($reduce_pattern, $path_to_file)) { - $path_to_file = preg_replace($reduce_pattern, '/', $path_to_file, 1); + $path_to_file = (string) preg_replace($reduce_pattern, '/', $path_to_file, 1); } if (DIRECTORY_SEPARATOR !== '/') { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/IssetAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/IssetAnalyzer.php index 72acab392e5..9e54a4b5be3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/IssetAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/IssetAnalyzer.php @@ -5,9 +5,12 @@ 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\InvalidArgument; +use Psalm\IssueBuffer; use Psalm\Type; /** @@ -32,6 +35,15 @@ public static function analyze( $context->vars_in_scope[$var_id] = Type::getMixed(); $context->vars_possibly_in_scope[$var_id] = true; } + } elseif (!self::isValidStatement($isset_var)) { + IssueBuffer::maybeAdd( + new InvalidArgument( + 'Isset only works with variables and array elements', + new CodeLocation($statements_analyzer->getSource(), $isset_var), + 'empty', + ), + $statements_analyzer->getSuppressedIssues(), + ); } self::analyzeIssetVar($statements_analyzer, $isset_var, $context); @@ -51,4 +63,15 @@ public static function analyzeIssetVar( $context->inside_isset = false; } + + private static function isValidStatement(PhpParser\Node\Expr $stmt): bool + { + return $stmt instanceof PhpParser\Node\Expr\Variable + || $stmt instanceof PhpParser\Node\Expr\ArrayDimFetch + || $stmt instanceof PhpParser\Node\Expr\PropertyFetch + || $stmt instanceof PhpParser\Node\Expr\StaticPropertyFetch + || $stmt instanceof PhpParser\Node\Expr\NullsafePropertyFetch + || $stmt instanceof PhpParser\Node\Expr\ClassConstFetch + || $stmt instanceof PhpParser\Node\Expr\AssignRef; + } } diff --git a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php index 20e31b5586d..dc573299309 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php @@ -34,6 +34,7 @@ use Psalm\Issue\MixedReturnStatement; use Psalm\Issue\MixedReturnTypeCoercion; use Psalm\Issue\NoValue; +use Psalm\Issue\NonVariableReferenceReturn; use Psalm\Issue\NullableReturnStatement; use Psalm\IssueBuffer; use Psalm\Storage\FunctionLikeStorage; @@ -229,6 +230,23 @@ public static function analyze( $storage = $source->getFunctionLikeStorage($statements_analyzer); + if ($storage->signature_return_type + && $storage->signature_return_type->by_ref + && $stmt->expr !== null + && !($stmt->expr instanceof PhpParser\Node\Expr\Variable + || $stmt->expr instanceof PhpParser\Node\Expr\PropertyFetch + || $stmt->expr instanceof PhpParser\Node\Expr\StaticPropertyFetch + ) + ) { + IssueBuffer::maybeAdd( + new NonVariableReferenceReturn( + 'Only variable references should be returned by reference', + new CodeLocation($source, $stmt->expr), + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + $cased_method_id = $source->getCorrectlyCasedMethodId(); if ($stmt->expr && $storage->location) { diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index bd2223944dc..0ec734c1f3e 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -23,6 +23,7 @@ use Psalm\Internal\Analyzer\Statements\Block\WhileAnalyzer; use Psalm\Internal\Analyzer\Statements\BreakAnalyzer; use Psalm\Internal\Analyzer\Statements\ContinueAnalyzer; +use Psalm\Internal\Analyzer\Statements\DeclareAnalyzer; use Psalm\Internal\Analyzer\Statements\EchoAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\Assignment\InstancePropertyAssignmentAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer; @@ -599,14 +600,7 @@ private static function analyzeStatement( } elseif ($stmt instanceof PhpParser\Node\Stmt\Label) { // do nothing } elseif ($stmt instanceof PhpParser\Node\Stmt\Declare_) { - foreach ($stmt->declares as $declaration) { - if ((string) $declaration->key === 'strict_types' - && $declaration->value instanceof PhpParser\Node\Scalar\LNumber - && $declaration->value->value === 1 - ) { - $context->strict_types = true; - } - } + DeclareAnalyzer::analyze($statements_analyzer, $stmt, $context); } elseif ($stmt instanceof PhpParser\Node\Stmt\HaltCompiler) { $context->has_returned = true; } else { @@ -795,6 +789,7 @@ private function parseStatementDocblock( $comments = $this->parsed_docblock; if (isset($comments->tags['psalm-scope-this'])) { + assert(count($comments->tags['psalm-scope-this'])); $trimmed = trim(reset($comments->tags['psalm-scope-this'])); $scope_fqcn = Type::getFQCLNFromString($trimmed, $this->getAliases()); diff --git a/src/Psalm/Internal/Cache.php b/src/Psalm/Internal/Cache.php index 1bea1ef9dcc..5b0fb39408e 100644 --- a/src/Psalm/Internal/Cache.php +++ b/src/Psalm/Internal/Cache.php @@ -94,7 +94,7 @@ public function deleteItem(string $path): void public function saveItem(string $path, array|object|string $item): void { if ($this->use_igbinary) { - $serialized = igbinary_serialize($item); + $serialized = (string) igbinary_serialize($item); } else { $serialized = serialize($item); } diff --git a/src/Psalm/Internal/Clause.php b/src/Psalm/Internal/Clause.php index 62863dbd710..bcd98fcf580 100644 --- a/src/Psalm/Internal/Clause.php +++ b/src/Psalm/Internal/Clause.php @@ -14,6 +14,7 @@ use function array_diff; use function array_keys; +use function assert; use function count; use function hash; use function implode; @@ -189,6 +190,7 @@ public function __toString(): string if (count($var_id_clauses) > 1) { $clause_strings[] = '('.implode(') || (', $var_id_clauses).')'; } else { + assert(!empty($var_id_clauses)); $clause_strings[] = reset($var_id_clauses); } } @@ -197,6 +199,8 @@ public function __toString(): string return '(' . implode(') || (', $clause_strings) . ')'; } + assert(!empty($clause_strings)); + return reset($clause_strings); } diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 46f0c5423a4..837e23c82b9 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -112,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 !== '--') { - $arg_name = preg_replace('/=.*$/', '', substr($arg, 2), 1); + $arg_name = (string) preg_replace('/=.*$/', '', substr($arg, 2), 1); if (!in_array($arg_name, $valid_long_options, true) && !in_array($arg_name . ':', $valid_long_options, true) diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php index bc30e3c8aed..21285a405e4 100644 --- a/src/Psalm/Internal/Cli/Psalm.php +++ b/src/Psalm/Internal/Cli/Psalm.php @@ -402,7 +402,7 @@ public static function run(array $argv): void !$paths_to_check, $start_time, isset($options['stats']), - self::initBaseline($options, $config, $current_dir, $path_to_config), + self::initBaseline($options, $config, $current_dir, $path_to_config, $paths_to_check), ); } else { self::autoGenerateConfig($project_analyzer, $current_dir, $init_source_dir, $vendor_dir); @@ -455,7 +455,7 @@ private static function validateCliArguments(array $args): void array_map( static function (string $arg): void { if (strpos($arg, '--') === 0 && $arg !== '--') { - $arg_name = preg_replace('/=.*$/', '', substr($arg, 2), 1); + $arg_name = (string) preg_replace('/=.*$/', '', substr($arg, 2), 1); if (!in_array($arg_name, self::LONG_OPTIONS) && !in_array($arg_name . ':', self::LONG_OPTIONS) @@ -469,7 +469,7 @@ static function (string $arg): void { exit(1); } } elseif (strpos($arg, '-') === 0 && $arg !== '-' && $arg !== '--') { - $arg_name = preg_replace('/=.*$/', '', substr($arg, 1)); + $arg_name = (string) preg_replace('/=.*$/', '', substr($arg, 1)); if (!in_array($arg_name, self::SHORT_OPTIONS) && !in_array($arg_name . ':', self::SHORT_OPTIONS) @@ -1035,6 +1035,7 @@ private static function initConfig( } /** + * @param ?list $paths_to_check * @return array}>> */ private static function initBaseline( @@ -1042,10 +1043,15 @@ private static function initBaseline( Config $config, string $current_dir, ?string $path_to_config, + ?array $paths_to_check ): array { $issue_baseline = []; if (isset($options['set-baseline']) && is_string($options['set-baseline'])) { + if ($paths_to_check !== null) { + fwrite(STDERR, PHP_EOL . 'Cannot generate baseline when checking specific files' . PHP_EOL); + exit(1); + } $issue_baseline = self::generateBaseline($options, $config, $current_dir, $path_to_config); } @@ -1062,6 +1068,10 @@ private static function initBaseline( } if (isset($options['update-baseline'])) { + if ($paths_to_check !== null) { + fwrite(STDERR, PHP_EOL . 'Cannot update baseline when checking specific files' . PHP_EOL); + exit(1); + } $issue_baseline = self::updateBaseline($options, $config); } @@ -1077,6 +1087,17 @@ private static function initBaseline( } } + if ($paths_to_check !== null) { + $filtered_issue_baseline = []; + foreach ($paths_to_check as $path_to_check) { + $path_to_check = substr($path_to_check, strlen($config->base_dir)); + if (isset($issue_baseline[$path_to_check])) { + $filtered_issue_baseline[$path_to_check] = $issue_baseline[$path_to_check]; + } + } + $issue_baseline = $filtered_issue_baseline; + } + return $issue_baseline; } diff --git a/src/Psalm/Internal/Cli/Psalter.php b/src/Psalm/Internal/Cli/Psalter.php index 35ac7bada23..242a27a967b 100644 --- a/src/Psalm/Internal/Cli/Psalter.php +++ b/src/Psalm/Internal/Cli/Psalter.php @@ -32,6 +32,7 @@ use function array_map; use function array_shift; use function array_slice; +use function assert; use function chdir; use function count; use function explode; @@ -106,6 +107,9 @@ public static function run(array $argv): void // get options from command line $options = getopt(implode('', self::SHORT_OPTIONS), self::LONG_OPTIONS); + if ($options === false) { + die('Failed to parse cli options' . PHP_EOL); + } self::validateCliArguments($args); @@ -450,7 +454,7 @@ private static function validateCliArguments(array $args): void array_map( static function (string $arg): void { if (strpos($arg, '--') === 0 && $arg !== '--') { - $arg_name = preg_replace('/=.*$/', '', substr($arg, 2), 1); + $arg_name = (string) preg_replace('/=.*$/', '', substr($arg, 2), 1); if ($arg_name === 'alter') { // valid option for psalm, ignored by psalter @@ -501,16 +505,17 @@ private static function syncShortOptions(array &$options): void private static function loadCodeowners(Providers $providers): array { if (file_exists('CODEOWNERS')) { - $codeowners_file_path = realpath('CODEOWNERS'); + $codeowners_file_path = (string) realpath('CODEOWNERS'); } elseif (file_exists('.github/CODEOWNERS')) { - $codeowners_file_path = realpath('.github/CODEOWNERS'); + $codeowners_file_path = (string) realpath('.github/CODEOWNERS'); } elseif (file_exists('docs/CODEOWNERS')) { - $codeowners_file_path = realpath('docs/CODEOWNERS'); + $codeowners_file_path = (string) realpath('docs/CODEOWNERS'); } else { die('Cannot use --codeowner without a CODEOWNERS file' . PHP_EOL); } $codeowners_file = file_get_contents($codeowners_file_path); + assert($codeowners_file != false); $codeowner_lines = array_map( static function (string $line): array { diff --git a/src/Psalm/Internal/Cli/Refactor.php b/src/Psalm/Internal/Cli/Refactor.php index 8c8b37e0c8a..b00848ddcb6 100644 --- a/src/Psalm/Internal/Cli/Refactor.php +++ b/src/Psalm/Internal/Cli/Refactor.php @@ -87,11 +87,14 @@ public static function run(array $argv): void // get options from command line $options = getopt(implode('', $valid_short_options), $valid_long_options); + if ($options === false) { + die('Failed to parse cli options' . PHP_EOL); + } array_map( static function (string $arg) use ($valid_long_options): void { if (strpos($arg, '--') === 0 && $arg !== '--') { - $arg_name = preg_replace('/=.*$/', '', substr($arg, 2), 1); + $arg_name = (string) preg_replace('/=.*$/', '', substr($arg, 2), 1); if ($arg_name === 'refactor') { // valid option for psalm, ignored by psalter diff --git a/src/Psalm/Internal/CliUtils.php b/src/Psalm/Internal/CliUtils.php index d73a9eec10a..78d964f0b94 100644 --- a/src/Psalm/Internal/CliUtils.php +++ b/src/Psalm/Internal/CliUtils.php @@ -14,10 +14,12 @@ use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Report; use RuntimeException; +use UnexpectedValueException; use function array_filter; use function array_key_exists; use function array_slice; +use function assert; use function count; use function define; use function dirname; @@ -173,7 +175,9 @@ public static function getVendorDir(string $current_dir): string return 'vendor'; } try { - $composer_json = json_decode(file_get_contents($composer_json_path), true, 512, JSON_THROW_ON_ERROR); + $composer_file_contents = file_get_contents($composer_json_path); + assert($composer_file_contents !== false); + $composer_json = json_decode($composer_file_contents, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $e) { fwrite( STDERR, @@ -399,9 +403,10 @@ public static function updateConfigFile(Config $config, string $config_file_path } $config_file_contents = file_get_contents($config_file); + assert($config_file_contents !== false); if ($config->error_baseline) { - $amended_config_file_contents = preg_replace( + $amended_config_file_contents = (string) preg_replace( '/errorBaseline=".*?"/', "errorBaseline=\"{$baseline_path}\"", $config_file_contents, @@ -486,7 +491,15 @@ public static function initPhpVersion(array $options, Config $config, ProjectAna } if ($version !== null && $source !== null) { - $project_analyzer->setPhpVersion($version, $source); + try { + $project_analyzer->setPhpVersion($version, $source); + } catch (UnexpectedValueException $e) { + fwrite( + STDERR, + $e->getMessage() . PHP_EOL, + ); + exit(1); + } } } diff --git a/src/Psalm/Internal/Codebase/Analyzer.php b/src/Psalm/Internal/Codebase/Analyzer.php index 8ffc358a558..62a361379ee 100644 --- a/src/Psalm/Internal/Codebase/Analyzer.php +++ b/src/Psalm/Internal/Codebase/Analyzer.php @@ -678,7 +678,7 @@ public function loadCachedResults(ProjectAnalyzer $project_analyzer): void $method_param_uses[$member_id], ); - $member_stub = preg_replace('/::.*$/', '::*', $member_id, 1); + $member_stub = (string) preg_replace('/::.*$/', '::*', $member_id, 1); if (isset($all_referencing_methods[$member_stub])) { $newly_invalidated_methods = array_merge( diff --git a/src/Psalm/Internal/Codebase/AssertionsFromInheritanceResolver.php b/src/Psalm/Internal/Codebase/AssertionsFromInheritanceResolver.php new file mode 100644 index 00000000000..aa42e2a8942 --- /dev/null +++ b/src/Psalm/Internal/Codebase/AssertionsFromInheritanceResolver.php @@ -0,0 +1,65 @@ +codebase = $codebase; + } + + /** + * @return array + */ + public function resolve( + MethodStorage $method_storage, + ClassLikeStorage $called_class + ): array { + $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))); + + foreach ($inherited_classes_and_interfaces as $potential_assertion_providing_class) { + $potential_assertion_providing_classlike_storage = $this->codebase->classlike_storage_provider->get( + $potential_assertion_providing_class, + ); + if (!isset($potential_assertion_providing_classlike_storage->methods[$method_name_lc])) { + continue; + } + + $potential_assertion_providing_method_storage = $potential_assertion_providing_classlike_storage + ->methods[$method_name_lc]; + + /** + * Since the inheritance does not provide its own assertions, we have to detect those + * from inherited classes + */ + $assertions += $potential_assertion_providing_method_storage->assertions; + } + + return $assertions; + } +} diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php index 823a13e1f66..1f540d63032 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -169,7 +169,7 @@ private function collectPredefinedClassLikes(): void $predefined_classes = get_declared_classes(); foreach ($predefined_classes as $predefined_class) { - $predefined_class = preg_replace('/^\\\/', '', $predefined_class, 1); + $predefined_class = (string) preg_replace('/^\\\/', '', $predefined_class, 1); /** @psalm-suppress ArgumentTypeCoercion */ $reflection_class = new ReflectionClass($predefined_class); @@ -185,7 +185,7 @@ private function collectPredefinedClassLikes(): void $predefined_interfaces = get_declared_interfaces(); foreach ($predefined_interfaces as $predefined_interface) { - $predefined_interface = preg_replace('/^\\\/', '', $predefined_interface, 1); + $predefined_interface = (string) preg_replace('/^\\\/', '', $predefined_interface, 1); /** @psalm-suppress ArgumentTypeCoercion */ $reflection_class = new ReflectionClass($predefined_interface); @@ -597,6 +597,7 @@ public function classExists( /** * Determine whether or not a class extends a parent * + * @psalm-mutation-free * @throws UnpopulatedClasslikeException when called on unpopulated class * @throws InvalidArgumentException when class does not exist */ @@ -620,6 +621,8 @@ public function classExtends(string $fq_class_name, string $possible_parent, boo /** * Check whether a class implements an interface + * + * @psalm-mutation-free */ public function classImplements(string $fq_class_name, string $interface): bool { diff --git a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php index a07a2173ac0..4198f185180 100644 --- a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php +++ b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php @@ -342,10 +342,9 @@ public static function resolve( if (isset($enum_storage->enum_cases[$c->case])) { if ($c instanceof EnumValueFetch) { $value = $enum_storage->enum_cases[$c->case]->value; - if (is_string($value)) { - return Type::getString($value)->getSingleAtomic(); - } elseif (is_int($value)) { - return Type::getInt(false, $value)->getSingleAtomic(); + + if ($value !== null) { + return $value; } } elseif ($c instanceof EnumNameFetch) { return Type::getString($c->case)->getSingleAtomic(); diff --git a/src/Psalm/Internal/Codebase/Functions.php b/src/Psalm/Internal/Codebase/Functions.php index 8cd03ce7554..27e770ba598 100644 --- a/src/Psalm/Internal/Codebase/Functions.php +++ b/src/Psalm/Internal/Codebase/Functions.php @@ -81,9 +81,8 @@ public function getStorage( $function_id = substr($function_id, 1); } - $from_stubs = false; if (isset(self::$stubbed_functions[$function_id])) { - $from_stubs = self::$stubbed_functions[$function_id]; + return self::$stubbed_functions[$function_id]; } $file_storage = null; @@ -115,10 +114,6 @@ public function getStorage( return $this->reflection->getFunctionStorage($function_id); } - if ($from_stubs) { - return $from_stubs; - } - throw new UnexpectedValueException( 'Expecting non-empty $root_file_path and $checked_file_path', ); @@ -137,10 +132,6 @@ public function getStorage( } } - if ($from_stubs) { - return $from_stubs; - } - throw new UnexpectedValueException( 'Expecting ' . $function_id . ' to have storage in ' . $checked_file_path, ); @@ -151,10 +142,6 @@ public function getStorage( $declaring_file_storage = $this->file_storage_provider->get($declaring_file_path); if (!isset($declaring_file_storage->functions[$function_id])) { - if ($from_stubs) { - return $from_stubs; - } - throw new UnexpectedValueException( 'Not expecting ' . $function_id . ' to not have storage in ' . $declaring_file_path, ); @@ -407,124 +394,7 @@ public function isCallMapFunctionPure( ?array $args, bool &$must_use = true, ): bool { - $impure_functions = [ - // file io - 'chdir', 'chgrp', 'chmod', 'chown', 'chroot', 'copy', 'file_get_contents', 'file_put_contents', - 'opendir', 'readdir', 'closedir', 'rewinddir', 'scandir', - 'fopen', 'fread', 'fwrite', 'fclose', 'touch', 'fpassthru', 'fputs', 'fscanf', 'fseek', 'flock', - 'ftruncate', 'fprintf', 'symlink', 'mkdir', 'unlink', 'rename', 'rmdir', 'popen', 'pclose', - 'fgetcsv', 'fputcsv', 'umask', 'finfo_open', 'finfo_close', 'finfo_file', - 'stream_set_timeout', 'fgets', 'fflush', 'move_uploaded_file', 'file_exists', 'realpath', 'glob', - 'is_readable', 'is_dir', 'is_file', - - // stream/socket io - 'stream_context_set_option', 'socket_write', 'stream_set_blocking', 'socket_close', - 'socket_set_option', 'stream_set_write_buffer', 'stream_socket_enable_crypto', 'stream_copy_to_stream', - 'stream_wrapper_register', 'socket_connect', 'socket_bind', 'socket_set_block', 'socket_set_nonblock', - 'socket_listen', - - // meta calls - 'call_user_func', 'call_user_func_array', 'define', 'create_function', - - // http - 'header', 'header_remove', 'http_response_code', 'setcookie', 'setrawcookie', - - // output buffer - 'ob_start', 'ob_end_clean', 'ob_get_clean', 'readfile', 'printf', 'var_dump', 'phpinfo', - 'ob_implicit_flush', 'vprintf', - - // mcrypt - 'mcrypt_generic_init', 'mcrypt_generic_deinit', 'mcrypt_module_close', - - // internal optimisation - 'opcache_compile_file', 'clearstatcache', - - // process-related - 'pcntl_signal', 'pcntl_alarm', 'posix_kill', 'cli_set_process_title', 'pcntl_async_signals', 'proc_close', - 'proc_nice', 'proc_open', 'proc_terminate', - - // curl - 'curl_setopt', 'curl_close', 'curl_multi_add_handle', 'curl_multi_remove_handle', - 'curl_multi_select', 'curl_multi_close', 'curl_setopt_array', - - // apc, apcu - 'apc_store', 'apc_delete', 'apc_clear_cache', 'apc_add', 'apc_inc', 'apc_dec', 'apc_cas', - 'apcu_store', 'apcu_delete', 'apcu_clear_cache', 'apcu_add', 'apcu_inc', 'apcu_dec', 'apcu_cas', - - // gz - 'gzwrite', 'gzrewind', 'gzseek', 'gzclose', - - // newrelic - 'newrelic_start_transaction', 'newrelic_name_transaction', 'newrelic_add_custom_parameter', - 'newrelic_add_custom_tracer', 'newrelic_background_job', 'newrelic_end_transaction', - 'newrelic_set_appname', - - // execution - 'shell_exec', 'exec', 'system', 'passthru', 'pcntl_exec', - - // well-known functions - 'libxml_use_internal_errors', 'libxml_disable_entity_loader', 'curl_exec', - 'mt_srand', 'openssl_pkcs7_sign', 'openssl_sign', - 'mt_rand', 'rand', 'random_int', 'random_bytes', - 'wincache_ucache_delete', 'wincache_ucache_set', 'wincache_ucache_inc', - 'class_alias', - 'class_exists', // impure by virtue of triggering autoloader - 'enum_exists', // impure by virtue of triggering autoloader - - // php environment - 'ini_set', 'sleep', 'usleep', 'register_shutdown_function', - 'error_reporting', 'register_tick_function', 'unregister_tick_function', - 'set_error_handler', 'user_error', 'trigger_error', 'restore_error_handler', - 'date_default_timezone_set', 'assert_options', 'setlocale', - 'set_exception_handler', 'set_time_limit', 'putenv', 'spl_autoload_register', - 'spl_autoload_unregister', 'microtime', 'array_rand', 'set_include_path', - - // logging - 'openlog', 'syslog', 'error_log', 'define_syslog_variables', - - // session - 'session_id', 'session_decode', 'session_name', 'session_set_cookie_params', - 'session_set_save_handler', 'session_regenerate_id', 'mb_internal_encoding', - 'session_start', 'session_cache_limiter', - - // ldap - 'ldap_set_option', - - // iterators - 'rewind', 'iterator_apply', 'iterator_to_array', - - // mysqli - 'mysqli_select_db', 'mysqli_dump_debug_info', 'mysqli_kill', 'mysqli_multi_query', - 'mysqli_next_result', 'mysqli_options', 'mysqli_ping', 'mysqli_query', 'mysqli_report', - 'mysqli_rollback', 'mysqli_savepoint', 'mysqli_set_charset', 'mysqli_ssl_set', 'mysqli_close', - - // script execution - 'ignore_user_abort', - - // ftp - 'ftp_close', 'ftp_pasv', - - // bcmath - 'bcscale', - - // json - 'json_last_error', - - // opcache - 'opcache_compile_file', 'opcache_get_configuration', 'opcache_get_status', - 'opcache_invalidate', 'opcache_is_script_cached', 'opcache_reset', - - //gettext - 'bindtextdomain', - - // hash - 'hash_update', 'hash_update_file', 'hash_update_stream', - - // unserialize - 'unserialize', - ]; - - if (in_array(strtolower($function_id), $impure_functions, true)) { + if (ImpureFunctionsList::isImpure($function_id)) { return false; } diff --git a/src/Psalm/Internal/Codebase/ImpureFunctionsList.php b/src/Psalm/Internal/Codebase/ImpureFunctionsList.php new file mode 100644 index 00000000000..386598af477 --- /dev/null +++ b/src/Psalm/Internal/Codebase/ImpureFunctionsList.php @@ -0,0 +1,31 @@ + */ + private static ?array $impure_functions_list = null; + + /** @psalm-assert !null self::$impure_functions_list */ + private static function load(): void + { + if (self::$impure_functions_list !== null) { + return; + } + + /** @var array */ + self::$impure_functions_list = require(dirname(__DIR__, 4) . '/dictionaries/ImpureFunctionsList.php'); + } + + public static function isImpure(string $function_id): bool + { + self::load(); + + return isset(self::$impure_functions_list[strtolower($function_id)]); + } +} diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index fd3c285f8fb..d7df9928512 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -37,6 +37,7 @@ use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNull; +use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; use UnexpectedValueException; @@ -46,7 +47,6 @@ use function count; use function explode; use function in_array; -use function is_int; use function reset; use function strtolower; @@ -560,6 +560,7 @@ public function getMethodReturnType( ?string &$self_class, ?SourceAnalyzer $source_analyzer = null, ?array $args = null, + ?TemplateResult $template_result = null ): ?Union { $original_fq_class_name = $method_id->fq_class_name; $original_method_name = $method_id->method_name; @@ -589,6 +590,7 @@ public function getMethodReturnType( if ($class_storage->abstract && isset($class_storage->overridden_method_ids[$original_method_name])) { $appearing_method_id = reset($class_storage->overridden_method_ids[$original_method_name]); + assert($appearing_method_id !== false); } else { return null; } @@ -631,9 +633,7 @@ public function getMethodReturnType( foreach ($original_class_storage->enum_cases as $case_name => $case_storage) { if (UnionTypeComparator::isContainedBy( $source_analyzer->getCodebase(), - is_int($case_storage->value) ? - Type::getInt(false, $case_storage->value) : - Type::getString($case_storage->value), + new Union([$case_storage->value ?? new TString()]), $first_arg_type, )) { $types[] = new TEnumCase($original_fq_class_name, $case_name); @@ -786,9 +786,18 @@ public function getMethodReturnType( ); if ($found_generic_params) { + $passed_template_result = $template_result; + $template_result = new TemplateResult( + [], + $found_generic_params, + ); + if ($passed_template_result !== null) { + $template_result = $template_result->merge($passed_template_result); + } + $overridden_storage_return_type = TemplateInferredTypeReplacer::replace( $overridden_storage_return_type, - new TemplateResult([], $found_generic_params), + $template_result, $source_analyzer->getCodebase(), ); } @@ -1011,6 +1020,8 @@ public function getDeclaringMethodId( } if ($class_storage->abstract && isset($class_storage->overridden_method_ids[$method_name])) { + assert(!empty($class_storage->overridden_method_ids[$method_name])); + return reset($class_storage->overridden_method_ids[$method_name]); } diff --git a/src/Psalm/Internal/Codebase/Scanner.php b/src/Psalm/Internal/Codebase/Scanner.php index d638ee07ac4..bf13e233267 100644 --- a/src/Psalm/Internal/Codebase/Scanner.php +++ b/src/Psalm/Internal/Codebase/Scanner.php @@ -666,7 +666,7 @@ private function fileExistsForClassLike(ClassLikes $classlikes, string $fq_class $classlikes->addFullyQualifiedClassLikeName( $fq_class_name_lc, - realpath($composer_file_path), + (string) realpath($composer_file_path), ); return true; diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index 52828b47d44..9ed1c865115 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -21,11 +21,13 @@ use Psalm\Issue\TaintedLdap; use Psalm\Issue\TaintedSSRF; use Psalm\Issue\TaintedShell; +use Psalm\Issue\TaintedSleep; use Psalm\Issue\TaintedSql; use Psalm\Issue\TaintedSystemSecret; use Psalm\Issue\TaintedTextWithQuotes; use Psalm\Issue\TaintedUnserialize; use Psalm\Issue\TaintedUserSecret; +use Psalm\Issue\TaintedXpath; use Psalm\IssueBuffer; use Psalm\Type\TaintKind; @@ -451,6 +453,24 @@ private function getChildNodes( ); break; + case TaintKind::INPUT_XPATH: + $issue = new TaintedXpath( + 'Detected tainted xpath query', + $issue_location, + $issue_trace, + $path, + ); + break; + + case TaintKind::INPUT_SLEEP: + $issue = new TaintedSleep( + 'Detected tainted sleep', + $issue_location, + $issue_trace, + $path, + ); + break; + default: $issue = new TaintedCustom( 'Detected tainted ' . $matching_taint, diff --git a/src/Psalm/Internal/Diff/FileStatementsDiffer.php b/src/Psalm/Internal/Diff/FileStatementsDiffer.php index 175520640db..113646dc28d 100644 --- a/src/Psalm/Internal/Diff/FileStatementsDiffer.php +++ b/src/Psalm/Internal/Diff/FileStatementsDiffer.php @@ -6,6 +6,7 @@ use PhpParser; +use function assert; use function end; use function get_class; use function substr; @@ -132,6 +133,7 @@ static function ( $add_or_delete[] = 'use:' . (string) $use->alias; } else { $name_parts = $use->name->getParts(); + assert(!empty($name_parts)); $add_or_delete[] = 'use:' . end($name_parts); } @@ -159,6 +161,7 @@ static function ( $add_or_delete[] = 'use:' . (string) $use->alias; } else { $name_parts = $use->name->getParts(); + assert(!empty($name_parts)); $add_or_delete[] = 'use:' . end($name_parts); } diff --git a/src/Psalm/Internal/Diff/NamespaceStatementsDiffer.php b/src/Psalm/Internal/Diff/NamespaceStatementsDiffer.php index dc6d57b3456..b441edff3ef 100644 --- a/src/Psalm/Internal/Diff/NamespaceStatementsDiffer.php +++ b/src/Psalm/Internal/Diff/NamespaceStatementsDiffer.php @@ -6,6 +6,7 @@ use PhpParser; +use function assert; use function end; use function get_class; use function substr; @@ -117,6 +118,7 @@ static function ( $add_or_delete[] = 'use:' . (string) $use->alias; } else { $name_parts = $use->name->getParts(); + assert(!empty($name_parts)); $add_or_delete[] = 'use:' . end($name_parts); } @@ -131,6 +133,7 @@ static function ( $add_or_delete[] = 'use:' . (string) $use->alias; } else { $name_parts = $use->name->getParts(); + assert(!empty($name_parts)); $add_or_delete[] = 'use:' . end($name_parts); } diff --git a/src/Psalm/Internal/ExecutionEnvironment/BuildInfoCollector.php b/src/Psalm/Internal/ExecutionEnvironment/BuildInfoCollector.php index 674b4525018..75fb6af7a65 100644 --- a/src/Psalm/Internal/ExecutionEnvironment/BuildInfoCollector.php +++ b/src/Psalm/Internal/ExecutionEnvironment/BuildInfoCollector.php @@ -7,6 +7,7 @@ use Psalm\SourceControl\Git\CommitInfo; use Psalm\SourceControl\Git\GitInfo; +use function assert; use function explode; use function file_get_contents; use function json_decode; @@ -279,6 +280,7 @@ protected function fillGithubActions(): BuildInfoCollector if (isset($this->env['GITHUB_EVENT_PATH'])) { $event_json = file_get_contents((string) $this->env['GITHUB_EVENT_PATH']); + assert($event_json !== false); /** @var array */ $event_data = json_decode($event_json, true, 512, JSON_THROW_ON_ERROR); @@ -302,7 +304,7 @@ protected function fillGithubActions(): BuildInfoCollector ->setCommitterName($head_commit_data['committer']['name']) ->setCommitterEmail($head_commit_data['committer']['email']) ->setMessage($head_commit_data['message']) - ->setDate(strtotime($head_commit_data['timestamp'])), + ->setDate((int) strtotime($head_commit_data['timestamp'])), [], ); diff --git a/src/Psalm/Internal/Fork/Pool.php b/src/Psalm/Internal/Fork/Pool.php index 9321cfeb553..fd33ed0bc49 100644 --- a/src/Psalm/Internal/Fork/Pool.php +++ b/src/Psalm/Internal/Fork/Pool.php @@ -136,7 +136,7 @@ public function __construct( exit(1); } - $disabled_functions = array_map('trim', explode(',', ini_get('disable_functions'))); + $disabled_functions = array_map('trim', explode(',', (string) ini_get('disable_functions'))); if (in_array('pcntl_fork', $disabled_functions)) { echo "pcntl_fork() is disabled by php configuration (disable_functions directive).\n" . "Please enable it or run Psalm single-threaded with --threads=1 cli switch.\n"; @@ -213,14 +213,14 @@ public function __construct( $task_done_message = new ForkTaskDoneMessage($task_result); if ($this->config->use_igbinary) { - $encoded_message = base64_encode(igbinary_serialize($task_done_message)); + $encoded_message = base64_encode((string) igbinary_serialize($task_done_message)); } else { $encoded_message = base64_encode(serialize($task_done_message)); } $serialized_message = $task_done_buffer . $encoded_message . "\n"; if (strlen($serialized_message) > 200) { - $bytes_written = @fwrite($write_stream, $serialized_message); + $bytes_written = (int) @fwrite($write_stream, $serialized_message); if (strlen($serialized_message) !== $bytes_written) { $task_done_buffer = substr($serialized_message, $bytes_written); @@ -250,7 +250,7 @@ public function __construct( } if ($this->config->use_igbinary) { - $encoded_message = base64_encode(igbinary_serialize($process_done_message)); + $encoded_message = base64_encode((string) igbinary_serialize($process_done_message)); } else { $encoded_message = base64_encode(serialize($process_done_message)); } @@ -261,7 +261,7 @@ public function __construct( while ($bytes_written < $bytes_to_write && !feof($write_stream)) { // attempt to write the remaining unsent part - $bytes_written += @fwrite($write_stream, substr($serialized_message, $bytes_written)); + $bytes_written += (int) @fwrite($write_stream, substr($serialized_message, $bytes_written)); if ($bytes_written < $bytes_to_write) { // wait a bit @@ -374,9 +374,9 @@ private function readResultsFromChildren(): array foreach ($serialized_messages as $serialized_message) { if ($this->config->use_igbinary) { - $message = igbinary_unserialize(base64_decode($serialized_message, true)); + $message = igbinary_unserialize((string) base64_decode($serialized_message, true)); } else { - $message = unserialize(base64_decode($serialized_message, true)); + $message = unserialize((string) base64_decode($serialized_message, true)); } if ($message instanceof ForkProcessDoneMessage) { diff --git a/src/Psalm/Internal/Fork/PsalmRestarter.php b/src/Psalm/Internal/Fork/PsalmRestarter.php index a8f69eab51e..69fecd823f2 100644 --- a/src/Psalm/Internal/Fork/PsalmRestarter.php +++ b/src/Psalm/Internal/Fork/PsalmRestarter.php @@ -9,6 +9,7 @@ use function array_filter; use function array_merge; use function array_splice; +use function assert; use function extension_loaded; use function file_get_contents; use function file_put_contents; @@ -76,7 +77,7 @@ protected function requiresRestart($default): bool '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(ini_get('opcache.jit_buffer_size')), + 'jit_buffer_size' => self::toBytes((string) ini_get('opcache.jit_buffer_size')), ]; foreach (self::REQUIRED_OPCACHE_SETTINGS as $ini_name => $required_value) { @@ -130,8 +131,9 @@ protected function restart($command): void if ($this->required && $this->tmpIni) { $regex = '/^\s*(extension\s*=.*(' . implode('|', $this->disabled_extensions) . ').*)$/mi'; $content = file_get_contents($this->tmpIni); + assert($content !== false); - $content = preg_replace($regex, ';$1', $content); + $content = (string) preg_replace($regex, ';$1', $content); file_put_contents($this->tmpIni, $content); } diff --git a/src/Psalm/Internal/LanguageServer/LanguageClient.php b/src/Psalm/Internal/LanguageServer/LanguageClient.php index fe9528e8cdf..13c1c21b9ec 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageClient.php +++ b/src/Psalm/Internal/LanguageServer/LanguageClient.php @@ -159,7 +159,7 @@ private function configurationRefreshed(array $config): void } /** @var array */ - $array = json_decode(json_encode($config), true); + $array = json_decode((string) json_encode($config), true); 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 bde7e0d4bd2..2c648e4fd13 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -19,7 +19,6 @@ use LanguageServerProtocol\CompletionOptions; use LanguageServerProtocol\Diagnostic; use LanguageServerProtocol\DiagnosticSeverity; -use LanguageServerProtocol\ExecuteCommandOptions; use LanguageServerProtocol\InitializeResult; use LanguageServerProtocol\InitializeResultServerInfo; use LanguageServerProtocol\LogMessage; @@ -418,9 +417,6 @@ public function initialize( $serverCapabilities = new ServerCapabilities(); - //The server provides execute command support. - $serverCapabilities->executeCommandProvider = new ExecuteCommandOptions(['test']); - $textDocumentSyncOptions = new TextDocumentSyncOptions(); //Open and close notifications are sent to the server. @@ -847,7 +843,7 @@ public function log(int $type, string $message, array $context = []): void } if (!empty($context)) { - $message .= "\n" . json_encode($context, JSON_PRETTY_PRINT); + $message .= "\n" . (string) json_encode($context, JSON_PRETTY_PRINT); } try { $this->client->logMessage( diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index 8c2ccd2e318..a098b377f44 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -446,25 +446,6 @@ public function codeAction(TextDocumentIdentifier $textDocument, CodeActionConte ], ]), ); - - /* - $fixers["fixAll.{$diagnostic->data->type}"] = new CodeAction( - "FixAll {$diagnostic->data->type} for this file", - CodeActionKind::QUICK_FIX, - null, - null, - null, - null, - new Command( - "Fix All", - "psalm.fixall", - [ - 'uri' => $textDocument->uri, - 'type' => $diagnostic->data->type - ] - ) - ); - */ } if (empty($fixers)) { diff --git a/src/Psalm/Internal/PhpVisitor/PartialParserVisitor.php b/src/Psalm/Internal/PhpVisitor/PartialParserVisitor.php index 54dc600d27d..44607f49a2d 100644 --- a/src/Psalm/Internal/PhpVisitor/PartialParserVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/PartialParserVisitor.php @@ -8,6 +8,7 @@ use PhpParser\ErrorHandler\Collecting; use PhpParser\Parser; +use function assert; use function count; use function preg_match_all; use function preg_replace; @@ -223,7 +224,11 @@ public function enterNode(PhpParser\Node $node, bool &$traverseChildren = true): } // changes "): {" to ") {" - $hacky_class_fix = preg_replace('/(\)[\s]*):([\s]*\{)/', '$1 $2', $hacky_class_fix); + $hacky_class_fix = (string) preg_replace( + '/(\)[\s]*):([\s]*\{)/', + '$1 $2', + $hacky_class_fix, + ); if ($hacky_class_fix !== $fake_class) { $replacement_stmts = $this->parser->parse( @@ -290,6 +295,8 @@ public function enterNode(PhpParser\Node $node, bool &$traverseChildren = true): $traverseChildren = false; + assert(!empty($replacement_stmts)); + return reset($replacement_stmts); } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php index 2ac3f84868b..2dd27096b8b 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php @@ -68,7 +68,7 @@ public static function parse( $templates = []; if (isset($parsed_docblock->combined_tags['template'])) { foreach ($parsed_docblock->combined_tags['template'] as $offset => $template_line) { - $template_type = preg_split('/[\s]+/', preg_replace('@^[ \t]*\*@m', '', $template_line)); + $template_type = preg_split('/[\s]+/', (string) preg_replace('@^[ \t]*\*@m', '', $template_line)); if ($template_type === false) { throw new IncorrectDocblockException('Invalid @ŧemplate tag: '.preg_last_error_msg()); } @@ -111,7 +111,7 @@ public static function parse( if (isset($parsed_docblock->combined_tags['template-covariant'])) { foreach ($parsed_docblock->combined_tags['template-covariant'] as $offset => $template_line) { - $template_type = preg_split('/[\s]+/', preg_replace('@^[ \t]*\*@m', '', $template_line)); + $template_type = preg_split('/[\s]+/', (string) preg_replace('@^[ \t]*\*@m', '', $template_line)); if ($template_type === false) { throw new IncorrectDocblockException('Invalid @template-covariant tag: '.preg_last_error_msg()); } @@ -171,7 +171,7 @@ public static function parse( if (isset($parsed_docblock->tags['psalm-require-extends']) && count($extension_requirements = $parsed_docblock->tags['psalm-require-extends']) > 0) { - $info->extension_requirement = trim(preg_replace( + $info->extension_requirement = trim((string) preg_replace( '@^[ \t]*\*@m', '', $extension_requirements[array_key_first($extension_requirements)], @@ -180,7 +180,7 @@ public static function parse( if (isset($parsed_docblock->tags['psalm-require-implements'])) { foreach ($parsed_docblock->tags['psalm-require-implements'] as $implementation_requirement) { - $info->implementation_requirements[] = trim(preg_replace( + $info->implementation_requirements[] = trim((string) preg_replace( '@^[ \t]*\*@m', '', $implementation_requirement, @@ -197,9 +197,9 @@ public static function parse( } if (isset($parsed_docblock->tags['psalm-yield'])) { - $yield = reset($parsed_docblock->tags['psalm-yield']); + $yield = (string) reset($parsed_docblock->tags['psalm-yield']); - $info->yield = trim(preg_replace('@^[ \t]*\*@m', '', $yield)); + $info->yield = trim((string) preg_replace('@^[ \t]*\*@m', '', $yield)); } if (isset($parsed_docblock->tags['deprecated'])) { @@ -316,7 +316,7 @@ public static function parse( $info->sealed_methods = true; } foreach ($parsed_docblock->combined_tags['method'] as $offset => $method_entry) { - $method_entry = preg_replace('/[ \t]+/', ' ', trim($method_entry)); + $method_entry = (string) preg_replace('/[ \t]+/', ' ', trim($method_entry)); $docblock_lines = []; @@ -340,9 +340,9 @@ public static function parse( } } - $method_entry = trim(preg_replace('/\/\/.*/', '', $method_entry)); + $method_entry = trim((string) preg_replace('/\/\/.*/', '', $method_entry)); - $method_entry = preg_replace( + $method_entry = (string) preg_replace( '/array\(([0-9a-zA-Z_\'\" ]+,)*([0-9a-zA-Z_\'\" ]+)\)/', '[]', $method_entry, @@ -355,10 +355,14 @@ public static function parse( } $method_entry = str_replace([', ', '( '], [',', '('], $method_entry); - $method_entry = preg_replace('/ (?!(\$|\.\.\.|&))/', '', trim($method_entry)); + $method_entry = (string) preg_replace('/ (?!(\$|\.\.\.|&))/', '', trim($method_entry)); // replace array bracket contents - $method_entry = preg_replace('/\[([0-9a-zA-Z_\'\" ]+,)*([0-9a-zA-Z_\'\" ]+)\]/', '[]', $method_entry); + $method_entry = (string) preg_replace( + '/\[([0-9a-zA-Z_\'\" ]+,)*([0-9a-zA-Z_\'\" ]+)\]/', + '[]', + $method_entry, + ); if (!$method_entry) { throw new DocblockParseException('No @method entry specified'); @@ -544,11 +548,11 @@ protected static function addMagicPropertyToInfo( ) { $line_parts[1] = str_replace('&', '', $line_parts[1]); - $line_parts[1] = preg_replace('/,$/', '', $line_parts[1], 1); + $line_parts[1] = (string) preg_replace('/,$/', '', $line_parts[1], 1); $end = $offset + strlen($line_parts[0]); - $line_parts[0] = str_replace("\n", '', preg_replace('@^[ \t]*\*@m', '', $line_parts[0])); + $line_parts[0] = str_replace("\n", '', (string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0])); if ($line_parts[0] === '' || ($line_parts[0][0] === '$' diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 1922bc74892..723e1e3b5c0 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -44,6 +44,7 @@ use Psalm\Issue\DuplicateClass; use Psalm\Issue\DuplicateConstant; use Psalm\Issue\DuplicateEnumCase; +use Psalm\Issue\DuplicateProperty; use Psalm\Issue\InvalidAttribute; use Psalm\Issue\InvalidDocblock; use Psalm\Issue\InvalidEnumBackingType; @@ -78,8 +79,6 @@ use function count; use function get_class; use function implode; -use function is_int; -use function is_string; use function preg_match; use function preg_replace; use function preg_split; @@ -739,14 +738,11 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool if ($storage->is_enum) { $name_types = []; $values_types = []; - foreach ($storage->enum_cases as $name => $enumCaseStorage) { + foreach ($storage->enum_cases as $name => $enum_case_storage) { $name_types[] = Type::getAtomicStringFromLiteral($name); - if ($storage->enum_type !== null) { - if (is_string($enumCaseStorage->value)) { - $values_types[] = Type::getAtomicStringFromLiteral($enumCaseStorage->value); - } elseif (is_int($enumCaseStorage->value)) { - $values_types[] = new Type\Atomic\TLiteralInt($enumCaseStorage->value); - } + if ($storage->enum_type !== null + && $enum_case_storage->value !== null) { + $values_types[] = $enum_case_storage->value; } } if ($name_types !== []) { @@ -944,7 +940,7 @@ public function handleTraitUse(PhpParser\Node\Stmt\TraitUse $node): void $this->useTemplatedType( $storage, $node, - trim(preg_replace('@^[ \t]*\*@m', '', $template_line)), + trim((string) preg_replace('@^[ \t]*\*@m', '', $template_line)), ); } } @@ -1443,9 +1439,9 @@ private function visitEnumDeclaration( if ($case_type) { if ($case_type->isSingleIntLiteral()) { - $enum_value = $case_type->getSingleIntLiteral()->value; + $enum_value = $case_type->getSingleIntLiteral(); } elseif ($case_type->isSingleStringLiteral()) { - $enum_value = $case_type->getSingleStringLiteral()->value; + $enum_value = $case_type->getSingleStringLiteral(); } else { IssueBuffer::maybeAdd( new InvalidEnumCaseValue( @@ -1463,7 +1459,7 @@ private function visitEnumDeclaration( if (!isset($storage->enum_cases[$stmt->name->name])) { $case = new EnumCaseStorage( - $enum_value, + $enum_value?->value, $case_location, ); @@ -1620,6 +1616,16 @@ private function visitPropertyDeclaration( foreach ($stmt->props as $property) { $doc_var_location = null; + if (isset($storage->properties[$property->name->name])) { + IssueBuffer::maybeAdd( + new DuplicateProperty( + 'Property ' . $fq_classlike_name . '::$' . $property->name->name . ' has already been defined', + new CodeLocation($this->file_scanner, $stmt, null, true), + $fq_classlike_name . '::$' . $property->name->name, + ), + ); + } + $property_storage = $storage->properties[$property->name->name] = new PropertyStorage(); $property_storage->is_static = $stmt->isStatic(); $property_storage->type = $signature_type; @@ -1906,8 +1912,8 @@ private static function getTypeAliasesFromCommentLines( continue; } - $var_line = preg_replace('/[ \t]+/', ' ', preg_replace('@^[ \t]*\*@m', '', $var_line)); - $var_line = preg_replace('/,\n\s+\}/', '}', $var_line); + $var_line = (string) preg_replace('/[ \t]+/', ' ', (string) preg_replace('@^[ \t]*\*@m', '', $var_line)); + $var_line = (string) preg_replace('/,\n\s+\}/', '}', $var_line); $var_line = str_replace("\n", '', $var_line); $var_line_parts = preg_split('/( |=)/', $var_line, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php index b5a37a0ef96..96431f0fb23 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php @@ -84,7 +84,7 @@ public static function parse( ) { $line_parts[1] = str_replace('&', '', $line_parts[1]); - $line_parts[1] = preg_replace('/,$/', '', $line_parts[1], 1); + $line_parts[1] = (string) preg_replace('/,$/', '', $line_parts[1], 1); $end = $offset + strlen($line_parts[0]); @@ -114,7 +114,7 @@ public static function parse( $description = substr($param, strlen($line_parts[0]) + strlen($line_parts[1]) + 2); $info_param['description'] = trim($description); // Handle multiline description. - $info_param['description'] = preg_replace( + $info_param['description'] = (string) preg_replace( '/\\n \\*\\s+/um', ' ', $info_param['description'], @@ -151,7 +151,11 @@ public static function parse( $line_parts[1] = substr($line_parts[1], 1); } - $line_parts[0] = str_replace("\n", '', preg_replace('@^[ \t]*\*@m', '', $line_parts[0])); + $line_parts[0] = str_replace( + "\n", + '', + (string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0]), + ); if ($line_parts[0] === '' || ($line_parts[0][0] === '$' @@ -160,7 +164,7 @@ public static function parse( throw new IncorrectDocblockException('Misplaced variable'); } - $line_parts[1] = preg_replace('/,$/', '', $line_parts[1], 1); + $line_parts[1] = (string) preg_replace('/,$/', '', $line_parts[1], 1); $info->params_out[] = [ 'name' => trim($line_parts[1]), @@ -190,7 +194,11 @@ public static function parse( $line_parts = CommentAnalyzer::splitDocLine($param); if (count($line_parts) > 0) { - $line_parts[0] = str_replace("\n", '', preg_replace('@^[ \t]*\*@m', '', $line_parts[0])); + $line_parts[0] = str_replace( + "\n", + '', + (string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0]), + ); $info->self_out = [ 'type' => str_replace("\n", '', $line_parts[0]), @@ -217,7 +225,7 @@ public static function parse( foreach ($parsed_docblock->tags['psalm-if-this-is'] as $offset => $param) { $line_parts = CommentAnalyzer::splitDocLine($param); - $line_parts[0] = str_replace("\n", '', preg_replace('@^[ \t]*\*@m', '', $line_parts[0])); + $line_parts[0] = str_replace("\n", '', (string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0])); $info->if_this_is = [ 'type' => str_replace("\n", '', $line_parts[0]), @@ -360,7 +368,7 @@ public static function parse( throw new IncorrectDocblockException('Misplaced variable'); } - $line_parts[1] = preg_replace('/,$/', '', $line_parts[1], 1); + $line_parts[1] = (string) preg_replace('/,$/', '', $line_parts[1], 1); $info->globals[] = [ 'name' => $line_parts[1], @@ -385,7 +393,7 @@ public static function parse( } if (isset($parsed_docblock->tags['since'])) { - $since = trim(reset($parsed_docblock->tags['since'])); + $since = trim((string) reset($parsed_docblock->tags['since'])); if (preg_match('/^[4578]\.\d(\.\d+)?$/', $since)) { $since_parts = explode('.', $since); @@ -416,6 +424,7 @@ public static function parse( if (isset($parsed_docblock->tags['throws'])) { foreach ($parsed_docblock->tags['throws'] as $offset => $throws_entry) { + /** @psalm-suppress PossiblyInvalidArrayAccess */ $throws_class = preg_split('/[\s]+/', $throws_entry)[0]; if (!$throws_class) { @@ -445,7 +454,7 @@ public static function parse( $templates = []; if (isset($parsed_docblock->combined_tags['template'])) { foreach ($parsed_docblock->combined_tags['template'] as $offset => $template_line) { - $template_type = preg_split('/[\s]+/', preg_replace('@^[ \t]*\*@m', '', $template_line)); + $template_type = preg_split('/[\s]+/', (string) preg_replace('@^[ \t]*\*@m', '', $template_line)); if ($template_type === false) { throw new AssertionError(preg_last_error_msg()); } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php index 980220be4c5..ae2cabd564c 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -1087,7 +1087,7 @@ private static function handleTaintFlow( $path_type = $matches[1]; } - $flow = preg_replace($fancy_path_regex, '->', $flow); + $flow = (string) preg_replace($fancy_path_regex, '->', $flow); } $flow_parts = explode('->', $flow); diff --git a/src/Psalm/Internal/PluginManager/ComposerLock.php b/src/Psalm/Internal/PluginManager/ComposerLock.php index b4e0af4b854..428b914aab8 100644 --- a/src/Psalm/Internal/PluginManager/ComposerLock.php +++ b/src/Psalm/Internal/PluginManager/ComposerLock.php @@ -7,6 +7,7 @@ use RuntimeException; use function array_merge; +use function assert; use function file_get_contents; use function is_array; use function is_string; @@ -61,7 +62,10 @@ public function getPlugins(): array private function read(string $file_name): array { - $contents = json_decode(file_get_contents($file_name), true); + $file_contents = file_get_contents($file_name); + assert($file_contents !== false); + + $contents = json_decode($file_contents, true); if ($error = json_last_error()) { throw new RuntimeException(json_last_error_msg(), $error); diff --git a/src/Psalm/Internal/PluginManager/ConfigFile.php b/src/Psalm/Internal/PluginManager/ConfigFile.php index a234d5fdb0e..33cbcc5eed7 100644 --- a/src/Psalm/Internal/PluginManager/ConfigFile.php +++ b/src/Psalm/Internal/PluginManager/ConfigFile.php @@ -113,6 +113,7 @@ private function readXml(): DOMDocument $doc = new DOMDocument(); $file_contents = file_get_contents($this->path); + assert($file_contents !== false); if (($tag_start = strpos($file_contents, '', $tag_start + 1); diff --git a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php index 909cecd0404..09db0b64f96 100644 --- a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php +++ b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php @@ -57,7 +57,7 @@ public function __construct(Config $config) throw new UnexpectedValueException($dependent_file_path . ' must exist'); } - $this->modified_timestamps .= ' ' . filemtime($dependent_file_path); + $this->modified_timestamps .= ' ' . (int) filemtime($dependent_file_path); } $this->modified_timestamps .= $config->computeHash(); diff --git a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php index 602be6cb6ff..e062ff7d117 100644 --- a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php +++ b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php @@ -57,7 +57,7 @@ public function __construct(Config $config) throw new UnexpectedValueException($dependent_file_path . ' must exist'); } - $this->modified_timestamps .= ' ' . filemtime($dependent_file_path); + $this->modified_timestamps .= ' ' . (int) filemtime($dependent_file_path); } $this->modified_timestamps .= $config->computeHash(); diff --git a/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php b/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php index 2779add0989..5b49684c6e6 100644 --- a/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php @@ -13,7 +13,6 @@ use Psalm\Internal\Provider\ReturnTypeProvider\DomNodeAppendChild; use Psalm\Internal\Provider\ReturnTypeProvider\ImagickPixelColorReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\PdoStatementReturnTypeProvider; -use Psalm\Internal\Provider\ReturnTypeProvider\SimpleXmlElementAsXml; use Psalm\Plugin\EventHandler\Event\MethodReturnTypeProviderEvent; use Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface; use Psalm\StatementsSource; @@ -41,7 +40,6 @@ public function __construct() $this->registerClass(DomNodeAppendChild::class); $this->registerClass(ImagickPixelColorReturnTypeProvider::class); - $this->registerClass(SimpleXmlElementAsXml::class); $this->registerClass(PdoStatementReturnTypeProvider::class); $this->registerClass(ClosureFromCallableReturnTypeProvider::class); $this->registerClass(DateTimeModifyReturnTypeProvider::class); diff --git a/src/Psalm/Internal/Provider/ParserCacheProvider.php b/src/Psalm/Internal/Provider/ParserCacheProvider.php index 8f7c4f33037..739fc54d852 100644 --- a/src/Psalm/Internal/Provider/ParserCacheProvider.php +++ b/src/Psalm/Internal/Provider/ParserCacheProvider.php @@ -12,6 +12,7 @@ use RuntimeException; use UnexpectedValueException; +use function assert; use function clearstatcache; use function error_log; use function file_put_contents; @@ -311,6 +312,7 @@ public function deleteOldParserCaches(float $time_before): int if (is_dir($cache_directory)) { $directory_files = scandir($cache_directory, SCANDIR_SORT_NONE); + assert($directory_files !== false); foreach ($directory_files as $directory_file) { $full_path = $cache_directory . DIRECTORY_SEPARATOR . $directory_file; diff --git a/src/Psalm/Internal/Provider/ProjectCacheProvider.php b/src/Psalm/Internal/Provider/ProjectCacheProvider.php index 107236874a4..8bfe3c41e24 100644 --- a/src/Psalm/Internal/Provider/ProjectCacheProvider.php +++ b/src/Psalm/Internal/Provider/ProjectCacheProvider.php @@ -69,7 +69,7 @@ public function getLastRun(string $psalm_version): int if (file_exists($run_cache_location) && Providers::safeFileGetContents($run_cache_location) === $psalm_version) { - $this->last_run = filemtime($run_cache_location); + $this->last_run = (int) filemtime($run_cache_location); } else { $this->last_run = 0; } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php index 65a9fd24ee9..92fad16f881 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php @@ -38,6 +38,7 @@ use function array_shift; use function array_slice; use function array_values; +use function assert; use function count; use function explode; use function in_array; @@ -163,6 +164,7 @@ function (array $sub) use ($null) { if ($function_call_type->hasCallableType()) { $closure_types = $function_call_type->getClosureTypes() ?: $function_call_type->getCallableTypes(); $closure_atomic_type = reset($closure_types); + assert($closure_atomic_type !== false); $closure_return_type = $closure_atomic_type->return_type ?: Type::getMixed(); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php index 895e838ee4c..dde31958fdf 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php @@ -19,6 +19,7 @@ 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; @@ -80,6 +81,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev case FILTER_VALIDATE_URL: case FILTER_VALIDATE_EMAIL: case FILTER_VALIDATE_DOMAIN: + case FILTER_SANITIZE_URL: $filter_type = Type::getString(); break; } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php index a881b0e8de5..20375ad9126 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php @@ -23,8 +23,6 @@ use UnitEnum; use stdClass; -use function is_int; -use function is_string; use function reset; use function strtolower; @@ -63,11 +61,11 @@ public static function getGetObjectVarsReturnType( return new TKeyedArray($properties); } $enum_case_storage = $enum_classlike_storage->enum_cases[$object_type->case_name]; - if (is_int($enum_case_storage->value)) { - $properties['value'] = new Union([new Atomic\TLiteralInt($enum_case_storage->value)]); - } elseif (is_string($enum_case_storage->value)) { - $properties['value'] = new Union([Type::getAtomicStringFromLiteral($enum_case_storage->value)]); + + if ($enum_case_storage->value !== null) { + $properties['value'] = new Union([$enum_case_storage->value]); } + return new TKeyedArray($properties); } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php index d3493493a03..dfb33ee153b 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php @@ -107,6 +107,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if ($key_type->isSingle() && $key_type->hasTemplate()) { $template_types = $key_type->getTemplateTypes(); $template_type = array_shift($template_types); + assert($template_type !== null); if ($template_type->as->hasMixed()) { $template_type = $template_type->replaceAs(Type::getArrayKey()); $key_type = new Union([$template_type]); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index c62abd9934f..a0c4ce460b2 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -51,12 +51,16 @@ private static function handleFetch(MethodReturnTypeProviderEvent $event): ?Unio $source = $event->getSource(); $call_args = $event->getCallArgs(); $fetch_mode = 0; - - if (isset($call_args[0]) - && ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value)) - && $first_arg_type->isSingleIntLiteral() - ) { - $fetch_mode = $first_arg_type->getSingleIntLiteral()->value; + + foreach ($call_args as $call_arg) { + $arg_name = $call_arg->name; + if (!isset($arg_name) || $arg_name->name === "mode") { + $arg_type = $source->getNodeTypeProvider()->getType($call_arg->value); + if (isset($arg_type) && $arg_type->isSingleIntLiteral()) { + $fetch_mode = $arg_type->getSingleIntLiteral()->value; + } + break; + } } switch ($fetch_mode) { diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/SimpleXmlElementAsXml.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/SimpleXmlElementAsXml.php deleted file mode 100644 index b694635a831..00000000000 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/SimpleXmlElementAsXml.php +++ /dev/null @@ -1,36 +0,0 @@ -getCallArgs(); - $method_name_lowercase = $event->getMethodNameLowercase(); - if ($method_name_lowercase === 'asxml' - && !count($call_args) - ) { - return Type::parseString('string|false'); - } - - return null; - } -} diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php index 52f19c2d4a4..7ad3ad0c387 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php @@ -6,6 +6,7 @@ use ArgumentCountError; use Psalm\Issue\InvalidArgument; +use Psalm\Issue\RedundantFunctionCall; use Psalm\Issue\TooFewArguments; use Psalm\Issue\TooManyArguments; use Psalm\IssueBuffer; @@ -27,6 +28,7 @@ use function is_string; use function preg_match; use function sprintf; +use function strlen; /** * @internal @@ -49,6 +51,11 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $statements_source = $event->getStatementsSource(); $call_args = $event->getCallArgs(); + // invalid - will already report an error for the params anyway + if (count($call_args) < 1) { + return null; + } + $has_splat_args = false; $node_type_provider = $statements_source->getNodeTypeProvider(); foreach ($call_args as $call_arg) { @@ -69,17 +76,29 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev // eventually this could be refined // to check if it's an array with literal string as first element for further checking if (count($call_args) === 1 && $has_splat_args === true) { + IssueBuffer::maybeAdd( + new RedundantFunctionCall( + 'Using the splat operator is redundant, as v' . $event->getFunctionId() + . ' without splat operator can be used instead of ' . $event->getFunctionId(), + $event->getCodeLocation(), + ), + $statements_source->getSuppressedIssues(), + ); + return null; } // it makes no sense to use sprintf when there is only 1 arg (the format) // as it wouldn't have any placeholders - if (count($call_args) === 1 && $event->getFunctionId() === 'sprintf') { + // if it's a literal string, we can check it further though! + $first_arg_type = $node_type_provider->getType($call_args[0]->value); + if (count($call_args) === 1 + && ($first_arg_type === null || !$first_arg_type->isSingleStringLiteral())) { IssueBuffer::maybeAdd( - new TooFewArguments( - 'Too few arguments for ' . $event->getFunctionId() . ', expecting at least 2 arguments', + new RedundantFunctionCall( + 'Using ' . $event->getFunctionId() + . ' with a single argument is redundant, since there are no placeholder params to be substituted', $event->getCodeLocation(), - $event->getFunctionId(), ), $statements_source->getSuppressedIssues(), ); @@ -91,7 +110,10 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $is_falsable = true; foreach ($call_args as $index => $call_arg) { $type = $node_type_provider->getType($call_arg->value); + if ($type === null && $index === 0 && $event->getFunctionId() === 'printf') { + // printf only has the format validated above + // don't change the return type break; } @@ -102,10 +124,9 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if ($index === 0 && $type->isSingleStringLiteral()) { if ($type->getSingleStringLiteral()->value === '') { IssueBuffer::maybeAdd( - new InvalidArgument( - 'Argument 1 of ' . $event->getFunctionId() . ' must not be an empty string', + new RedundantFunctionCall( + 'Calling ' . $event->getFunctionId() . ' with an empty first argument does nothing', $event->getCodeLocation(), - $event->getFunctionId(), ), $statements_source->getSuppressedIssues(), ); @@ -160,17 +181,48 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $initial_result = $result; if ($result === $type->getSingleStringLiteral()->value) { - IssueBuffer::maybeAdd( - new InvalidArgument( - 'Argument 1 of ' . $event->getFunctionId() - . ' does not contain any placeholders', - $event->getCodeLocation(), - $event->getFunctionId(), - ), - $statements_source->getSuppressedIssues(), - ); - - return null; + if (count($call_args) > 1) { + // we need to report this here too, since we return early without further validation + // otherwise people who have suspended RedundantFunctionCall errors + // will not get an error for this + IssueBuffer::maybeAdd( + new TooManyArguments( + 'Too many arguments for the number of placeholders in ' + . $event->getFunctionId(), + $event->getCodeLocation(), + $event->getFunctionId(), + ), + $statements_source->getSuppressedIssues(), + ); + } + + // the same error as above, but we have validated the pattern now + if (count($call_args) === 1) { + IssueBuffer::maybeAdd( + new RedundantFunctionCall( + 'Using ' . $event->getFunctionId() + . ' with a single argument is redundant,' + . ' since there are no placeholder params to be substituted', + $event->getCodeLocation(), + ), + $statements_source->getSuppressedIssues(), + ); + } else { + IssueBuffer::maybeAdd( + new RedundantFunctionCall( + 'Argument 1 of ' . $event->getFunctionId() + . ' does not contain any placeholders', + $event->getCodeLocation(), + ), + $statements_source->getSuppressedIssues(), + ); + } + + if ($event->getFunctionId() === 'printf') { + return Type::getInt(false, strlen($type->getSingleStringLiteral()->value)); + } + + return $type; } } } catch (ValueError $value_error) { diff --git a/src/Psalm/Internal/Scanner/DocblockParser.php b/src/Psalm/Internal/Scanner/DocblockParser.php index 05bc610c748..e8ebd9ece11 100644 --- a/src/Psalm/Internal/Scanner/DocblockParser.php +++ b/src/Psalm/Internal/Scanner/DocblockParser.php @@ -104,7 +104,7 @@ public static function parse(string $docblock, int $offsetStart): ParsedDocblock [$data, $data_offset] = $data_info; if (strpos($data, '*') !== false) { - $data = rtrim(preg_replace('/^ *\*\s*$/m', '', $data)); + $data = rtrim((string) preg_replace('/^ *\*\s*$/m', '', $data)); } if (empty($special[$type])) { diff --git a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php index c76a274a524..e894a1d7189 100644 --- a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php @@ -6,7 +6,6 @@ use Psalm\Codebase; use Psalm\Internal\MethodIdentifier; -use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Atomic\Scalar; use Psalm\Type\Atomic\TArray; @@ -20,11 +19,9 @@ use Psalm\Type\Atomic\TEmptyMixed; use Psalm\Type\Atomic\TEnumCase; use Psalm\Type\Atomic\TGenericObject; -use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TIterable; use Psalm\Type\Atomic\TKeyOf; 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; @@ -47,7 +44,6 @@ use function assert; use function count; use function get_class; -use function is_int; use function strtolower; /** @@ -632,40 +628,6 @@ public static function isContainedBy( } } - if ($input_type_part instanceof TEnumCase - && $codebase->classlike_storage_provider->has($input_type_part->value) - ) { - if ($container_type_part instanceof TString || $container_type_part instanceof TInt) { - $input_type_classlike_storage = $codebase->classlike_storage_provider->get($input_type_part->value); - if ($input_type_classlike_storage->enum_type === null - || !isset($input_type_classlike_storage->enum_cases[$input_type_part->case_name]) - ) { - // Not a backed enum or non-existent enum case - return false; - } - - $input_type_enum_case_storage = $input_type_classlike_storage->enum_cases[$input_type_part->case_name]; - assert( - $input_type_enum_case_storage->value !== null, - 'Backed enums cannot have values without a value.', - ); - - if (is_int($input_type_enum_case_storage->value)) { - return self::isContainedBy( - $codebase, - new TLiteralInt($input_type_enum_case_storage->value), - $container_type_part, - ); - } - - return self::isContainedBy( - $codebase, - Type::getAtomicStringFromLiteral($input_type_enum_case_storage->value), - $container_type_part, - ); - } - } - if ($container_type_part instanceof TString || $container_type_part instanceof TScalar) { if ($input_type_part instanceof TNamedObject) { // check whether the object has a __toString method diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index afd0b514d29..880c3ae3f80 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -20,7 +20,6 @@ use Psalm\Type\Atomic; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; -use Psalm\Type\Atomic\TCallableArray; use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TClosure; use Psalm\Type\Atomic\TKeyedArray; @@ -30,6 +29,7 @@ use Psalm\Type\Union; use UnexpectedValueException; +use function array_slice; use function end; use function strtolower; use function substr; @@ -66,6 +66,8 @@ public static function isContainedBy( return false; } + $input_variadic_param_idx = null; + if ($input_type_part->params !== null && $container_type_part->params !== null) { foreach ($input_type_part->params as $i => $input_param) { $container_param = null; @@ -80,7 +82,15 @@ public static function isContainedBy( } } + if ($input_param->is_variadic) { + $input_variadic_param_idx = $i; + } + if (!$container_param) { + if ($input_param->is_variadic) { + break; + } + if ($input_param->is_optional) { break; } @@ -104,6 +114,26 @@ public static function isContainedBy( } } + if ($input_variadic_param_idx && isset($input_type_part->params[$input_variadic_param_idx])) { + $input_param = $input_type_part->params[$input_variadic_param_idx]; + + foreach (array_slice($container_type_part->params ?? [], $input_variadic_param_idx) as $container_param) { + if ($container_param->type + && !$container_param->type->hasMixed() + && !UnionTypeComparator::isContainedBy( + $codebase, + $container_param->type, + $input_param->type ?: Type::getMixed(), + false, + false, + $atomic_comparison_result, + ) + ) { + return false; + } + } + } + if (isset($container_type_part->return_type)) { if (!isset($input_type_part->return_type)) { if ($atomic_comparison_result) { @@ -159,15 +189,6 @@ public static function isNotExplicitlyCallableTypeCallable( if (!$input_type_part->type_params[1]->hasString()) { return false; } - - if (!$input_type_part instanceof TCallableArray) { - if ($atomic_comparison_result) { - $atomic_comparison_result->type_coerced_from_mixed = true; - $atomic_comparison_result->type_coerced = true; - } - - return false; - } } elseif ($input_type_part instanceof TKeyedArray) { $method_id = self::getCallableMethodIdFromTKeyedArray($input_type_part); diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index e9ab799ac7c..b590ef98d25 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -36,7 +36,6 @@ use Psalm\Type\Atomic\TArrayKey; use Psalm\Type\Atomic\TBool; use Psalm\Type\Atomic\TCallable; -use Psalm\Type\Atomic\TCallableArray; use Psalm\Type\Atomic\TCallableKeyedArray; use Psalm\Type\Atomic\TCallableObject; use Psalm\Type\Atomic\TCallableString; @@ -84,6 +83,7 @@ use function count; use function explode; use function get_class; +use function in_array; use function is_int; use function min; use function strlen; @@ -532,7 +532,11 @@ public static function reconcile( } if ($assertion_type instanceof TValueOf) { - return $assertion_type->type; + return self::reconcileValueOf( + $codebase, + $assertion_type, + $failed_reconciliation, + ); } return null; @@ -1594,7 +1598,7 @@ private static function reconcileObject( bool $is_equality, ): Union { if ($existing_var_type->hasMixed()) { - return Type::getObject(); + return new Union([$assertion_type]); } $old_var_type_string = $existing_var_type->getId(); @@ -2302,7 +2306,7 @@ private static function reconcileArray( } elseif ($type instanceof TCallable) { $array_types[] = new TCallableKeyedArray([ new Union([new TClassString, new TObject]), - Type::getString(), + Type::getNonEmptyString(), ]); $redundant = false; @@ -2426,7 +2430,7 @@ private static function reconcileList( } elseif ($type instanceof TCallable) { $array_types[] = new TCallableKeyedArray([ new Union([new TClassString, new TObject]), - Type::getString(), + Type::getNonEmptyString(), ]); $redundant = false; @@ -2649,7 +2653,7 @@ private static function reconcileCallable( $callable_types[] = $type; $redundant = false; } elseif ($type instanceof TArray) { - $type = new TCallableArray($type->type_params); + $type = new TCallableKeyedArray($type->type_params); $callable_types[] = $type; $redundant = false; } elseif ($type instanceof TKeyedArray && count($type->properties) === 2) { @@ -2933,6 +2937,71 @@ private static function reconcileClassConstant( return TypeCombiner::combine(array_values($matched_class_constant_types), $codebase); } + /** + * @param Reconciler::RECONCILIATION_* $failed_reconciliation + */ + private static function reconcileValueOf( + Codebase $codebase, + TValueOf $assertion_type, + int &$failed_reconciliation + ): ?Union { + $reconciled_types = []; + + // For now, only enums are supported here + foreach ($assertion_type->type->getAtomicTypes() as $atomic_type) { + $enum_case_to_assert = null; + if ($atomic_type instanceof TClassConstant) { + $class_name = $atomic_type->fq_classlike_name; + $enum_case_to_assert = $atomic_type->const_name; + } elseif ($atomic_type instanceof TNamedObject) { + $class_name = $atomic_type->value; + } else { + return null; + } + + if (!$codebase->classOrInterfaceOrEnumExists($class_name)) { + return null; + } + + $class_storage = $codebase->classlike_storage_provider->get($class_name); + if (!$class_storage->is_enum) { + return null; + } + + if (!in_array($class_storage->enum_type, ['string', 'int'], true)) { + return null; + } + + // For value-of, the assertion is meant to return *ANY* value of *ANY* enum case + if ($enum_case_to_assert === null) { + foreach ($class_storage->enum_cases as $enum_case) { + assert( + $enum_case->value !== null, + 'Verified enum type above, value can not contain `null` anymore.', + ); + $reconciled_types[] = $enum_case->value; + } + + continue; + } + + $enum_case = $class_storage->enum_cases[$atomic_type->const_name] ?? null; + if ($enum_case === null) { + return null; + } + + assert($enum_case->value !== null, 'Verified enum type above, value can not contain `null` anymore.'); + $reconciled_types[] = $enum_case->value; + } + + if ($reconciled_types === []) { + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; + return Type::getNever(); + } + + return TypeCombiner::combine($reconciled_types, $codebase, false, false); + } + /** * @psalm-assert-if-true TCallableObject|TObjectWithProperties|TNamedObject $type */ diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index c3704ad3d4d..e8502df6f6c 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -28,9 +28,10 @@ use Psalm\Type\Atomic\TArrayKey; use Psalm\Type\Atomic\TBool; use Psalm\Type\Atomic\TCallable; -use Psalm\Type\Atomic\TCallableArray; +use Psalm\Type\Atomic\TCallableKeyedArray; use Psalm\Type\Atomic\TCallableObject; use Psalm\Type\Atomic\TCallableString; +use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TEmptyMixed; use Psalm\Type\Atomic\TEmptyNumeric; use Psalm\Type\Atomic\TEmptyScalar; @@ -1189,9 +1190,9 @@ private static function reconcileObject( $non_object_types[] = $type; } } elseif ($type instanceof TCallable) { - $non_object_types[] = new TCallableArray([ - Type::getArrayKey(), - Type::getMixed(), + $non_object_types[] = new TCallableKeyedArray([ + new Union([new TClassString, new TObject]), + Type::getNonEmptyString(), ]); $non_object_types[] = new TCallableString(); $redundant = false; @@ -1588,9 +1589,9 @@ private static function reconcileString( $non_string_types[] = new TInt(); $redundant = false; } elseif ($type instanceof TCallable) { - $non_string_types[] = new TCallableArray([ - Type::getArrayKey(), - Type::getMixed(), + $non_string_types[] = new TCallableKeyedArray([ + new Union([new TClassString, new TObject]), + Type::getNonEmptyString(), ]); $non_string_types[] = new TCallableObject(); $redundant = false; diff --git a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php index ba12f0b0bfb..e25548f49f2 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -35,6 +35,7 @@ use function array_merge; use function array_shift; use function array_values; +use function assert; use function strpos; /** @@ -281,6 +282,7 @@ private static function replaceTemplateParam( )); } elseif ($atomic_template_type instanceof TObject) { $first_atomic_type = array_shift($atomic_type->extra_types); + assert($first_atomic_type !== null); if ($atomic_type->extra_types) { $first_atomic_type = $first_atomic_type->setIntersectionTypes($atomic_type->extra_types); diff --git a/src/Psalm/Internal/Type/TemplateResult.php b/src/Psalm/Internal/Type/TemplateResult.php index 1564250c816..e8e7ef9610e 100644 --- a/src/Psalm/Internal/Type/TemplateResult.php +++ b/src/Psalm/Internal/Type/TemplateResult.php @@ -6,6 +6,9 @@ use Psalm\Type\Union; +use function array_merge; +use function array_replace_recursive; + /** * This class captures the result of running Psalm's argument analysis with * regard to generic parameters. @@ -65,4 +68,19 @@ public function __construct(array $template_types, array $lower_bounds) } } } + + public function merge(TemplateResult $result): TemplateResult + { + if ($result === $this) { + return $this; + } + + $instance = clone $this; + /** @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); + + return $instance; + } } diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 09ae9676cd6..cd5b2c62a6d 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -13,7 +13,6 @@ use Psalm\Type\Atomic\TArrayKey; use Psalm\Type\Atomic\TBool; use Psalm\Type\Atomic\TCallable; -use Psalm\Type\Atomic\TCallableArray; use Psalm\Type\Atomic\TCallableKeyedArray; use Psalm\Type\Atomic\TCallableObject; use Psalm\Type\Atomic\TCallableString; @@ -402,7 +401,6 @@ private static function scrapeTypeProperties( bool $allow_mixed_union, int $literal_limit, ): ?Union { - if ($type instanceof TMixed) { if ($type->from_loop_isset) { if ($combination->mixed_from_loop_isset === null) { @@ -546,11 +544,17 @@ private static function scrapeTypeProperties( } } - if ($type instanceof TArray && $type_key === 'array') { - if ($type instanceof TCallableArray && isset($combination->value_types['callable'])) { + if ($type instanceof TCallableKeyedArray) { + if (isset($combination->value_types['callable'])) { return null; } - + if ($combination->all_arrays_callable !== false) { + $combination->all_arrays_callable = true; + } else { + $combination->all_arrays_callable = false; + } + } + if ($type instanceof TArray && $type_key === 'array') { foreach ($type->type_params as $i => $type_param) { // See https://github.com/vimeo/psalm/pull/9439#issuecomment-1464563015 /** @psalm-suppress PropertyTypeCoercion */ @@ -589,14 +593,7 @@ private static function scrapeTypeProperties( $combination->all_arrays_class_string_maps = false; } - if ($type instanceof TCallableArray) { - if ($combination->all_arrays_callable !== false) { - $combination->all_arrays_callable = true; - } - } else { - $combination->all_arrays_callable = false; - } - + $combination->all_arrays_callable = false; return null; } @@ -958,8 +955,8 @@ private static function scrapeTypeProperties( if ($type instanceof TCallable && $type_key === 'callable') { if (($combination->value_types['string'] ?? null) instanceof TCallableString) { unset($combination->value_types['string']); - } elseif (!empty($combination->array_type_params) && $combination->all_arrays_callable) { - $combination->array_type_params = []; + } elseif (!empty($combination->objectlike_entries) && $combination->all_arrays_callable) { + $combination->objectlike_entries = []; } elseif (isset($combination->value_types['callable-object'])) { unset($combination->value_types['callable-object']); } @@ -1414,7 +1411,6 @@ private static function handleKeyedArrayEntries( $sealed || $fallback_key_type === null || $fallback_value_type === null ? null : [$fallback_key_type, $fallback_value_type], - (bool)$combination->all_arrays_lists, $from_docblock, ); } else { @@ -1527,7 +1523,7 @@ private static function getArrayTypeFromGenericParams( } if ($combination->all_arrays_callable) { - $array_type = new TCallableArray($generic_type_params); + $array_type = new TCallableKeyedArray($generic_type_params); } elseif ($combination->array_always_filled || ($combination->array_sometimes_filled && $overwrite_empty_array) || ($combination->objectlike_entries diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 3f9362aa2e7..db5735e1528 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -464,12 +464,6 @@ private static function getGenericParamClass( ); } - if (!$as->isSingle()) { - throw new TypeParseTreeException( - 'Invalid templated classname \'' . $as . '\'', - ); - } - foreach ($as->getAtomicTypes() as $t) { if ($t instanceof TObject) { return new TTemplateParamClass( @@ -1167,6 +1161,7 @@ private static function getTypeFromIntersectionTree( } $first_type = array_shift($keyed_intersection_types); + assert($first_type !== null); // Keyed array intersection are merged together and are not combinable with object-types if ($first_type instanceof TKeyedArray) { @@ -1235,7 +1230,7 @@ private static function getTypeFromCallableTree( $is_optional = $child_tree->has_default; } else { if ($child_tree instanceof Value && strpos($child_tree->value, '$') > 0) { - $child_tree->value = preg_replace('/(.+)\$.*/', '$1', $child_tree->value); + $child_tree->value = (string) preg_replace('/(.+)\$.*/', '$1', $child_tree->value); } $tree_type = self::getTypeFromTree( @@ -1628,9 +1623,7 @@ private static function resolveTypeAliases(Codebase $codebase, array $intersecti continue; } - $modified = true; - - $normalized_intersection_types[] = TypeExpander::expandAtomic( + $expanded_intersection_type = TypeExpander::expandAtomic( $codebase, $intersection_type, null, @@ -1643,6 +1636,9 @@ private static function resolveTypeAliases(Codebase $codebase, array $intersecti true, true, ); + + $modified = $modified || $expanded_intersection_type[0] !== $intersection_type; + $normalized_intersection_types[] = $expanded_intersection_type; } if ($modified === false) { diff --git a/src/Psalm/Internal/Type/TypeTokenizer.php b/src/Psalm/Internal/Type/TypeTokenizer.php index 97a25802417..3f62bffef73 100644 --- a/src/Psalm/Internal/Type/TypeTokenizer.php +++ b/src/Psalm/Internal/Type/TypeTokenizer.php @@ -409,7 +409,7 @@ public static function getFullyQualifiedTokens( } if (strpos($string_type_token[0], '$')) { - $string_type_token[0] = preg_replace('/(.+)\$.*/', '$1', $string_type_token[0]); + $string_type_token[0] = (string) preg_replace('/(.+)\$.*/', '$1', $string_type_token[0]); } $fixed_token = !isset($type_tokens[$i + 1]) || $type_tokens[$i + 1][0] !== '(' diff --git a/src/Psalm/Issue/DuplicateProperty.php b/src/Psalm/Issue/DuplicateProperty.php new file mode 100644 index 00000000000..89538730a7d --- /dev/null +++ b/src/Psalm/Issue/DuplicateProperty.php @@ -0,0 +1,9 @@ +stdout_report_options->format, - [Report::TYPE_CONSOLE, Report::TYPE_PHP_STORM], + [Report::TYPE_CONSOLE, Report::TYPE_PHP_STORM, Report::TYPE_GITHUB_ACTIONS], )) { echo str_repeat('-', 30) . "\n"; diff --git a/src/Psalm/Plugin/Shepherd.php b/src/Psalm/Plugin/Shepherd.php index 77d4512c1cb..6955b504dee 100644 --- a/src/Psalm/Plugin/Shepherd.php +++ b/src/Psalm/Plugin/Shepherd.php @@ -15,6 +15,7 @@ use function array_key_exists; use function array_merge; use function array_values; +use function assert; use function curl_close; use function curl_exec; use function curl_getinfo; @@ -127,6 +128,7 @@ private static function sendPayload(string $endpoint, array $rawPayload): void // Prepare new cURL resource $ch = curl_init($endpoint); + assert($ch !== false); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLINFO_HEADER_OUT, true); diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php index 12ef494ae62..5d622b16f7e 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -503,6 +503,10 @@ public function hasAttributeIncludingParents( } foreach ($this->parent_classes as $parent_class) { + // skip missing dependencies + if (!$codebase->classlike_storage_provider->has($parent_class)) { + continue; + } $parent_class_storage = $codebase->classlike_storage_provider->get($parent_class); if ($parent_class_storage->hasAttribute($fq_class_name)) { return true; diff --git a/src/Psalm/Storage/EnumCaseStorage.php b/src/Psalm/Storage/EnumCaseStorage.php index 269a4fdc87a..ca9be915aae 100644 --- a/src/Psalm/Storage/EnumCaseStorage.php +++ b/src/Psalm/Storage/EnumCaseStorage.php @@ -8,24 +8,11 @@ final class EnumCaseStorage { - /** - * @var int|string|null - */ - public $value; - - /** @var CodeLocation */ - public $stmt_location; - - /** - * @var bool - */ - public $deprecated = false; + public bool $deprecated = false; public function __construct( - int|string|null $value, - CodeLocation $location, + public int|string|null $value, + public CodeLocation $location, ) { - $this->value = $value; - $this->stmt_location = $location; } } diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 02059a16efb..01bfc03c8c1 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -60,6 +60,7 @@ use function explode; use function get_class; use function implode; +use function is_int; use function preg_quote; use function preg_replace; use function stripos; @@ -143,7 +144,7 @@ public static function getStringFromFQCLN( } if ($namespace && stripos($value, $namespace . '\\') === 0) { - $candidate = preg_replace( + $candidate = (string) preg_replace( '/^' . preg_quote($namespace . '\\') . '/i', '', $value, @@ -260,6 +261,20 @@ public static function getNumericString(): Union return new Union([$type]); } + /** + * @psalm-suppress PossiblyUnusedMethod + * @param int|string $value + * @return TLiteralString|TLiteralInt + */ + public static function getLiteral($value): Atomic + { + if (is_int($value)) { + return new TLiteralInt($value); + } + + return TLiteralString::make($value); + } + public static function getString(?string $value = null): Union { return new Union([$value === null ? new TString() : self::getAtomicStringFromLiteral($value)]); diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 554bb0e8ecc..a879ec989bb 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -18,7 +18,6 @@ use Psalm\Type\Atomic\TArrayKey; use Psalm\Type\Atomic\TBool; use Psalm\Type\Atomic\TCallable; -use Psalm\Type\Atomic\TCallableArray; use Psalm\Type\Atomic\TCallableKeyedArray; use Psalm\Type\Atomic\TCallableObject; use Psalm\Type\Atomic\TCallableString; @@ -260,9 +259,19 @@ private static function createInner( ]); case 'callable-array': - return new TCallableArray([ - new Union([new TArrayKey($from_docblock)]), - new Union([new TMixed(false, $from_docblock)]), + $classString = new TClassString( + 'object', + null, + false, + false, + false, + true, + ); + $object = new TObject(true); + $string = new TNonEmptyString(true); + return new TCallableKeyedArray([ + new Union([$classString, $object]), + new Union([$string]), ]); case 'list': @@ -323,7 +332,7 @@ private static function createInner( return $analysis_php_version_id !== null ? new TNamedObject($value) : new TScalar(); case 'null': - if ($analysis_php_version_id === null || $analysis_php_version_id >= 8_00_00) { + if ($analysis_php_version_id === null || $analysis_php_version_id >= 7_00_00) { return new TNull(); } @@ -461,7 +470,6 @@ public function isCallableType(): bool return $this instanceof TCallable || $this instanceof TCallableObject || $this instanceof TCallableString - || $this instanceof TCallableArray || $this instanceof TCallableKeyedArray || $this instanceof TClosure; } diff --git a/src/Psalm/Type/Atomic/TCallableArray.php b/src/Psalm/Type/Atomic/TCallableArray.php deleted file mode 100644 index 559b014361d..00000000000 --- a/src/Psalm/Type/Atomic/TCallableArray.php +++ /dev/null @@ -1,18 +0,0 @@ - $properties + * @param array{Union, Union}|null $fallback_params + * @param array $class_strings + */ + public function __construct( + array $properties, + ?array $class_strings = null, + ?array $fallback_params = null, + bool $from_docblock = false + ) { + parent::__construct( + $properties, + $class_strings, + $fallback_params, + true, + $from_docblock, + ); + } } diff --git a/src/Psalm/Type/Atomic/TLiteralFloat.php b/src/Psalm/Type/Atomic/TLiteralFloat.php index 74676334786..3fe2a407269 100644 --- a/src/Psalm/Type/Atomic/TLiteralFloat.php +++ b/src/Psalm/Type/Atomic/TLiteralFloat.php @@ -11,8 +11,7 @@ */ final class TLiteralFloat extends TFloat { - /** @var float */ - public $value; + public float $value; public function __construct(float $value, bool $from_docblock = false) { diff --git a/src/Psalm/Type/Atomic/TLiteralInt.php b/src/Psalm/Type/Atomic/TLiteralInt.php index 4cc4650f600..72706da25bc 100644 --- a/src/Psalm/Type/Atomic/TLiteralInt.php +++ b/src/Psalm/Type/Atomic/TLiteralInt.php @@ -11,8 +11,7 @@ */ final class TLiteralInt extends TInt { - /** @var int */ - public $value; + public int $value; public function __construct(int $value, bool $from_docblock = false) { diff --git a/src/Psalm/Type/Atomic/TValueOf.php b/src/Psalm/Type/Atomic/TValueOf.php index 09f75acbd0c..b278bc82989 100644 --- a/src/Psalm/Type/Atomic/TValueOf.php +++ b/src/Psalm/Type/Atomic/TValueOf.php @@ -5,7 +5,6 @@ namespace Psalm\Type\Atomic; use Psalm\Codebase; -use Psalm\Internal\Codebase\ConstantTypeResolver; use Psalm\Storage\EnumCaseStorage; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -39,13 +38,15 @@ private static function getValueTypeForNamedObject(array $cases, TNamedObject $a assert(isset($cases[$atomic_type->case_name]), 'Should\'ve been verified in TValueOf#getValueType'); $value = $cases[$atomic_type->case_name]->value; assert($value !== null, 'Backed enum must have a value.'); - return new Union([ConstantTypeResolver::getLiteralTypeFromScalarValue($value)]); + + return new Union([$value]); } return new Union(array_map( function (EnumCaseStorage $case): Atomic { assert($case->value !== null); // Backed enum must have a value - return ConstantTypeResolver::getLiteralTypeFromScalarValue($case->value); + + return $case->value; }, array_values($cases), )); diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 18e3d05215f..6cc30cdc79f 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -356,7 +356,7 @@ public static function reconcileKeyedTypes( } // Set references pointing to $new_key to point // to the first other reference from the same group - $new_primary_reference = key($reference_graph[$references_to_fix[0]]); + $new_primary_reference = (string) key($reference_graph[$references_to_fix[0]]); unset($existing_references[$new_primary_reference]); foreach ($existing_references as $existing_reference => $existing_referenced) { if ($existing_referenced === $new_key) { @@ -425,6 +425,10 @@ private static function addNestedAssertions(array $new_types, array $existing_ty { foreach ($new_types as $nk => $type) { if (strpos($nk, '[') || strpos($nk, '->')) { + $type = array_values($type); + if (!isset($type[0][0])) { + continue; + } if ($type[0][0] instanceof IsEqualIsset || $type[0][0] instanceof IsIsset || $type[0][0] instanceof NonEmpty @@ -450,7 +454,7 @@ private static function addNestedAssertions(array $new_types, array $existing_ty $divider = array_shift($key_parts); if ($divider === '[') { - $array_key = array_shift($key_parts); + $array_key = (string) array_shift($key_parts); array_shift($key_parts); $new_base_key = $base_key . '[' . $array_key . ']'; @@ -686,7 +690,7 @@ private static function getValueForKey( $divider = array_shift($key_parts); if ($divider === '[') { - $array_key = array_shift($key_parts); + $array_key = (string) array_shift($key_parts); array_shift($key_parts); $new_base_key = $base_key . '[' . $array_key . ']'; @@ -790,7 +794,7 @@ private static function getValueForKey( $base_key = $new_base_key; } elseif ($divider === '->' || $divider === '::$') { - $property_name = array_shift($key_parts); + $property_name = (string) array_shift($key_parts); $new_base_key = $base_key . $divider . $property_name; if (!isset($existing_keys[$new_base_key])) { diff --git a/src/Psalm/Type/TaintKind.php b/src/Psalm/Type/TaintKind.php index ffa0ce2a872..ecd355d3532 100644 --- a/src/Psalm/Type/TaintKind.php +++ b/src/Psalm/Type/TaintKind.php @@ -22,6 +22,8 @@ final class TaintKind public const INPUT_FILE = 'file'; public const INPUT_COOKIE = 'cookie'; public const INPUT_HEADER = 'header'; + public const INPUT_XPATH = 'xpath'; + public const INPUT_SLEEP = 'sleep'; 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 cc561a18eac..19c8bb63030 100644 --- a/src/Psalm/Type/TaintKindGroup.php +++ b/src/Psalm/Type/TaintKindGroup.php @@ -23,5 +23,7 @@ final class TaintKindGroup TaintKind::INPUT_FILE, TaintKind::INPUT_HEADER, TaintKind::INPUT_COOKIE, + TaintKind::INPUT_XPATH, + TaintKind::INPUT_SLEEP, ]; } diff --git a/stubs/CoreGenericClasses.phpstub b/stubs/CoreGenericClasses.phpstub index 8e650083f18..2b7b76da7e4 100644 --- a/stubs/CoreGenericClasses.phpstub +++ b/stubs/CoreGenericClasses.phpstub @@ -125,7 +125,7 @@ interface ArrayAccess { * This class allows objects to work as arrays. * @link http://php.net/manual/en/class.arrayobject.php * - * @template TKey + * @template TKey of array-key * @template TValue * @template-implements IteratorAggregate * @template-implements ArrayAccess diff --git a/stubs/CoreGenericFunctions.phpstub b/stubs/CoreGenericFunctions.phpstub index 6603f130fbf..026e90c65cb 100644 --- a/stubs/CoreGenericFunctions.phpstub +++ b/stubs/CoreGenericFunctions.phpstub @@ -1359,9 +1359,10 @@ function realpath(string $path) {} * * @param numeric-string $num1 * @param numeric-string $num2 + * @param int|null $scale * @return (PHP_MAJOR_VERSION is 8 ? numeric-string : ($num2 is "0" ? null : numeric-string)) */ -function bcdiv(string $num1, string $num2, int $scale = 0): ?string {} +function bcdiv(string $num1, string $num2, ?int $scale = null): ?string {} /** * @psalm-pure @@ -1785,3 +1786,36 @@ if (defined('GLOB_BRACE')) { function glob (string $pattern, int $flags = 0): array|false {} } +/** + * @psalm-template TOutput of array|null + * + * @param TOutput $output + * @param-out (TOutput is null ? list : array) $output + * @param-out int $result_code + * + * @psalm-taint-specialize + * @psalm-taint-sink shell $command + */ +function exec(string $command, &$output = null, int &$result_code = null): string|false {} + +/** + * @psalm-taint-specialize + * @psalm-taint-sink sleep $seconds + */ +function sleep(int $seconds): int {} + +/** + * @psalm-taint-sink sleep $microseconds + */ +function usleep(int $microseconds): void {} + +/** + * @psalm-taint-sink sleep $seconds + * @psalm-taint-sink sleep $nanoseconds + */ +function time_nanosleep(int $seconds, int $nanoseconds): array|bool {} + +/** + * @psalm-taint-sink sleep $timestamp + */ +function time_sleep_until(float $timestamp): bool {} diff --git a/stubs/CoreImmutableClasses.phpstub b/stubs/CoreImmutableClasses.phpstub index 91c0a41c69c..8698d1151ca 100644 --- a/stubs/CoreImmutableClasses.phpstub +++ b/stubs/CoreImmutableClasses.phpstub @@ -105,6 +105,7 @@ class DateTimeImmutable implements DateTimeInterface */ class DateTimeZone { + /** @param non-empty-string $timezone */ public function __construct(string $timezone) {} } diff --git a/stubs/Php74.phpstub b/stubs/Php74.phpstub new file mode 100644 index 00000000000..4ade3a6da4c --- /dev/null +++ b/stubs/Php74.phpstub @@ -0,0 +1,11 @@ + return + * + * @param null|string|array $allowed_tags + */ +function strip_tags(string $string, null|string|array $allowed_tags = null) : string {} diff --git a/stubs/Reflection.phpstub b/stubs/Reflection.phpstub index 82d2090cc0e..3e86431e581 100644 --- a/stubs/Reflection.phpstub +++ b/stubs/Reflection.phpstub @@ -15,6 +15,7 @@ class ReflectionClass implements Reflector { /** * @param T|class-string|interface-string|trait-string|enum-string $argument * @psalm-pure + * @psalm-taint-sink callable $argument */ public function __construct($argument) {} @@ -424,7 +425,7 @@ class ReflectionFunction extends ReflectionFunctionAbstract { /** * @param callable-string|Closure $function - * + * @psalm-taint-sink callable $function * @psalm-pure */ public function __construct(callable $function) {} diff --git a/stubs/extensions/dom.phpstub b/stubs/extensions/dom.phpstub index 5b42368ce98..76563f18f01 100644 --- a/stubs/extensions/dom.phpstub +++ b/stubs/extensions/dom.phpstub @@ -471,13 +471,13 @@ class DOMDocument extends DOMNode implements DOMParentNode public function importNode(DOMNode $node, bool $deep = false) {} /** - * @return DOMDocument|false + * @return bool * @psalm-ignore-falsable-return **/ public function load(string $filename, int $options = 0) {} /** - * @return DOMDocument|false + * @return bool * @psalm-ignore-falsable-return */ public function loadXML(string $source, int $options = 0) {} @@ -492,10 +492,10 @@ class DOMDocument extends DOMNode implements DOMParentNode */ public function save(string $filename, int $options = 0) {} - /** @return DOMDocument|bool */ + /** @return bool */ public function loadHTML(string $source, int $options = 0) {} - /** @return DOMDocument|bool */ + /** @return bool */ public function loadHTMLFile(string $filename, int $options = 0) {} /** @@ -972,10 +972,15 @@ class DOMXPath public function __construct(DOMDocument $document, bool $registerNodeNS = true) {} + /** + * @return DOMNodeList|false + * @psalm-taint-sink xpath $expression + */ public function evaluate(string $expression, ?DOMNode $contextNode = null, bool $registerNodeNS = true): mixed {} /** * @return DOMNodeList|false + * @psalm-taint-sink xpath $expression */ public function query(string $expression, ?DOMNode $contextNode = null, bool $registerNodeNS = true): mixed {} diff --git a/stubs/extensions/intl.phpstub b/stubs/extensions/intl.phpstub new file mode 100644 index 00000000000..99ead8eaeed --- /dev/null +++ b/stubs/extensions/intl.phpstub @@ -0,0 +1,107 @@ +getConstants() as $name => $value) echo "const " . $name . " = " . var_export($value, true) . ";" . PHP_EOL;' +// ``` +const MYSQLI_READ_DEFAULT_GROUP = 5; +const MYSQLI_READ_DEFAULT_FILE = 4; +const MYSQLI_OPT_CONNECT_TIMEOUT = 0; +const MYSQLI_OPT_LOCAL_INFILE = 8; +const MYSQLI_OPT_LOAD_DATA_LOCAL_DIR = 43; +const MYSQLI_INIT_COMMAND = 3; +const MYSQLI_OPT_READ_TIMEOUT = 11; +const MYSQLI_OPT_NET_CMD_BUFFER_SIZE = 202; +const MYSQLI_OPT_NET_READ_BUFFER_SIZE = 203; +const MYSQLI_OPT_INT_AND_FLOAT_NATIVE = 201; +const MYSQLI_OPT_SSL_VERIFY_SERVER_CERT = 21; +const MYSQLI_SERVER_PUBLIC_KEY = 35; +const MYSQLI_CLIENT_SSL = 2048; +const MYSQLI_CLIENT_COMPRESS = 32; +const MYSQLI_CLIENT_INTERACTIVE = 1024; +const MYSQLI_CLIENT_IGNORE_SPACE = 256; +const MYSQLI_CLIENT_NO_SCHEMA = 16; +const MYSQLI_CLIENT_FOUND_ROWS = 2; +const MYSQLI_CLIENT_SSL_VERIFY_SERVER_CERT = 1073741824; +const MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT = 64; +const MYSQLI_CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS = 4194304; +const MYSQLI_OPT_CAN_HANDLE_EXPIRED_PASSWORDS = 37; +const MYSQLI_STORE_RESULT = 0; +const MYSQLI_USE_RESULT = 1; +const MYSQLI_ASYNC = 8; +const MYSQLI_STORE_RESULT_COPY_DATA = 16; +const MYSQLI_ASSOC = 1; +const MYSQLI_NUM = 2; +const MYSQLI_BOTH = 3; +const MYSQLI_STMT_ATTR_UPDATE_MAX_LENGTH = 0; +const MYSQLI_STMT_ATTR_CURSOR_TYPE = 1; +const MYSQLI_CURSOR_TYPE_NO_CURSOR = 0; +const MYSQLI_CURSOR_TYPE_READ_ONLY = 1; +const MYSQLI_CURSOR_TYPE_FOR_UPDATE = 2; +const MYSQLI_CURSOR_TYPE_SCROLLABLE = 4; +const MYSQLI_STMT_ATTR_PREFETCH_ROWS = 2; +const MYSQLI_NOT_NULL_FLAG = 1; +const MYSQLI_PRI_KEY_FLAG = 2; +const MYSQLI_UNIQUE_KEY_FLAG = 4; +const MYSQLI_MULTIPLE_KEY_FLAG = 8; +const MYSQLI_BLOB_FLAG = 16; +const MYSQLI_UNSIGNED_FLAG = 32; +const MYSQLI_ZEROFILL_FLAG = 64; +const MYSQLI_AUTO_INCREMENT_FLAG = 512; +const MYSQLI_TIMESTAMP_FLAG = 1024; +const MYSQLI_SET_FLAG = 2048; +const MYSQLI_NUM_FLAG = 32768; +const MYSQLI_PART_KEY_FLAG = 16384; +const MYSQLI_GROUP_FLAG = 32768; +const MYSQLI_ENUM_FLAG = 256; +const MYSQLI_BINARY_FLAG = 128; +const MYSQLI_NO_DEFAULT_VALUE_FLAG = 4096; +const MYSQLI_ON_UPDATE_NOW_FLAG = 8192; +const MYSQLI_TYPE_DECIMAL = 0; +const MYSQLI_TYPE_TINY = 1; +const MYSQLI_TYPE_SHORT = 2; +const MYSQLI_TYPE_LONG = 3; +const MYSQLI_TYPE_FLOAT = 4; +const MYSQLI_TYPE_DOUBLE = 5; +const MYSQLI_TYPE_NULL = 6; +const MYSQLI_TYPE_TIMESTAMP = 7; +const MYSQLI_TYPE_LONGLONG = 8; +const MYSQLI_TYPE_INT24 = 9; +const MYSQLI_TYPE_DATE = 10; +const MYSQLI_TYPE_TIME = 11; +const MYSQLI_TYPE_DATETIME = 12; +const MYSQLI_TYPE_YEAR = 13; +const MYSQLI_TYPE_NEWDATE = 14; +const MYSQLI_TYPE_ENUM = 247; +const MYSQLI_TYPE_SET = 248; +const MYSQLI_TYPE_TINY_BLOB = 249; +const MYSQLI_TYPE_MEDIUM_BLOB = 250; +const MYSQLI_TYPE_LONG_BLOB = 251; +const MYSQLI_TYPE_BLOB = 252; +const MYSQLI_TYPE_VAR_STRING = 253; +const MYSQLI_TYPE_STRING = 254; +const MYSQLI_TYPE_CHAR = 1; +const MYSQLI_TYPE_INTERVAL = 247; +const MYSQLI_TYPE_GEOMETRY = 255; +const MYSQLI_TYPE_JSON = 245; +const MYSQLI_TYPE_NEWDECIMAL = 246; +const MYSQLI_TYPE_BIT = 16; +const MYSQLI_SET_CHARSET_NAME = 7; +const MYSQLI_SET_CHARSET_DIR = 6; +const MYSQLI_NO_DATA = 100; +const MYSQLI_DATA_TRUNCATED = 101; +const MYSQLI_REPORT_INDEX = 4; +const MYSQLI_REPORT_ERROR = 1; +const MYSQLI_REPORT_STRICT = 2; +const MYSQLI_REPORT_ALL = 255; +const MYSQLI_REPORT_OFF = 0; +const MYSQLI_DEBUG_TRACE_ENABLED = 0; +const MYSQLI_SERVER_QUERY_NO_GOOD_INDEX_USED = 16; +const MYSQLI_SERVER_QUERY_NO_INDEX_USED = 32; +const MYSQLI_SERVER_QUERY_WAS_SLOW = 2048; +const MYSQLI_SERVER_PS_OUT_PARAMS = 4096; +const MYSQLI_REFRESH_GRANT = 1; +const MYSQLI_REFRESH_LOG = 2; +const MYSQLI_REFRESH_TABLES = 4; +const MYSQLI_REFRESH_HOSTS = 8; +const MYSQLI_REFRESH_STATUS = 16; +const MYSQLI_REFRESH_THREADS = 32; +const MYSQLI_REFRESH_REPLICA = 64; +const MYSQLI_REFRESH_SLAVE = 64; +const MYSQLI_REFRESH_MASTER = 128; +const MYSQLI_REFRESH_BACKUP_LOG = 2097152; +const MYSQLI_TRANS_START_WITH_CONSISTENT_SNAPSHOT = 1; +const MYSQLI_TRANS_START_READ_WRITE = 2; +const MYSQLI_TRANS_START_READ_ONLY = 4; +const MYSQLI_TRANS_COR_AND_CHAIN = 1; +const MYSQLI_TRANS_COR_AND_NO_CHAIN = 2; +const MYSQLI_TRANS_COR_RELEASE = 4; +const MYSQLI_TRANS_COR_NO_RELEASE = 8; +/** @var bool */ +const MYSQLI_IS_MARIADB = false; + class mysqli { /** @@ -82,3 +202,9 @@ class mysqli_stmt * @return T|null|false */ function mysqli_fetch_object(mysqli_result $result, string $class = stdClass::class, array $constructor_args = []): object|false|null {} + + +final class mysqli_sql_exception extends RuntimeException implements Stringable, Throwable { + protected string $sqlstate = '00000'; + public function getSqlState(): string {} +} diff --git a/stubs/extensions/pdo.phpstub b/stubs/extensions/pdo.phpstub index aec4965477c..4169ffbed03 100644 --- a/stubs/extensions/pdo.phpstub +++ b/stubs/extensions/pdo.phpstub @@ -151,14 +151,8 @@ class PDOStatement implements Traversable */ public function fetchObject($class = \stdclass::class, array $ctorArgs = array()) {} - /** - * @psalm-taint-sink sql $value - */ public function bindValue(string|int $param, mixed $value, int $type = PDO::PARAM_STR): bool {} - /** - * @psalm-taint-sink sql $var - */ public function bindParam(string|int $param, mixed &$var, int $type = PDO::PARAM_STR, int $maxLength = 0, mixed $driverOptions = null): bool {} } diff --git a/stubs/extensions/simplexml.phpstub b/stubs/extensions/simplexml.phpstub index 5c2fb2f5736..d2501f62096 100644 --- a/stubs/extensions/simplexml.phpstub +++ b/stubs/extensions/simplexml.phpstub @@ -25,10 +25,14 @@ function simplexml_import_dom(SimpleXMLElement|DOMNode $node, ?string $class_nam /** * @implements Traversable + * @psalm-no-seal-properties */ class SimpleXMLElement implements Traversable, Countable { - /** @return array|null|false */ + /** + * @return array|null|false + * @psalm-taint-sink xpath $expression + */ public function xpath(string $expression) {} public function registerXPathNamespace(string $prefix, string $namespace): bool {} @@ -63,6 +67,8 @@ class SimpleXMLElement implements Traversable, Countable public function __toString(): string {} public function count(): int {} + + public function __get(string $name): SimpleXMLElement|SimpleXMLIterator|null {} } /** diff --git a/tests/ArrayAccessTest.php b/tests/ArrayAccessTest.php index 01da42eb9e6..2302b2661b6 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -1013,7 +1013,7 @@ public function pop(): void { ], 'simpleXmlArrayFetch' => [ 'code' => ' ['$a===' => "array{1: 'b'}"], ], + 'noCrashOnUnknownClassArrayAccess' => [ + 'code' => <<<'PHP' + [], + 'ignored_issues' => ['UndefinedDocblockClass'], + ], ]; } diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index f18fee7eb07..11f30867619 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -1297,7 +1297,6 @@ function foo(ArrayObject $a) : array { /** * @psalm-suppress MixedAssignment - * @psalm-suppress MixedArrayOffset */ foreach ($a as $k => $v) { $arr[$k] = $v; diff --git a/tests/AssertAnnotationTest.php b/tests/AssertAnnotationTest.php index fba322b7d98..5b619971f8e 100644 --- a/tests/AssertAnnotationTest.php +++ b/tests/AssertAnnotationTest.php @@ -2194,6 +2194,10 @@ function assertSomeString(string $foo): void function assertSomeInt(int $foo): void {} + /** @psalm-assert value-of $foo */ + function assertAnyEnumValue(string|int $foo): void + {} + /** @param "foo"|"bar" $foo */ function takesSomeStringFromEnum(string $foo): StringEnum { @@ -2216,8 +2220,14 @@ function takesSomeIntFromEnum(int $foo): IntEnum assertSomeInt($int); takesSomeIntFromEnum($int); + + /** @var string|int $potentialEnumValue */ + $potentialEnumValue = null; + assertAnyEnumValue($potentialEnumValue); ', - 'assertions' => [], + 'assertions' => [ + '$potentialEnumValue===' => "'bar'|'baz'|'foo'|1|2|3", + ], 'ignored_issues' => [], 'php_version' => '8.1', ], @@ -2873,6 +2883,92 @@ public static function doAssert($value): void '$iterable===' => 'non-empty-list', ], ], + 'assertFromInheritedDocBlock' => [ + 'code' => ' + */ + abstract class AbstractPluginManager implements PluginManagerInterface + { + } + + /** + * @template InstanceType of object + * @template-extends AbstractPluginManager + */ + abstract class AbstractSingleInstancePluginManager extends AbstractPluginManager + { + public function validate(mixed $value): void + { + } + } + } + + namespace Namespace2 { + use InvalidArgumentException;use Namespace1\AbstractSingleInstancePluginManager; + use Namespace1\AbstractPluginManager; + use stdClass; + + /** @template-extends AbstractSingleInstancePluginManager */ + final class Qoo extends AbstractSingleInstancePluginManager + { + } + + /** @template-extends AbstractPluginManager */ + final class Ooq extends AbstractPluginManager + { + public function validate(mixed $value): void + { + } + } + } + + namespace { + $baz = new \Namespace2\Qoo(); + + /** @var mixed $object */ + $object = null; + $baz->validate($object); + + $ooq = new \Namespace2\Ooq(); + /** @var mixed $callable */ + $callable = null; + $ooq->validate($callable); + } + ', + 'assertions' => [ + '$object===' => 'stdClass', + '$callable===' => 'callable', + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'objectShapeAssertion' => [ + 'code' => ' [ + '$value===' => 'object{foo:string, bar:int}', + ], + 'ignored_issues' => [], + 'php_version' => '8.0', + ], ]; } diff --git a/tests/AsyncTestCase.php b/tests/AsyncTestCase.php index e9834d7af89..f7fb5b30c36 100644 --- a/tests/AsyncTestCase.php +++ b/tests/AsyncTestCase.php @@ -48,7 +48,7 @@ public static function setUpBeforeClass(): void } parent::setUpBeforeClass(); - self::$src_dir_path = getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR; + self::$src_dir_path = (string) getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR; } protected function makeConfig(): Config diff --git a/tests/BinaryOperationTest.php b/tests/BinaryOperationTest.php index 3ac18147fd4..f0fe0c9e3ae 100644 --- a/tests/BinaryOperationTest.php +++ b/tests/BinaryOperationTest.php @@ -1022,6 +1022,58 @@ function toPositiveInt(int $i): int '$a===' => 'float(9.2233720368548E+18)', ], ], + 'invalidArrayOperations' => [ + 'code' => <<<'PHP' + [ + '$a1' => 'float|int', + '$a2' => 'float|int', + '$a3' => 'array', + '$b1' => 'float|int', + '$b2' => 'float|int', + '$b3' => 'float|int', + '$c1' => 'float|int', + '$c2' => 'float|int', + '$c3' => 'float|int', + '$d1' => 'float|int', + '$d2' => 'float|int', + '$d3' => 'float|int', + '$e1' => 'float|int', + '$e2' => 'float|int', + '$e3' => 'float|int', + '$f1' => 'float|int', + '$f2' => 'float|int', + '$f3' => 'float|int', + ], + 'ignored_issues' => ['InvalidOperand'], + ], ]; } diff --git a/tests/CallableTest.php b/tests/CallableTest.php index ff9acffaf65..a6c4f421fde 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -1821,6 +1821,41 @@ abstract class TestClass { use TestTrait; }', ], + 'variadicClosureAssignability' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.0', + ], + 'callableArrayTypes' => [ + 'code' => ' [ + '$a' => 'class-string|object', + '$b' => 'string', + '$c' => 'list{class-string|object, string}', + ], + ], ]; } @@ -2262,6 +2297,29 @@ function appHandler(mixed $param1): array 'ignored_issues' => [], 'php_version' => '8.1', ], + 'variadicClosureAssignability' => [ + 'code' => ' 'InvalidScalarArgument', + 'ignored_issues' => [], + 'php_version' => '8.0', + ], ]; } } diff --git a/tests/ClassLikeStringTest.php b/tests/ClassLikeStringTest.php index 1e7f5b9eaab..b3bcca56121 100644 --- a/tests/ClassLikeStringTest.php +++ b/tests/ClassLikeStringTest.php @@ -879,6 +879,27 @@ class TypeTwo {} $foo->baz = TypeTwo::class; $foo->baz = TypeTwo::class;', ], + 'classStringOfUnionTypeParameter' => [ + 'code' => ' $class + * @return class-string + */ + function test(string $class): string { + return $class; + } + + $r = test(A::class);', + 'assertions' => [ + '$r' => 'class-string', + ], + ], ]; } diff --git a/tests/ClassTest.php b/tests/ClassTest.php index 89778446a46..a5645130310 100644 --- a/tests/ClassTest.php +++ b/tests/ClassTest.php @@ -640,7 +640,7 @@ function intersect(A $a) { 'code' => ' @@ -675,7 +675,7 @@ class b extends a { 'preventDoubleStaticResolution2' => [ 'code' => ' @@ -712,7 +712,7 @@ public function ret(): iter { 'preventDoubleStaticResolution3' => [ 'code' => ' @@ -857,7 +857,6 @@ private final function __construct() {} */ class BaseClass {} class FooClass extends BaseClass {} - $a = new FooClass(); PHP, ], 'unionInheritorIsAllowed' => [ @@ -868,9 +867,7 @@ class FooClass extends BaseClass {} */ class BaseClass {} class FooClass extends BaseClass {} - $a = new FooClass(); class BarClass extends FooClass {} - $b = new BarClass(); PHP, ], 'multiInheritorIsAllowed' => [ @@ -881,9 +878,7 @@ class BarClass extends FooClass {} */ class BaseClass {} class FooClass extends BaseClass {} - $a = new FooClass(); class BarClass extends FooClass {} - $b = new BarClass(); PHP, ], 'skippedInheritorIsAllowed' => [ @@ -894,9 +889,7 @@ class BarClass extends FooClass {} */ class BaseClass {} class FooClass extends BaseClass {} - $a = new FooClass(); class BarClass extends FooClass {} - $b = new BarClass(); PHP, ], 'CompositeInheritorIsAllowed' => [ @@ -908,7 +901,6 @@ class BarClass extends FooClass {} class BaseClass {} interface FooInterface {} class BarClass extends BaseClass implements FooInterface {} - $b = new BarClass(); PHP, ], 'InterfaceInheritorIsAllowed' => [ @@ -919,12 +911,10 @@ class BarClass extends BaseClass implements FooInterface {} */ interface BaseInterface {} class FooClass implements BaseInterface {} - $a = new FooClass(); class BarClass implements BaseInterface {} - $b = new BarClass(); PHP, - ], - 'MultiInterfaceInheritorIsAllowed' => [ + ], + 'MultiInterfaceInheritorIsAllowed' => [ 'code' => <<<'PHP' [ + 'code' => <<<'PHP' + 'InheritorViolation', 'ignored_issues' => [], @@ -1406,7 +1404,18 @@ class BazClass extends BaseClass {} // this is an error */ interface BaseInterface {} class BazClass implements BaseInterface {} - $a = new BazClass(); + PHP, + 'error_message' => 'InheritorViolation', + 'ignored_issues' => [], + ], + 'interfaceCannotImplementIfNotInInheritors' => [ + 'code' => <<<'PHP' + 'InheritorViolation', 'ignored_issues' => [], @@ -1423,11 +1432,54 @@ interface InterfaceA {} */ interface InterfaceB {} class BazClass implements InterFaceA, InterFaceB {} - $a = new BazClass(); PHP, 'error_message' => 'InheritorViolation', 'ignored_issues' => [], ], + 'duplicateInstanceProperties' => [ + 'code' => <<<'PHP' + 'DuplicateProperty', + 'ignored_issues' => [], + ], + 'duplicateStaticProperties' => [ + 'code' => <<<'PHP' + 'DuplicateProperty', + 'ignored_issues' => [], + ], + 'duplicateMixedProperties' => [ + 'code' => <<<'PHP' + 'DuplicateProperty', + 'ignored_issues' => [], + ], + 'duplicatePropertiesDifferentVisibility' => [ + 'code' => <<<'PHP' + 'DuplicateProperty', + 'ignored_issues' => [], + ], ]; } } diff --git a/tests/ClosureTest.php b/tests/ClosureTest.php index 43d441944fa..43fff6bb036 100644 --- a/tests/ClosureTest.php +++ b/tests/ClosureTest.php @@ -957,6 +957,20 @@ function () { } PHP, ], + 'returnByReferenceVariableInClosure' => [ + 'code' => ' [ + 'code' => ' $x; + ', + ], ]; } @@ -1428,6 +1442,20 @@ public function f(): int { 'ignored_issues' => [], 'php_version' => '8.1', ], + 'returnByReferenceNonVariableInClosure' => [ + 'code' => ' 'NonVariableReferenceReturn', + ], + 'returnByReferenceNonVariableInShortClosure' => [ + 'code' => ' 45; + ', + 'error_message' => 'NonVariableReferenceReturn', + ], ]; } } diff --git a/tests/Config/ConfigFileTest.php b/tests/Config/ConfigFileTest.php index a198a41c87d..1a58495b728 100644 --- a/tests/Config/ConfigFileTest.php +++ b/tests/Config/ConfigFileTest.php @@ -8,6 +8,7 @@ use Psalm\Internal\RuntimeCaches; use Psalm\Tests\TestCase; +use function assert; use function file_get_contents; use function file_put_contents; use function getcwd; @@ -26,7 +27,9 @@ class ConfigFileTest extends TestCase public function setUp(): void { RuntimeCaches::clearAll(); - $this->file_path = tempnam(sys_get_temp_dir(), 'psalm-test-config'); + $temp_name = tempnam(sys_get_temp_dir(), 'psalm-test-config'); + assert($temp_name !== false); + $this->file_path = $temp_name; } public function tearDown(): void @@ -65,6 +68,8 @@ public function addCanAddPluginClassToExistingPluginsNode(): void $config_file = new ConfigFile((string)getcwd(), $this->file_path); $config_file->addPlugin('a\b\c'); + $file_contents = file_get_contents($this->file_path); + assert($file_contents !== false); $this->assertTrue(static::compareContentWithTemplateAndTrailingLineEnding( ' @@ -73,7 +78,7 @@ public function addCanAddPluginClassToExistingPluginsNode(): void > ', - file_get_contents($this->file_path), + $file_contents, )); } @@ -90,11 +95,13 @@ public function addCanCreateMissingPluginsNode(): void $config_file = new ConfigFile((string)getcwd(), $this->file_path); $config_file->addPlugin('a\b\c'); + $file_contents = file_get_contents($this->file_path); + assert($file_contents !== false); $this->assertTrue(static::compareContentWithTemplateAndTrailingLineEnding( ' ', - file_get_contents($this->file_path), + $file_contents, )); } @@ -110,10 +117,12 @@ public function removeDoesNothingWhenThereIsNoPluginsNode(): void $config_file = new ConfigFile((string)getcwd(), $this->file_path); $config_file->removePlugin('a\b\c'); + $file_contents = file_get_contents($this->file_path); + assert($file_contents !== false); $this->assertSame( $noPlugins, - file_get_contents($this->file_path), + $file_contents, ); } @@ -134,10 +143,12 @@ public function removeKillsEmptyPluginsNode(): void $config_file = new ConfigFile((string)getcwd(), $this->file_path); $config_file->removePlugin('a\b\c'); + $file_contents = file_get_contents($this->file_path); + assert($file_contents !== false); $this->assertXmlStringEqualsXmlString( $noPlugins, - file_get_contents($this->file_path), + $file_contents, ); } @@ -160,10 +171,12 @@ public function removeKillsSpecifiedPlugin(): void $config_file = new ConfigFile((string)getcwd(), $this->file_path); $config_file->removePlugin('a\b\c'); + $file_contents = file_get_contents($this->file_path); + assert($file_contents !== false); $this->assertXmlStringEqualsXmlString( $noPlugins, - file_get_contents($this->file_path), + $file_contents, ); } @@ -195,10 +208,12 @@ public function removeKillsSpecifiedPluginWithOneRemaining(): void $config_file = new ConfigFile((string)getcwd(), $this->file_path); $config_file->removePlugin('a\b\c'); + $file_contents = file_get_contents($this->file_path); + assert($file_contents !== false); $this->assertXmlStringEqualsXmlString( $noPlugins, - file_get_contents($this->file_path), + $file_contents, ); } diff --git a/tests/Config/ConfigTest.php b/tests/Config/ConfigTest.php index d62990a294e..c664bd71ac3 100644 --- a/tests/Config/ConfigTest.php +++ b/tests/Config/ConfigTest.php @@ -4,6 +4,7 @@ use Composer\Autoload\ClassLoader; use ErrorException; +use Psalm\CodeLocation\Raw; use Psalm\Config; use Psalm\Config\IssueHandler; use Psalm\Context; @@ -16,6 +17,8 @@ use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; use Psalm\Internal\Scanner\FileScanner; +use Psalm\Issue\TooManyArguments; +use Psalm\Issue\UndefinedFunction; use Psalm\Tests\Config\Plugin\FileTypeSelfRegisteringPlugin; use Psalm\Tests\Internal\Provider\FakeParserCacheProvider; use Psalm\Tests\TestCase; @@ -102,8 +105,8 @@ public function testBarebonesConfig(): void $config = $this->project_analyzer->getConfig(); - $this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php'))); - $this->assertFalse($config->isInProjectDirs(realpath('examples/TemplateScanner.php'))); + $this->assertTrue($config->isInProjectDirs((string) realpath('src/Psalm/Type.php'))); + $this->assertFalse($config->isInProjectDirs((string) realpath('examples/TemplateScanner.php'))); } public function testIgnoreProjectDirectory(): void @@ -125,9 +128,9 @@ public function testIgnoreProjectDirectory(): void $config = $this->project_analyzer->getConfig(); - $this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php'))); - $this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'))); - $this->assertFalse($config->isInProjectDirs(realpath('examples/TemplateScanner.php'))); + $this->assertTrue($config->isInProjectDirs((string) realpath('src/Psalm/Type.php'))); + $this->assertFalse($config->isInProjectDirs((string) realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'))); + $this->assertFalse($config->isInProjectDirs((string) realpath('examples/TemplateScanner.php'))); } public function testIgnoreMissingProjectDirectory(): void @@ -149,9 +152,9 @@ public function testIgnoreMissingProjectDirectory(): void $config = $this->project_analyzer->getConfig(); - $this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php'))); - $this->assertFalse($config->isInProjectDirs(realpath(__DIR__ . '/../../') . '/does/not/exist/FileAnalyzer.php')); - $this->assertFalse($config->isInProjectDirs(realpath('examples/TemplateScanner.php'))); + $this->assertTrue($config->isInProjectDirs((string) realpath('src/Psalm/Type.php'))); + $this->assertFalse($config->isInProjectDirs((string) realpath(__DIR__ . '/../../') . '/does/not/exist/FileAnalyzer.php')); + $this->assertFalse($config->isInProjectDirs((string) realpath('examples/TemplateScanner.php'))); } public function testIgnoreSymlinkedProjectDirectory(): void @@ -195,9 +198,9 @@ public function testIgnoreSymlinkedProjectDirectory(): void $config = $this->project_analyzer->getConfig(); - $this->assertTrue($config->isInProjectDirs(realpath('tests/AnnotationTest.php'))); - $this->assertFalse($config->isInProjectDirs(realpath('tests/fixtures/symlinktest/a/ignoreme.php'))); - $this->assertFalse($config->isInProjectDirs(realpath('examples/TemplateScanner.php'))); + $this->assertTrue($config->isInProjectDirs((string) realpath('tests/AnnotationTest.php'))); + $this->assertFalse($config->isInProjectDirs((string) realpath('tests/fixtures/symlinktest/a/ignoreme.php'))); + $this->assertFalse($config->isInProjectDirs((string) realpath('examples/TemplateScanner.php'))); $regex = '/^unlink\([^\)]+\): (?:Permission denied|No such file or directory)$/'; $last_error = error_get_last(); @@ -242,10 +245,10 @@ public function testIgnoreWildcardProjectDirectory(): void $config = $this->project_analyzer->getConfig(); - $this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php'))); - $this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'))); - $this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php'))); - $this->assertFalse($config->isInProjectDirs(realpath('examples/TemplateScanner.php'))); + $this->assertTrue($config->isInProjectDirs((string) realpath('src/Psalm/Type.php'))); + $this->assertFalse($config->isInProjectDirs((string) realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'))); + $this->assertFalse($config->isInProjectDirs((string) realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php'))); + $this->assertFalse($config->isInProjectDirs((string) realpath('examples/TemplateScanner.php'))); } public function testIgnoreRecursiveWildcardProjectDirectory(): void @@ -267,9 +270,9 @@ public function testIgnoreRecursiveWildcardProjectDirectory(): void $config = $this->project_analyzer->getConfig(); - $this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php'))); - $this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php'))); - $this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Node/Expr/BinaryOp/VirtualPlus.php'))); + $this->assertTrue($config->isInProjectDirs((string) realpath('src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php'))); + $this->assertFalse($config->isInProjectDirs((string) realpath('src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php'))); + $this->assertFalse($config->isInProjectDirs((string) realpath('src/Psalm/Node/Expr/BinaryOp/VirtualPlus.php'))); } public function testIgnoreRecursiveDoubleWildcardProjectFiles(): void @@ -291,9 +294,9 @@ public function testIgnoreRecursiveDoubleWildcardProjectFiles(): void $config = $this->project_analyzer->getConfig(); - $this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php'))); - $this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'))); - $this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php'))); + $this->assertTrue($config->isInProjectDirs((string) realpath('src/Psalm/Type.php'))); + $this->assertFalse($config->isInProjectDirs((string) realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'))); + $this->assertFalse($config->isInProjectDirs((string) realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php'))); } public function testIgnoreWildcardFiles(): void @@ -315,10 +318,10 @@ public function testIgnoreWildcardFiles(): void $config = $this->project_analyzer->getConfig(); - $this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php'))); - $this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'))); - $this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php'))); - $this->assertFalse($config->isInProjectDirs(realpath('examples/TemplateScanner.php'))); + $this->assertTrue($config->isInProjectDirs((string) realpath('src/Psalm/Type.php'))); + $this->assertFalse($config->isInProjectDirs((string) realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'))); + $this->assertTrue($config->isInProjectDirs((string) realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php'))); + $this->assertFalse($config->isInProjectDirs((string) realpath('examples/TemplateScanner.php'))); } public function testIgnoreWildcardFilesInWildcardFolder(): void @@ -342,11 +345,11 @@ public function testIgnoreWildcardFilesInWildcardFolder(): void $config = $this->project_analyzer->getConfig(); - $this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php'))); - $this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php'))); - $this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'))); - $this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php'))); - $this->assertTrue($config->isInProjectDirs(realpath('examples/plugins/StringChecker.php'))); + $this->assertTrue($config->isInProjectDirs((string) realpath('src/Psalm/Type.php'))); + $this->assertTrue($config->isInProjectDirs((string) realpath('src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php'))); + $this->assertFalse($config->isInProjectDirs((string) realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'))); + $this->assertFalse($config->isInProjectDirs((string) realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php'))); + $this->assertTrue($config->isInProjectDirs((string) realpath('examples/plugins/StringChecker.php'))); } public function testIgnoreWildcardFilesInAllPossibleWildcardFolders(): void @@ -370,10 +373,10 @@ public function testIgnoreWildcardFilesInAllPossibleWildcardFolders(): void $config = $this->project_analyzer->getConfig(); - $this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Type.php'))); - $this->assertTrue($config->isInProjectDirs(realpath('src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php'))); - $this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'))); - $this->assertFalse($config->isInProjectDirs(realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php'))); + $this->assertTrue($config->isInProjectDirs((string) realpath('src/Psalm/Type.php'))); + $this->assertTrue($config->isInProjectDirs((string) realpath('src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php'))); + $this->assertFalse($config->isInProjectDirs((string) realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'))); + $this->assertFalse($config->isInProjectDirs((string) realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php'))); } public function testIssueHandler(): void @@ -397,8 +400,8 @@ public function testIssueHandler(): void $config = $this->project_analyzer->getConfig(); - $this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath(__FILE__))); - $this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath('src/Psalm/Type.php'))); + $this->assertFalse($config->reportIssueInFile('MissingReturnType', (string) realpath(__FILE__))); + $this->assertFalse($config->reportIssueInFile('MissingReturnType', (string) realpath('src/Psalm/Type.php'))); } public function testReportMixedIssues(): void @@ -418,7 +421,7 @@ public function testReportMixedIssues(): void $config = $this->project_analyzer->getConfig(); $this->assertNull($config->show_mixed_issues); - $this->assertTrue($config->reportIssueInFile('MixedArgument', realpath(__FILE__))); + $this->assertTrue($config->reportIssueInFile('MixedArgument', (string) realpath(__FILE__))); $this->project_analyzer = $this->getProjectAnalyzerWithConfig( Config::loadFromXML( @@ -435,7 +438,7 @@ public function testReportMixedIssues(): void $config = $this->project_analyzer->getConfig(); $this->assertFalse($config->show_mixed_issues); - $this->assertFalse($config->reportIssueInFile('MixedArgument', realpath(__FILE__))); + $this->assertFalse($config->reportIssueInFile('MixedArgument', (string) realpath(__FILE__))); $this->project_analyzer = $this->getProjectAnalyzerWithConfig( Config::loadFromXML( @@ -452,7 +455,7 @@ public function testReportMixedIssues(): void $config = $this->project_analyzer->getConfig(); $this->assertNull($config->show_mixed_issues); - $this->assertFalse($config->reportIssueInFile('MixedArgument', realpath(__FILE__))); + $this->assertFalse($config->reportIssueInFile('MixedArgument', (string) realpath(__FILE__))); $this->project_analyzer = $this->getProjectAnalyzerWithConfig( Config::loadFromXML( @@ -469,7 +472,7 @@ public function testReportMixedIssues(): void $config = $this->project_analyzer->getConfig(); $this->assertTrue($config->show_mixed_issues); - $this->assertTrue($config->reportIssueInFile('MixedArgument', realpath(__FILE__))); + $this->assertTrue($config->reportIssueInFile('MixedArgument', (string) realpath(__FILE__))); } public function testGlobalUndefinedFunctionSuppression(): void @@ -526,8 +529,8 @@ public function testMultipleIssueHandlers(): void $config = $this->project_analyzer->getConfig(); - $this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath(__FILE__))); - $this->assertFalse($config->reportIssueInFile('UndefinedClass', realpath(__FILE__))); + $this->assertFalse($config->reportIssueInFile('MissingReturnType', (string) realpath(__FILE__))); + $this->assertFalse($config->reportIssueInFile('UndefinedClass', (string) realpath(__FILE__))); } public function testIssueHandlerWithCustomErrorLevels(): void @@ -606,7 +609,7 @@ public function testIssueHandlerWithCustomErrorLevels(): void 'info', $config->getReportingLevelForFile( 'MissingReturnType', - realpath('src/Psalm/Type.php'), + (string) realpath('src/Psalm/Type.php'), ), ); @@ -614,7 +617,7 @@ public function testIssueHandlerWithCustomErrorLevels(): void 'error', $config->getReportingLevelForFile( 'MissingReturnType', - realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'), + (string) realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'), ), ); @@ -622,7 +625,7 @@ public function testIssueHandlerWithCustomErrorLevels(): void 'error', $config->getReportingLevelForFile( 'PossiblyInvalidArgument', - realpath('src/psalm.php'), + (string) realpath('src/psalm.php'), ), ); @@ -630,7 +633,7 @@ public function testIssueHandlerWithCustomErrorLevels(): void 'info', $config->getReportingLevelForFile( 'PossiblyInvalidArgument', - realpath('examples/TemplateChecker.php'), + (string) realpath('examples/TemplateChecker.php'), ), ); @@ -839,7 +842,7 @@ public function testIssueHandlerSetDynamically(): void 'info', $config->getReportingLevelForFile( 'MissingReturnType', - realpath('src/Psalm/Type.php'), + (string) realpath('src/Psalm/Type.php'), ), ); @@ -847,7 +850,7 @@ public function testIssueHandlerSetDynamically(): void 'error', $config->getReportingLevelForFile( 'MissingReturnType', - realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'), + (string) realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'), ), ); @@ -855,7 +858,7 @@ public function testIssueHandlerSetDynamically(): void 'error', $config->getReportingLevelForFile( 'PossiblyInvalidArgument', - realpath('src/psalm.php'), + (string) realpath('src/psalm.php'), ), ); @@ -863,7 +866,7 @@ public function testIssueHandlerSetDynamically(): void 'info', $config->getReportingLevelForFile( 'PossiblyInvalidArgument', - realpath('examples/TemplateChecker.php'), + (string) realpath('examples/TemplateChecker.php'), ), ); @@ -976,6 +979,124 @@ public function testIssueHandlerSetDynamically(): void ); } + public function testIssueHandlerOverride(): void + { + $this->project_analyzer = $this->getProjectAnalyzerWithConfig( + Config::loadFromXML( + dirname(__DIR__, 2), + ' + + + + + + + + + + + + + + + + + ', + ), + ); + + $config = $this->project_analyzer->getConfig(); + $config->setAdvancedErrorLevel('MissingReturnType', [ + [ + 'type' => 'error', + 'directory' => [['name' => 'src/Psalm/Internal/Analyzer']], + ], + ], 'info'); + $config->setCustomErrorLevel('UndefinedClass', 'suppress'); + + $this->assertSame( + 'info', + $config->getReportingLevelForFile( + 'MissingReturnType', + (string) realpath('src/Psalm/Type.php'), + ), + ); + + $this->assertSame( + 'error', + $config->getReportingLevelForFile( + 'MissingReturnType', + (string) realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'), + ), + ); + $this->assertSame( + 'suppress', + $config->getReportingLevelForFile( + 'UndefinedClass', + (string) realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'), + ), + ); + } + + public function testIssueHandlerSafeOverride(): void + { + $this->project_analyzer = $this->getProjectAnalyzerWithConfig( + Config::loadFromXML( + dirname(__DIR__, 2), + ' + + + + + + + + + + + + + + + + + ', + ), + ); + + $config = $this->project_analyzer->getConfig(); + $config->safeSetAdvancedErrorLevel('MissingReturnType', [ + [ + 'type' => 'error', + 'directory' => [['name' => 'src/Psalm/Internal/Analyzer']], + ], + ], 'info'); + $config->safeSetCustomErrorLevel('UndefinedClass', 'suppress'); + + $this->assertSame( + 'error', + $config->getReportingLevelForFile( + 'MissingReturnType', + (string) realpath('src/Psalm/Type.php'), + ), + ); + + $this->assertSame( + 'info', + $config->getReportingLevelForFile( + 'MissingReturnType', + (string) realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'), + ), + ); + $this->assertSame( + 'info', + $config->getReportingLevelForFile( + 'UndefinedClass', + (string) realpath('src/Psalm/Internal/Analyzer/FileAnalyzer.php'), + ), + ); + } + public function testAllPossibleIssues(): void { $all_possible_handlers = implode( @@ -1045,7 +1166,7 @@ public function testThing(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -1080,7 +1201,7 @@ public function testValidThrowInvalidCatch(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -1128,7 +1249,7 @@ public function testInvalidThrowValidCatch(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -1179,7 +1300,7 @@ public function testValidThrowValidCatch(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -1252,7 +1373,7 @@ public function testGlobals(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -1350,7 +1471,7 @@ public function testIgnoreExceptions(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -1416,7 +1537,7 @@ public function testNotIgnoredException(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -1633,14 +1754,14 @@ public function testTypeStatsForFileReporting(): void $config = $this->project_analyzer->getConfig(); - $this->assertFalse($config->reportTypeStatsForFile(realpath('src/Psalm/Config') . DIRECTORY_SEPARATOR)); - $this->assertTrue($config->reportTypeStatsForFile(realpath('src/Psalm/Internal') . DIRECTORY_SEPARATOR)); - $this->assertTrue($config->reportTypeStatsForFile(realpath('src/Psalm/Issue') . DIRECTORY_SEPARATOR)); - $this->assertTrue($config->reportTypeStatsForFile(realpath('src/Psalm/Node') . DIRECTORY_SEPARATOR)); - $this->assertTrue($config->reportTypeStatsForFile(realpath('src/Psalm/Plugin') . DIRECTORY_SEPARATOR)); - $this->assertTrue($config->reportTypeStatsForFile(realpath('src/Psalm/Progress') . DIRECTORY_SEPARATOR)); - $this->assertTrue($config->reportTypeStatsForFile(realpath('src/Psalm/Report') . DIRECTORY_SEPARATOR)); - $this->assertTrue($config->reportTypeStatsForFile(realpath('src/Psalm/SourceControl') . DIRECTORY_SEPARATOR)); + $this->assertFalse($config->reportTypeStatsForFile((string) realpath('src/Psalm/Config') . DIRECTORY_SEPARATOR)); + $this->assertTrue($config->reportTypeStatsForFile((string) realpath('src/Psalm/Internal') . DIRECTORY_SEPARATOR)); + $this->assertTrue($config->reportTypeStatsForFile((string) realpath('src/Psalm/Issue') . DIRECTORY_SEPARATOR)); + $this->assertTrue($config->reportTypeStatsForFile((string) realpath('src/Psalm/Node') . DIRECTORY_SEPARATOR)); + $this->assertTrue($config->reportTypeStatsForFile((string) realpath('src/Psalm/Plugin') . DIRECTORY_SEPARATOR)); + $this->assertTrue($config->reportTypeStatsForFile((string) realpath('src/Psalm/Progress') . DIRECTORY_SEPARATOR)); + $this->assertTrue($config->reportTypeStatsForFile((string) realpath('src/Psalm/Report') . DIRECTORY_SEPARATOR)); + $this->assertTrue($config->reportTypeStatsForFile((string) realpath('src/Psalm/SourceControl') . DIRECTORY_SEPARATOR)); } public function testStrictTypesForFileReporting(): void @@ -1666,14 +1787,14 @@ public function testStrictTypesForFileReporting(): void $config = $this->project_analyzer->getConfig(); - $this->assertTrue($config->useStrictTypesForFile(realpath('src/Psalm/Config') . DIRECTORY_SEPARATOR)); - $this->assertFalse($config->useStrictTypesForFile(realpath('src/Psalm/Internal') . DIRECTORY_SEPARATOR)); - $this->assertFalse($config->useStrictTypesForFile(realpath('src/Psalm/Issue') . DIRECTORY_SEPARATOR)); - $this->assertFalse($config->useStrictTypesForFile(realpath('src/Psalm/Node') . DIRECTORY_SEPARATOR)); - $this->assertFalse($config->useStrictTypesForFile(realpath('src/Psalm/Plugin') . DIRECTORY_SEPARATOR)); - $this->assertFalse($config->useStrictTypesForFile(realpath('src/Psalm/Progress') . DIRECTORY_SEPARATOR)); - $this->assertFalse($config->useStrictTypesForFile(realpath('src/Psalm/Report') . DIRECTORY_SEPARATOR)); - $this->assertFalse($config->useStrictTypesForFile(realpath('src/Psalm/SourceControl') . DIRECTORY_SEPARATOR)); + $this->assertTrue($config->useStrictTypesForFile((string) realpath('src/Psalm/Config') . DIRECTORY_SEPARATOR)); + $this->assertFalse($config->useStrictTypesForFile((string) realpath('src/Psalm/Internal') . DIRECTORY_SEPARATOR)); + $this->assertFalse($config->useStrictTypesForFile((string) realpath('src/Psalm/Issue') . DIRECTORY_SEPARATOR)); + $this->assertFalse($config->useStrictTypesForFile((string) realpath('src/Psalm/Node') . DIRECTORY_SEPARATOR)); + $this->assertFalse($config->useStrictTypesForFile((string) realpath('src/Psalm/Plugin') . DIRECTORY_SEPARATOR)); + $this->assertFalse($config->useStrictTypesForFile((string) realpath('src/Psalm/Progress') . DIRECTORY_SEPARATOR)); + $this->assertFalse($config->useStrictTypesForFile((string) realpath('src/Psalm/Report') . DIRECTORY_SEPARATOR)); + $this->assertFalse($config->useStrictTypesForFile((string) realpath('src/Psalm/SourceControl') . DIRECTORY_SEPARATOR)); } public function testConfigFileWithXIncludeWithoutFallbackShouldThrowException(): void @@ -1727,7 +1848,7 @@ public function testConfigFileWithXIncludeWithFallback(): void $config = $this->project_analyzer->getConfig(); - $this->assertFalse($config->reportIssueInFile('MixedAssignment', realpath('src/Psalm/Type.php'))); + $this->assertFalse($config->reportIssueInFile('MixedAssignment', (string) realpath('src/Psalm/Type.php'))); } public function testConfigFileWithWildcardPathIssueHandler(): void @@ -1756,14 +1877,14 @@ public function testConfigFileWithWildcardPathIssueHandler(): void $config = $this->project_analyzer->getConfig(); - $this->assertTrue($config->reportIssueInFile('MissingReturnType', realpath(__FILE__))); - $this->assertTrue($config->reportIssueInFile('MissingReturnType', realpath('src/Psalm/Type.php'))); - $this->assertTrue($config->reportIssueInFile('MissingReturnType', realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php'))); + $this->assertTrue($config->reportIssueInFile('MissingReturnType', (string) realpath(__FILE__))); + $this->assertTrue($config->reportIssueInFile('MissingReturnType', (string) realpath('src/Psalm/Type.php'))); + $this->assertTrue($config->reportIssueInFile('MissingReturnType', (string) realpath('src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php'))); - $this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath('src/Psalm/Node/Expr/BinaryOp/VirtualPlus.php'))); - $this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath('src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php'))); - $this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath('src/Psalm/Internal/Type/TypeAlias.php'))); - $this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath('src/Psalm/Internal/Type/TypeAlias/ClassTypeAlias.php'))); + $this->assertFalse($config->reportIssueInFile('MissingReturnType', (string) realpath('src/Psalm/Node/Expr/BinaryOp/VirtualPlus.php'))); + $this->assertFalse($config->reportIssueInFile('MissingReturnType', (string) realpath('src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php'))); + $this->assertFalse($config->reportIssueInFile('MissingReturnType', (string) realpath('src/Psalm/Internal/Type/TypeAlias.php'))); + $this->assertFalse($config->reportIssueInFile('MissingReturnType', (string) realpath('src/Psalm/Internal/Type/TypeAlias/ClassTypeAlias.php'))); } /** @@ -1787,7 +1908,7 @@ public function testConfigWarnsAboutDeprecatedWayToLoadStubsButLoadsTheStub(): v $config->visitStubFiles($codebase); - $this->assertContains(realpath('stubs/extensions/apcu.phpstub'), $config->internal_stubs); + $this->assertContains((string) realpath('stubs/extensions/apcu.phpstub'), $config->internal_stubs); $this->assertContains( 'Psalm 6 will not automatically load stubs for ext-apcu. You should explicitly enable or disable this ext in composer.json or Psalm config.', $config->config_warnings, @@ -1818,10 +1939,70 @@ public function testConfigWithDisableExtensionsDoesNotLoadExtensionStubsAndHides $config->visitStubFiles($codebase); - $this->assertNotContains(realpath('stubs/extensions/apcu.phpstub'), $config->internal_stubs); + $this->assertNotContains((string) realpath('stubs/extensions/apcu.phpstub'), $config->internal_stubs); $this->assertNotContains( 'Psalm 6 will not automatically load stubs for ext-apcu. You should explicitly enable or disable this ext in composer.json or Psalm config.', $config->internal_stubs, ); } + + public function testReferencedFunctionAllowsMethods(): void + { + $config_xml = Config::loadFromXML( + (string) getcwd(), + << + + + + + + + + + + XML, + ); + + $this->assertSame( + Config::REPORT_SUPPRESS, + $config_xml->getReportingLevelForIssue( + new TooManyArguments( + 'too many', + new Raw('aaa', 'aaa.php', 'aaa.php', 1, 2), + 'Foo\Bar::baZ', + ), + ), + ); + } + + public function testReferencedFunctionAllowsNamespacedFunctions(): void + { + $config_xml = Config::loadFromXML( + (string) getcwd(), + << + + + + + + + + + + XML, + ); + + $this->assertSame( + Config::REPORT_SUPPRESS, + $config_xml->getReportingLevelForIssue( + new UndefinedFunction( + 'Function Foo\Bar\baz does not exist', + new Raw('aaa', 'aaa.php', 'aaa.php', 1, 2), + 'foo\bar\baz', + ), + ), + ); + } } diff --git a/tests/Config/PluginTest.php b/tests/Config/PluginTest.php index 130fd2d3304..57d759e67c0 100644 --- a/tests/Config/PluginTest.php +++ b/tests/Config/PluginTest.php @@ -96,7 +96,7 @@ public function testStringAnalyzerPlugin(): void $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -130,7 +130,7 @@ public function testStringAnalyzerPluginWithClassConstant(): void $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -168,7 +168,7 @@ public function testStringAnalyzerPluginWithClassConstantConcat(): void $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -206,7 +206,7 @@ public function testEchoAnalyzerPluginWithJustHtml(): void $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -244,7 +244,7 @@ public function testEchoAnalyzerPluginWithUnescapedConcatenatedString(): void $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -281,7 +281,7 @@ public function testEchoAnalyzerPluginWithUnescapedString(): void $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -319,7 +319,7 @@ public function testFileAnalyzerPlugin(): void $this->assertCount(1, $codebase->config->eventDispatcher->before_file_checks); $this->assertCount(1, $codebase->config->eventDispatcher->after_file_checks); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -362,7 +362,7 @@ public function testFloatCheckerPlugin(): void $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -400,7 +400,7 @@ public function testFloatCheckerPluginIssueSuppressionByConfig(): void $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -433,7 +433,7 @@ public function testFloatCheckerPluginIssueSuppressionByDocblock(): void $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -535,7 +535,7 @@ public function testPropertyProviderHooks(): void $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -574,7 +574,7 @@ public function testMethodProviderHooksValidArg(): void $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -635,7 +635,7 @@ public function testFunctionProviderHooks(): void $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -671,7 +671,7 @@ public function testPropertyProviderHooksInvalidAssignment(): void $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -712,7 +712,7 @@ public function testMethodProviderHooksInvalidArg(): void $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -755,7 +755,7 @@ public function testFunctionProviderHooksInvalidArg(): void $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -888,7 +888,7 @@ public static function afterEveryFunctionCallAnalysis(AfterEveryFunctionCallAnal $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $this->project_analyzer->getCodebase()->config->eventDispatcher->after_every_function_checks[] = get_class($plugin); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -928,7 +928,7 @@ public function testRemoveTaints(): void $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -1000,7 +1000,7 @@ public function testFunctionDynamicStorageProviderHook(): void $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, diff --git a/tests/ConstantTest.php b/tests/ConstantTest.php index 1bb5fa1c3d4..785dbf4f0cd 100644 --- a/tests/ConstantTest.php +++ b/tests/ConstantTest.php @@ -23,7 +23,7 @@ class ConstantTest extends TestCase // $this->testConfig->ensure_array_int_offsets_exist = true; - // $file_path = getcwd() . '/src/somefile.php'; + // $file_path = (string) getcwd() . '/src/somefile.php'; // $this->addFile( // $file_path, @@ -49,8 +49,8 @@ class ConstantTest extends TestCase public function testUseObjectConstant(): void { - $file1 = getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'file1.php'; - $file2 = getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'file2.php'; + $file1 = (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'file1.php'; + $file2 = (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'file2.php'; $this->addFile( $file1, diff --git a/tests/DocumentationTest.php b/tests/DocumentationTest.php index 1e0eb7852f9..fe3b54c38e5 100644 --- a/tests/DocumentationTest.php +++ b/tests/DocumentationTest.php @@ -24,6 +24,7 @@ use function array_keys; use function array_map; use function array_shift; +use function assert; use function count; use function dirname; use function explode; @@ -100,9 +101,12 @@ private static function getCodeBlocksFromDocs(): array } $issue_code = []; + $files = glob($issues_dir . '/*.md'); + assert($files !== false); - foreach (glob($issues_dir . '/*.md') as $file_path) { + foreach ($files as $file_path) { $file_contents = file_get_contents($file_path); + assert($file_contents !== false); $file_lines = explode("\n", $file_contents); @@ -357,7 +361,9 @@ public function testAllAnnotationsAreDocumented(string $annotation): void { if ('' === self::$docContents) { foreach (self::ANNOTATION_DOCS as $file) { - self::$docContents .= file_get_contents(__DIR__ . '/../' . $file); + $file_contents = file_get_contents(__DIR__ . '/../' . $file); + assert($file_contents !== false); + self::$docContents .= $file_contents; } } @@ -445,13 +451,15 @@ public function testIssuesIndex(): void return $matches[1]; }, $issues_index_contents); + $dir_contents = scandir($issues_dir); + assert($dir_contents !== false); $issue_files = array_filter(array_map(function (string $issue_file) { if ($issue_file === "." || $issue_file === "..") { return false; } $this->assertStringEndsWith(".md", $issue_file, "Invalid file in issues documentation: $issue_file"); return substr($issue_file, 0, strlen($issue_file) - 3); - }, scandir($issues_dir))); + }, $dir_contents)); $unlisted_issues = array_diff($issue_files, $issues_index_list); $this->assertEmpty($unlisted_issues, "Issue documentation missing from issues.md: " . implode(", ", $unlisted_issues)); diff --git a/tests/EndToEnd/PsalmEndToEndTest.php b/tests/EndToEnd/PsalmEndToEndTest.php index b02660cecd2..2a9cff2bf0d 100644 --- a/tests/EndToEnd/PsalmEndToEndTest.php +++ b/tests/EndToEnd/PsalmEndToEndTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Process\Process; +use function assert; use function closedir; use function copy; use function file_exists; @@ -39,6 +40,7 @@ class PsalmEndToEndTest extends TestCase public static function setUpBeforeClass(): void { self::$tmpDir = tempnam(sys_get_temp_dir(), 'PsalmEndToEndTest_'); + assert(self::$tmpDir !== false); unlink(self::$tmpDir); mkdir(self::$tmpDir); @@ -215,8 +217,9 @@ public function testLegacyConfigWithoutresolveFromConfigFile(): void { $this->runPsalmInit(1); $psalmXmlContent = file_get_contents(self::$tmpDir . '/psalm.xml'); + assert($psalmXmlContent !== false); $count = 0; - $psalmXmlContent = preg_replace('/resolveFromConfigFile="true"/', 'resolveFromConfigFile="false"', $psalmXmlContent, -1, $count); + $psalmXmlContent = (string) preg_replace('/resolveFromConfigFile="true"/', 'resolveFromConfigFile="false"', $psalmXmlContent, -1, $count); $this->assertEquals(1, $count); file_put_contents(self::$tmpDir . '/src/psalm.xml', $psalmXmlContent); @@ -232,7 +235,8 @@ public function testPsalmWithNoProgressDoesNotProduceOutputOnStderr(): void $this->runPsalmInit(); $psalmXml = file_get_contents(self::$tmpDir . '/psalm.xml'); - $psalmXml = preg_replace('/findUnusedCode="(true|false)"/', '', $psalmXml, 1); + assert($psalmXml !== false); + $psalmXml = (string) preg_replace('/findUnusedCode="(true|false)"/', '', $psalmXml, 1); file_put_contents(self::$tmpDir . '/psalm.xml', $psalmXml); $result = $this->runPsalm(['--no-progress'], self::$tmpDir); @@ -255,6 +259,7 @@ private function runPsalmInit(?int $level = null, ?string $php_version = null): $ret = $this->runPsalm($args, self::$tmpDir, false, false); $psalm_config_contents = file_get_contents(self::$tmpDir . '/psalm.xml'); + assert($psalm_config_contents !== false); $psalm_config_contents = str_replace( 'errorLevel="1"', 'errorLevel="1" ' @@ -273,6 +278,7 @@ private function runPsalmInit(?int $level = null, ?string $php_version = null): private static function recursiveRemoveDirectory(string $src): void { $dir = opendir($src); + assert($dir !== false); while (false !== ($file = readdir($dir))) { if (($file !== '.') && ($file !== '..')) { $full = $src . '/' . $file; diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 481824c1089..83fd1cf7591 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -574,6 +574,67 @@ enum IntEnum: int { 'ignored_issues' => [], 'php_version' => '8.1', ], + 'nameTypeOnKnownCases' => [ + 'code' => <<<'PHP' + name; + PHP, + 'assertions' => [ + '$_name===' => "'BIKE'|'BOAT'|'CAR'", + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'nameTypeOnUnknownCases' => [ + 'code' => <<<'PHP' + name; + /** @psalm-check-type-exact $_name='BIKE'|'BOAT'|'CAR' */; + } + PHP, + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'classStringAsBackedEnumValue' => [ + 'code' => <<<'PHP' + value; + noop($foo); + noop(FooEnum::Foo->value); + PHP, + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } @@ -979,6 +1040,21 @@ function withA(WithState $_): void {} 'ignored_issues' => [], 'php_version' => '8.1', ], + 'backedEnumDoesNotPassNativeType' => [ + 'code' => ' 'InvalidArgument', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } } diff --git a/tests/FileUpdates/AnalyzedMethodTest.php b/tests/FileUpdates/AnalyzedMethodTest.php index 43a4eb8e87e..e5d7f84b924 100644 --- a/tests/FileUpdates/AnalyzedMethodTest.php +++ b/tests/FileUpdates/AnalyzedMethodTest.php @@ -113,7 +113,7 @@ public function providerTestValidUpdates(): array return [ 'basicRequire' => [ 'start_files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::foofoo' => 1, 'foo\a::barbar' => 1, ], - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => 1, 'foo\b::bar' => 1, 'foo\b::noreturntype' => 1, ], ], 'unaffected_analyzed_methods' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::barbar' => 1, ], - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => 1, 'foo\b::noreturntype' => 1, ], @@ -194,14 +194,14 @@ public function noReturnType() {} ], 'invalidateAfterPropertyChange' => [ 'start_files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => 1, 'foo\b::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => 1, ], ], ], 'invalidateAfterStaticPropertyChange' => [ 'start_files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => 1, 'foo\b::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => 1, ], ], ], 'invalidateAfterStaticFlipPropertyChange' => [ 'start_files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => 1, 'foo\b::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => 1, ], ], ], 'invalidateAfterConstantChange' => [ 'start_files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => 1, 'foo\b::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => 1, ], ], ], 'dontInvalidateTraitMethods' => [ 'start_files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ 'foo\a::barbar&foo\t::barbar' => 1, ], - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::foofoo' => 1, ], - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => 1, 'foo\b::bar' => 1, 'foo\b::noreturntype' => 1, ], ], 'unaffected_analyzed_methods' => [ - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ 'foo\a::barbar&foo\t::barbar' => 1, ], - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [], - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [], + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => 1, 'foo\b::noreturntype' => 1, ], @@ -504,7 +504,7 @@ public function barBar(): string { ], 'invalidateTraitMethodsWhenTraitRemoved' => [ 'start_files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' 'barBar(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' 'barBar(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ 'foo\a::barbar&foo\t::barbar' => 1, ], - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::foofoo' => 1, ], - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => 1, 'foo\b::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [], - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [], - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [], + (string) getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [], + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [], + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [], ], ], 'invalidateTraitMethodsWhenTraitReplaced' => [ 'start_files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' 'barBar(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' 'barBar(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ 'foo\a::barbar&foo\t::barbar' => 1, ], - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::foofoo' => 1, ], - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => 1, 'foo\b::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [], - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [], - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [], + (string) getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [], + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [], + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [], ], ], 'invalidateTraitMethodsWhenMethodChanged' => [ 'start_files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' 'barBar(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' 'barBar(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ 'foo\a::barbar&foo\t::barbar' => 1, 'foo\a::bat&foo\t::bat' => 1, ], - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::foofoo' => 1, ], - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => 1, 'foo\b::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ 'foo\a::bat&foo\t::bat' => 1, ], - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [], - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [], + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [], + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [], ], ], 'invalidateTraitMethodsWhenMethodSuperimposed' => [ 'start_files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' 'barBar(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' 'barBar(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ 'foo\a::barbar&foo\t::barbar' => 1, ], - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [], - getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [], + (string) getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [], + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [], ], ], 'dontInvalidateConstructor' => [ 'start_files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, 'foo\a::setfoo' => 1, 'foo\a::reallysetfoo' => 1, ], ], 'unaffected_analyzed_methods' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, 'foo\a::setfoo' => 1, 'foo\a::reallysetfoo' => 1, @@ -877,7 +877,7 @@ private function reallySetFoo() : void { ], 'invalidateConstructorWhenDependentMethodChanges' => [ 'start_files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, 'foo\a::setfoo' => 1, 'foo\a::reallysetfoo' => 1, ], ], 'unaffected_analyzed_methods' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::setfoo' => 1, ], ], ], 'invalidateConstructorWhenDependentMethodInSubclassChanges' => [ 'start_files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 1, 'foo\a::setfoo' => 1, ], - getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => [ 'foo\achild::setfoo' => 1, 'foo\achild::reallysetfoo' => 1, 'foo\achild::__construct' => 2, ], ], 'unaffected_analyzed_methods' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 1, 'foo\a::setfoo' => 1, ], - getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => [ 'foo\achild::setfoo' => 1, ], ], ], 'invalidateConstructorWhenDependentMethodInSubclassChanges2' => [ 'start_files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'foo = "bar"; } }', - getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'foo = "baz"; } }', - getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, 'foo\a::setfoo' => 1, ], - getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => [ 'foo\achild::__construct' => 2, ], ], 'unaffected_analyzed_methods' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [], - getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => [], + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [], + (string) getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => [], ], ], 'invalidateConstructorWhenDependentTraitMethodChanges' => [ 'start_files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'setFoo(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'setFoo(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ 'foo\a::setfoo&foo\t::setfoo' => 1, ], - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, ], ], 'unaffected_analyzed_methods' => [ - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [], - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [], + (string) getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [], + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [], ], ], 'rescanPropertyAssertingMethod' => [ 'start_files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, 'foo\a::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, ], ], @@ -1182,7 +1182,7 @@ public function bar() : void { ], 'noChangeAfterSyntaxError' => [ 'start_files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, 'foo\a::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, 'foo\a::bar' => 1, ], @@ -1224,7 +1224,7 @@ public function bar() : void { ], 'nothingBeforeSyntaxError' => [ 'start_files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, 'foo\a::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, 'foo\a::bar' => 1, ], @@ -1266,7 +1266,7 @@ public function bar() : void { ], 'modifyPropertyOfChildClass' => [ 'start_files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'b = $b; } }', - getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'b = $b; } }', - getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, ], - getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => [ 'foo\achild::__construct' => 2, ], ], 'unaffected_analyzed_methods' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, ], - getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => [], + (string) getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => [], ], ], ]; diff --git a/tests/FileUpdates/CachedStorageTest.php b/tests/FileUpdates/CachedStorageTest.php index 08936b6085b..3e6b97bff90 100644 --- a/tests/FileUpdates/CachedStorageTest.php +++ b/tests/FileUpdates/CachedStorageTest.php @@ -57,23 +57,23 @@ public function testValidInclude(): void $codebase = $this->project_analyzer->getCodebase(); $vendor_files = [ - getcwd() . DIRECTORY_SEPARATOR . 'V1.php' => ' ' ' ' ' ' ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' 'bar();', ], [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' 'foo();', ], [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' 'foo();', ], [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' 'existingMethod();', ], [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' 'newMethod();', ], [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' 'foo;', ], [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' 'foo;', ], [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' 'foo;', - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'T.php' => ' ' ' 'foo;', - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'T.php' => ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' 'foo;', - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'T.php' => ' ' ' 'foo;', - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'T.php' => ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' 'foo;', ], [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' 'foo;', ], [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' 'foo;', ], [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' 'foo();', ], [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' 'foo();', ], [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' 'foo("hello");', ], [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' 'bar();', ], [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' 'foo;', ], [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' 'bar();', ], [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' ' ' [ 'file_stages' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ 'files' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' [ 'files' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' [ 'files' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' 'bar(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'T.php' => ' ' ' 'bar(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'T.php' => ' ' ' 'bar(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'T.php' => ' ' [ 'files' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' 'bar(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'T.php' => ' ' ' 'bar(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'T.php' => ' ' ' 'bar(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'T.php' => ' ' [ 'files' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' [ 'files' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ 'files' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' 'hasMethod($method); }', ], [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'C.php' => ' ' ' 'hasMethod($method); @@ -416,14 +416,14 @@ function hasMethod(object $input, string $method): bool { 'missingConstructorForTwoVars' => [ 'files' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ 'files' => [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'bar(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' ' ' 'bar(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' ' ' 'bat(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' ' ' 'bat(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' ' ' 'bar(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'foo();', - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' ' ' 'foo();', - getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' ' ' ' ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' 'foo();', ], [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' 'foo();', @@ -1781,7 +1781,7 @@ public function bar() : void {} 'usedMethodWithNoAffectedConstantChanges' => [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' 'doFoo();', ], [ - getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' 'doFoo();', @@ -1840,7 +1840,7 @@ public function doFoo() : void { 'syntaxErrorFixed' => [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ [ [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' ' 'addFile( $file_path, @@ -107,7 +107,7 @@ public function testForbiddenCodeFunctionViaFunctions(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -128,7 +128,7 @@ public function testAllowedPrintFunction(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -155,7 +155,7 @@ public function testForbiddenPrintFunction(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -176,7 +176,7 @@ public function testAllowedVarExportFunction(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -204,7 +204,7 @@ public function testForbiddenVarExportFunction(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -231,7 +231,7 @@ public function testNoExceptionWithMatchingNameButDifferentNamespace(): void XML, ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, <<<'PHP' @@ -266,7 +266,7 @@ public function testForbiddenEmptyFunction(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -293,7 +293,7 @@ public function testForbiddenExitFunction(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -321,7 +321,7 @@ public function testForbiddenDieFunction(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -349,7 +349,7 @@ public function testForbiddenEvalExpression(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, diff --git a/tests/IncludeTest.php b/tests/IncludeTest.php index 9be667aa707..f9c80691267 100644 --- a/tests/IncludeTest.php +++ b/tests/IncludeTest.php @@ -107,7 +107,7 @@ public function providerTestValidIncludes(): array return [ 'basicRequire' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => ' 'fooFoo(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], ], 'requireSingleStringType' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => ' 'fooFoo(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], ], 'nestedRequire' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file3.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file3.php', ], ], 'requireNamespace' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], ], 'requireFunction' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], ], 'namespacedRequireFunction' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], ], 'requireConstant' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], ], 'requireNamespacedWithUse' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], ], 'noInfiniteRequireLoop' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php', - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', - getcwd() . DIRECTORY_SEPARATOR . 'file3.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file1.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file3.php', ], ], 'analyzeAllClasses' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php', - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file1.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], ], 'loopWithInterdependencies' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php', - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file1.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], ], 'variadicArgs' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file1.php', ], ], 'globalIncludedVar' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file1.php', ], ], 'returnNamespacedFunctionCallType' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file3.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file3.php', ], ], 'functionUsedElsewhere' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file1.php', ], ], 'closureInIncludedFile' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file1.php', ], ], 'hoistConstants' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php', - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file1.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], 'hoist_constants' => true, ], 'duplicateClasses' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' 'aa(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => ' 'dd(); } }', ], 'files_to_check' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php', - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file1.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], 'hoist_constants' => false, 'ignored_issues' => ['DuplicateClass'], ], 'duplicateClassesProperty' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php', - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file1.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], 'hoist_constants' => false, 'ignored_issues' => ['DuplicateClass', 'MissingPropertyType'], ], 'functionsDefined' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'index.php' => ' ' ' ' ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'index.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'index.php', ], ], 'suppressMissingFile' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file1.php', ], ], 'nestedParentFile' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'a' . DIRECTORY_SEPARATOR . 'b' . DIRECTORY_SEPARATOR . 'c' . DIRECTORY_SEPARATOR . 'd' . DIRECTORY_SEPARATOR . 'script.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'a' . DIRECTORY_SEPARATOR . 'b' . DIRECTORY_SEPARATOR . 'c' . DIRECTORY_SEPARATOR . 'd' . DIRECTORY_SEPARATOR . 'script.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'a' . DIRECTORY_SEPARATOR . 'b' . DIRECTORY_SEPARATOR . 'c' . DIRECTORY_SEPARATOR . 'd' . DIRECTORY_SEPARATOR . 'script.php', ], ], 'undefinedMethodAfterInvalidRequire' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], ], 'returnValue' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], ], 'noCrash' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'classes.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'user.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'user.php', ], ], 'pathStartingWithDot' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'test_1.php' => ' ' ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'test_1.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'test_1.php', ], ], ]; @@ -668,7 +668,7 @@ public function providerTestInvalidIncludes(): array return [ 'undefinedMethodInRequire' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => ' 'fooFo(); } }', - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], 'error_message' => 'UndefinedMethod', ], 'requireFunctionWithStrictTypes' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], 'error_message' => 'InvalidArgument', ], 'requireFunctionWithStrictTypesInClass' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], 'error_message' => 'InvalidArgument', ], 'requireFunctionWithWeakTypes' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], 'error_message' => 'InvalidScalarArgument', ], 'requireFunctionWithStrictTypesButDocblockType' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], 'error_message' => 'InvalidArgument', ], 'namespacedRequireFunction' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], 'error_message' => 'UndefinedFunction', ], 'globalIncludedIncorrectVar' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file1.php', ], 'error_message' => 'UndefinedVariable', ], 'invalidTraitFunctionReturnInUncheckedFile' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], 'error_message' => 'InvalidReturnType', ], 'invalidDoubleNestedTraitFunctionReturnInUncheckedFile' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file3.php' => ' ' ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file3.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file3.php', ], 'error_message' => 'InvalidReturnType', ], 'invalidTraitFunctionMissingNestedUse' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'A.php', - getcwd() . DIRECTORY_SEPARATOR . 'B.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'A.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'B.php', ], 'error_message' => 'UndefinedTrait - A.php:3:33', ], 'SKIPPED-noHoistConstants' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file1.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file1.php', ], 'error_message' => 'UndefinedConstant', ], 'undefinedMethodAfterInvalidRequire' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'file2.php', ], 'error_message' => 'UndefinedFunction', ], 'pathStartingWithDot' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'test_1.php' => ' ' ' ' [ - getcwd() . DIRECTORY_SEPARATOR . 'test_1.php', - getcwd() . DIRECTORY_SEPARATOR . 'a' . DIRECTORY_SEPARATOR . 'test_2.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'test_1.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'a' . DIRECTORY_SEPARATOR . 'test_2.php', ], 'error_message' => 'MissingFile', ], 'directoryPath' => [ 'files' => [ - getcwd() . DIRECTORY_SEPARATOR . 'test.php' => ' ' [getcwd() . DIRECTORY_SEPARATOR . 'test.php'], + 'files_to_check' => [(string) getcwd() . DIRECTORY_SEPARATOR . 'test.php'], 'error_message' => 'MissingFile', - 'directories' => [getcwd() . DIRECTORY_SEPARATOR], + 'directories' => [(string) getcwd() . DIRECTORY_SEPARATOR], ], ]; } diff --git a/tests/Internal/Analyzer/DeclareAnalyzerTest.php b/tests/Internal/Analyzer/DeclareAnalyzerTest.php new file mode 100644 index 00000000000..fd6fefc1e7a --- /dev/null +++ b/tests/Internal/Analyzer/DeclareAnalyzerTest.php @@ -0,0 +1,98 @@ + [ + 'code' => <<<'PHP' + [ + 'code' => <<<'PHP' + [ + 'code' => <<<'PHP' + [ + 'code' => <<<'PHP' + [ + 'code' => <<<'PHP' + [ + 'code' => <<<'PHP' + [ + 'code' => <<<'PHP' + 'UnrecognizedStatement', + ]; + + yield 'declareUnknownValueForStrictTypes' => [ + 'code' => <<<'PHP' + 'UnrecognizedStatement', + ]; + + yield 'declareStrictTypesBlockMode' => [ + 'code' => <<<'PHP' + 'UnrecognizedStatement', + ]; + + yield 'declareInvalidValueForTicks' => [ + 'code' => <<<'PHP' + 'UnrecognizedStatement', + ]; + + yield 'declareInvalidValueForEncoding' => [ + 'code' => <<<'PHP' + 'UnrecognizedStatement', + ]; + } +} diff --git a/tests/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzerTest.php b/tests/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzerTest.php index 250f52dae6a..e34a9ab87ea 100644 --- a/tests/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzerTest.php +++ b/tests/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzerTest.php @@ -5,11 +5,13 @@ namespace Psalm\Tests\Internal\Analyzer\Statements\Expression\Fetch; use Psalm\Tests\TestCase; +use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; final class AtomicPropertyFetchAnalyzerTest extends TestCase { use ValidCodeAnalysisTestTrait; + use InvalidCodeAnalysisTestTrait; public function providerValidCodeParse(): iterable { @@ -72,4 +74,21 @@ class C extends B {} ], ]; } + + public function providerInvalidCodeParse(): iterable + { + return [ + 'undefinedPropertyAccessOnMissingDependency' => [ + 'code' => <<<'PHP' + prop; + PHP, + 'error_message' => 'UndefinedPropertyFetch', + 'ignored_issues' => ['MissingDependency'], + ], + ]; + } } diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index c92f7924238..019c2484c0d 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -350,7 +350,7 @@ public function callMapEntryProvider(): iterable continue; } - yield "$function: " . json_encode($entry) => [$function, $entry]; + yield "$function: " . (string) json_encode($entry) => [$function, $entry]; } } diff --git a/tests/IssueBufferTest.php b/tests/IssueBufferTest.php index d167cf59317..620052ba130 100644 --- a/tests/IssueBufferTest.php +++ b/tests/IssueBufferTest.php @@ -126,7 +126,7 @@ public function testFinishDoesNotCorruptInternalState(): void ob_start(); IssueBuffer::finish($projectAnalzyer, false, microtime(true), false, $baseline); - $output = ob_get_clean(); + $output = (string) ob_get_clean(); $this->assertStringNotContainsString("ERROR", $output, "all issues baselined"); IssueBuffer::clear(); } @@ -137,7 +137,7 @@ public function testPrintSuccessMessageWorks(): void $project_analyzer->stdout_report_options = new ReportOptions; ob_start(); IssueBuffer::printSuccessMessage($project_analyzer); - $output = ob_get_clean(); + $output = (string) ob_get_clean(); $this->assertStringContainsString('No errors found!', $output); } diff --git a/tests/IssueSuppressionTest.php b/tests/IssueSuppressionTest.php index beeb66bfe80..3fa40ae3d93 100644 --- a/tests/IssueSuppressionTest.php +++ b/tests/IssueSuppressionTest.php @@ -30,7 +30,7 @@ public function testIssueSuppressedOnFunction(): void $this->expectExceptionMessage('UnusedPsalmSuppress'); $this->addFile( - getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', 'analyzeFile(getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', new Context()); + $this->analyzeFile((string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', new Context()); } public function testIssueSuppressedOnStatement(): void @@ -54,13 +54,13 @@ public function testIssueSuppressedOnStatement(): void $this->expectExceptionMessage('UnusedPsalmSuppress'); $this->addFile( - getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', 'analyzeFile(getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', new Context()); + $this->analyzeFile((string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', new Context()); } public function testUnusedSuppressAllOnFunction(): void @@ -70,7 +70,7 @@ public function testUnusedSuppressAllOnFunction(): void $this->addFile( - getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', 'analyzeFile(getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', new Context()); + $this->analyzeFile((string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', new Context()); } public function testUnusedSuppressAllOnStatement(): void @@ -87,12 +87,12 @@ public function testUnusedSuppressAllOnStatement(): void $this->expectExceptionMessage('UnusedPsalmSuppress'); $this->addFile( - getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', 'analyzeFile(getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', new Context()); + $this->analyzeFile((string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', new Context()); } public function testMissingThrowsDocblockSuppressed(): void @@ -100,7 +100,7 @@ public function testMissingThrowsDocblockSuppressed(): void Config::getInstance()->check_for_throws_docblock = true; $this->addFile( - getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', 'analyzeFile(getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', $context); + $this->analyzeFile((string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', $context); } public function testMissingThrowsDocblockSuppressedWithoutThrow(): void @@ -127,7 +127,7 @@ public function testMissingThrowsDocblockSuppressedWithoutThrow(): void Config::getInstance()->check_for_throws_docblock = true; $this->addFile( - getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', 'analyzeFile(getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', $context); + $this->analyzeFile((string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', $context); } public function testMissingThrowsDocblockSuppressedDuplicate(): void @@ -147,7 +147,7 @@ public function testMissingThrowsDocblockSuppressedDuplicate(): void Config::getInstance()->check_for_throws_docblock = true; $this->addFile( - getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', 'analyzeFile(getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', $context); + $this->analyzeFile((string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', $context); } public function testUncaughtThrowInGlobalScopeSuppressed(): void @@ -166,7 +166,7 @@ public function testUncaughtThrowInGlobalScopeSuppressed(): void Config::getInstance()->check_for_throws_in_global_scope = true; $this->addFile( - getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', 'analyzeFile(getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', $context); + $this->analyzeFile((string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', $context); } public function testUncaughtThrowInGlobalScopeSuppressedWithoutThrow(): void @@ -195,7 +195,7 @@ public function testUncaughtThrowInGlobalScopeSuppressedWithoutThrow(): void Config::getInstance()->check_for_throws_in_global_scope = true; $this->addFile( - getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', + (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', 'analyzeFile(getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', $context); + $this->analyzeFile((string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', $context); } public function testPossiblyUnusedPropertySuppressedOnClass(): void { $this->project_analyzer->getCodebase()->find_unused_code = "always"; - $file_path = getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php'; + $file_path = (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php'; $this->addFile( $file_path, 'project_analyzer->getCodebase()->find_unused_code = "always"; - $file_path = getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php'; + $file_path = (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php'; $this->addFile( $file_path, 'assertCount(2, $completion_items); } + public function testCompletionOnNestedArrayKey(): void + { + $codebase = $this->codebase; + $config = $codebase->config; + $config->throw_exception = false; + + $this->addFile( + 'somefile.php', + ' ["bar" => 1]]; + $my_array["foo"][] + ', + ); + + $codebase->file_provider->openFile('somefile.php'); + $codebase->scanFiles(); + $this->analyzeFile('somefile.php', new Context()); + + $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', new Position(2, 33)); + $this->assertSame( + [ + 'array{bar: 1}', + '[', + 92, + ], + $completion_data, + ); + + $completion_items = $codebase->getCompletionItemsForArrayKeys($completion_data[0]); + + $this->assertCount(1, $completion_items); + } + public function testTypeContextForFunctionArgument(): void { diff --git a/tests/LanguageServer/DiagnosticTest.php b/tests/LanguageServer/DiagnosticTest.php index d6eee08e893..7648c7b559d 100644 --- a/tests/LanguageServer/DiagnosticTest.php +++ b/tests/LanguageServer/DiagnosticTest.php @@ -86,7 +86,7 @@ public function testSnippetSupportDisabled(): void $this->codebase, $clientConfiguration, new Progress, - new PathMapper(getcwd(), getcwd()), + new PathMapper((string) getcwd(), (string) getcwd()), ); $write->on('message', function (Message $message) use ($deferred, $server): void { diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index f6cb3e75099..1f00196809e 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -1203,6 +1203,17 @@ public function method(mixed $value): mixed 'ignored_issues' => [], 'php_version' => '8.0', ], + 'phpdocObjectTypeAndReferenceInParameter' => [ + 'code' => 'bar($x);', + ], ]; } diff --git a/tests/ProjectCheckerTest.php b/tests/ProjectCheckerTest.php index cce7da9d697..cb155cf2408 100644 --- a/tests/ProjectCheckerTest.php +++ b/tests/ProjectCheckerTest.php @@ -93,7 +93,7 @@ public function testCheck(): void ob_start(); $this->project_analyzer->check('tests/fixtures/DummyProject'); - $output = ob_get_clean(); + $output = (string) ob_get_clean(); $this->assertStringContainsString('Target PHP version: 8.1 (set by tests)', $output); $this->assertStringContainsString('Scanning files...', $output); @@ -225,7 +225,7 @@ public function testCheckAfterFileChange(): void ), ); - $bat_file_path = getcwd() + $bat_file_path = (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'fixtures' . DIRECTORY_SEPARATOR . 'DummyProject' @@ -280,7 +280,7 @@ public function testCheckDir(): void ob_start(); $this->project_analyzer->checkDir('tests/fixtures/DummyProject'); - $output = ob_get_clean(); + $output = (string) ob_get_clean(); $this->assertStringContainsString('Target PHP version: 8.1 (set by tests)', $output); $this->assertStringContainsString('Scanning files...', $output); @@ -318,10 +318,10 @@ public function testCheckPaths(): void // checkPaths expects absolute paths, // otherwise it's unable to match them against configured folders $this->project_analyzer->checkPaths([ - realpath(getcwd() . '/tests/fixtures/DummyProject/Bar.php'), - realpath(getcwd() . '/tests/fixtures/DummyProject/SomeTrait.php'), + (string) realpath((string) getcwd() . '/tests/fixtures/DummyProject/Bar.php'), + (string) realpath((string) getcwd() . '/tests/fixtures/DummyProject/SomeTrait.php'), ]); - $output = ob_get_clean(); + $output = (string) ob_get_clean(); $this->assertStringContainsString('Target PHP version: 8.1 (set by tests)', $output); $this->assertStringContainsString('Scanning files...', $output); @@ -359,10 +359,10 @@ public function testCheckFile(): void // checkPaths expects absolute paths, // otherwise it's unable to match them against configured folders $this->project_analyzer->checkPaths([ - realpath(getcwd() . '/tests/fixtures/DummyProject/Bar.php'), - realpath(getcwd() . '/tests/fixtures/DummyProject/SomeTrait.php'), + (string) realpath((string) getcwd() . '/tests/fixtures/DummyProject/Bar.php'), + (string) realpath((string) getcwd() . '/tests/fixtures/DummyProject/SomeTrait.php'), ]); - $output = ob_get_clean(); + $output = (string) ob_get_clean(); $this->assertStringContainsString('Target PHP version: 8.1 (set by tests)', $output); $this->assertStringContainsString('Scanning files...', $output); diff --git a/tests/PropertyTypeTest.php b/tests/PropertyTypeTest.php index 5d75810bdde..0fbd78699e3 100644 --- a/tests/PropertyTypeTest.php +++ b/tests/PropertyTypeTest.php @@ -1375,18 +1375,23 @@ public function __construct() { /** @psalm-suppress UndefinedPropertyFetch */ if ($a->bar === null && rand(0, 1)) {}', ], - 'setPropertiesOfSpecialObjects' => [ + 'setPropertiesOfStdClass' => [ 'code' => 'b = "c"; - - $d = new SimpleXMLElement(""); - $d->e = "f";', + $a->b = "c";', 'assertions' => [ '$a' => 'stdClass', '$a->b' => 'string', - '$d' => 'SimpleXMLElement', - '$d->e' => 'mixed', + ], + ], + 'getPropertiesOfSimpleXmlElement' => [ + 'code' => '"); + $b = $a->b;', + 'assertions' => [ + '$a' => 'SimpleXMLElement', + '$a->b' => 'SimpleXMLElement|null', + '$b' => 'SimpleXMLElement|null', ], ], 'allowLessSpecificReturnTypeForOverriddenMethod' => [ @@ -3820,6 +3825,22 @@ class A { ', 'error_message' => '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 11f3a99422c..7ea34d9ad0c 100644 --- a/tests/PureAnnotationTest.php +++ b/tests/PureAnnotationTest.php @@ -41,7 +41,7 @@ function lower(string $s) : string { function highlight(string $needle, string $output) : string { $needle = preg_quote($needle, \'#\'); $needles = str_replace([\'"\', \' \'], [\'\', \'|\'], $needle); - $output = preg_replace("#({$needles})#im", "$1", $output); + $output = (string) preg_replace("#({$needles})#im", "$1", $output); return $output; }', @@ -152,7 +152,10 @@ function foo(array $arr) : array { 'code' => ' '\'\'', ], 'ignored_issues' => [ - 'InvalidArgument', + 'RedundantFunctionCall', ], ]; @@ -221,7 +221,9 @@ public function providerValidCodeParse(): iterable 'assertions' => [ '$val===' => 'string', ], - 'ignored_issues' => [], + 'ignored_issues' => [ + 'RedundantFunctionCall', + ], 'php_version' => '8.0', ]; @@ -251,11 +253,17 @@ public function providerValidCodeParse(): iterable public function providerInvalidCodeParse(): iterable { return [ - 'sprintfOnlyFormat' => [ + 'sprintfOnlyFormatWithoutPlaceholders' => [ 'code' => ' 'TooFewArguments', + 'error_message' => 'RedundantFunctionCall', + ], + 'printfOnlyFormatWithoutPlaceholders' => [ + 'code' => ' 'RedundantFunctionCall', ], 'sprintfTooFewArguments' => [ 'code' => ' ' 'InvalidArgument', + 'error_message' => 'RedundantFunctionCall', ], 'sprintfFormatWithoutPlaceholders' => [ 'code' => ' 'InvalidArgument', + 'error_message' => 'TooManyArguments', + 'ignored_issues' => [ + 'RedundantFunctionCall', + ], ], 'sprintfPaddedComplexEmptyStringFormat' => [ 'code' => ' 'InvalidArgument', ], + 'printfVariableFormat' => [ + 'code' => ' 'RedundantFunctionCall', + ], ]; } } diff --git a/tests/ReturnTypeTest.php b/tests/ReturnTypeTest.php index 2557fdf8abe..275f093c08e 100644 --- a/tests/ReturnTypeTest.php +++ b/tests/ReturnTypeTest.php @@ -1279,6 +1279,40 @@ function aggregate($type) { return $t; }', ], + 'returnByReferenceVariableInStaticMethod' => [ + 'code' => <<<'PHP' + [ + 'code' => <<<'PHP' + foo; + } + } + PHP, + ], + 'returnByReferenceVariableInFunction' => [ + 'code' => <<<'PHP' + [], 'php_version' => '8.0', ], + 'returnByReferenceNonVariableInStaticMethod' => [ + 'code' => <<<'PHP' + 'NonVariableReferenceReturn', + ], + 'returnByReferenceNonVariableInInstanceMethod' => [ + 'code' => <<<'PHP' + 'NonVariableReferenceReturn', + ], + 'returnByReferenceNonVariableInFunction' => [ + 'code' => <<<'PHP' + 'NonVariableReferenceReturn', + ], ]; } } diff --git a/tests/StubTest.php b/tests/StubTest.php index c52239d68ee..55d61ffbe7e 100644 --- a/tests/StubTest.php +++ b/tests/StubTest.php @@ -15,6 +15,7 @@ use Psalm\Internal\RuntimeCaches; use Psalm\Tests\Internal\Provider\FakeParserCacheProvider; +use function assert; use function define; use function defined; use function dirname; @@ -111,7 +112,7 @@ public function testStubFileClass(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -153,6 +154,7 @@ public function testLoadStubFileWithRelativePath(): void $path = $this->getOperatingSystemStyledPath('tests/fixtures/stubs/systemclass.phpstub'); $stub_files = $this->project_analyzer->getConfig()->getStubFiles(); + assert(!empty($stub_files)); $this->assertStringContainsString($path, reset($stub_files)); } @@ -175,6 +177,7 @@ public function testLoadStubFileWithAbsolutePath(): void $path = $this->getOperatingSystemStyledPath('tests/fixtures/stubs/systemclass.phpstub'); $stub_files = $this->project_analyzer->getConfig()->getStubFiles(); + assert(!empty($stub_files)); $this->assertStringContainsString($path, reset($stub_files)); } @@ -198,7 +201,7 @@ public function testStubFileConstant(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -236,7 +239,7 @@ public function testStubFileParentClass(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -278,7 +281,7 @@ public function testStubFileCircularReference(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -310,17 +313,17 @@ public function testPhpStormMetaParsingFile(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, 'creAte2("object"); $y2 = (new \Ns\MyClass)->creaTe2("exception"); - + $const1 = (new \Ns\MyClass)->creAte3(\Ns\MyClass::OBJECT); $const2 = (new \Ns\MyClass)->creaTe3("exception"); @@ -484,7 +487,7 @@ public function testNamespacedStubClass(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -521,7 +524,7 @@ public function testStubRegularFunction(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -552,7 +555,7 @@ public function testStubVariadicFunction(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -585,7 +588,7 @@ public function testStubVariadicFunctionWrongArgType(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -614,7 +617,7 @@ public function testUserVariadicWithFalseVariadic(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -649,7 +652,7 @@ public function testPolyfilledFunction(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -681,7 +684,7 @@ public function testConditionalConstantDefined(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -716,7 +719,7 @@ public function testStubbedConstantVarCommentType(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -755,7 +758,7 @@ public function testClassAlias(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -816,7 +819,7 @@ public function testStubFunctionWithFunctionExists(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -848,7 +851,7 @@ public function testNamespacedStubFunctionWithFunctionExists(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -879,7 +882,7 @@ public function testNoStubFunction(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -910,7 +913,7 @@ public function testNamespacedStubFunction(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -941,7 +944,7 @@ public function testConditionalNamespacedStubFunction(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -972,7 +975,7 @@ public function testConditionallyExtendingInterface(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -1019,7 +1022,7 @@ public function testStubFileWithExistingClassDefinition(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -1069,7 +1072,7 @@ public function testVersionDependentStubs(string $php_version, string $code): vo ); $this->project_analyzer->setPhpVersion($php_version, 'tests'); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile($file_path, $code); @@ -1096,7 +1099,7 @@ public function testStubFileWithPartialClassDefinitionWithMoreMethods(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -1144,7 +1147,7 @@ public function testExtendOnlyStubbedClass(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -1179,7 +1182,7 @@ public function testStubFileWithExtendedStubbedClass(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -1216,7 +1219,7 @@ public function testStubFileWithPartialClassDefinitionWithCoercion(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -1261,7 +1264,7 @@ public function testStubFileWithPartialClassDefinitionGeneralReturnType(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -1302,7 +1305,7 @@ public function testStubFileWithTemplatedClassDefinitionAndMagicMethodOverride() ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -1353,7 +1356,7 @@ public function testInheritedMethodUsedInStub(): void $this->project_analyzer->getCodebase()->reportUnusedCode(); - $vendor_file_path = getcwd() . '/vendor/vendor_class.php'; + $vendor_file_path = (string) getcwd() . '/vendor/vendor_class.php'; $this->addFile( $vendor_file_path, @@ -1369,7 +1372,7 @@ public static function vendorFunction(VendorClass $v) : void { }', ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -1403,7 +1406,7 @@ public function testStubOverridingMissingClass(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -1434,7 +1437,7 @@ public function testStubOverridingMissingMethod(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, @@ -1466,7 +1469,7 @@ public function testStubReplacingInterfaceDocblock(): void ); $this->addFile( - getcwd() . '/vendor/doctrine/import.php', + (string) getcwd() . '/vendor/doctrine/import.php', 'addFile( $file_path, @@ -1514,4 +1517,48 @@ function em(EntityManager $em) : void { $this->analyzeFile($file_path, new Context()); } + + /** + * This covers the following case encountered by mmcev106: + * - A function was defined without a docblock + * - The autoloader defined a global containing the path to that file + * - The code being scanned required the path specified by the autoloader defined global + * - A docblock was added via a stub that marked the function as a taint source + * - The stub docblock was incorrectly ignored, causing the the taint source to be ignored + */ + public function testAutoloadDefinedRequirePath(): void + { + $this->project_analyzer = $this->getProjectAnalyzerWithConfig( + TestConfig::loadFromXML( + dirname(__DIR__), + ' + + + + + + + + + ', + ), + ); + + $this->project_analyzer->trackTaintedInputs(); + + $file_path = (string) getcwd() . '/src/somefile.php'; + + $this->addFile( + $file_path, + 'expectExceptionMessage('TaintedHtml - /src/somefile.php'); + $this->analyzeFile($file_path, new Context()); + } } diff --git a/tests/TaintTest.php b/tests/TaintTest.php index e0610c8024a..4a847bd4bb0 100644 --- a/tests/TaintTest.php +++ b/tests/TaintTest.php @@ -750,6 +750,29 @@ function bar(array $arr): void { $mysqli->query("$a$b$c$d");', ], + 'querySimpleXMLElement' => [ + 'code' => 'xpath($expression); + }', + ], + 'escapeSeconds' => [ + 'code' => ' 'TaintedHtml', ], + 'taintedReflectionClass' => [ + 'code' => 'newInstance();', + 'error_message' => 'TaintedCallable', + ], + 'taintedReflectionFunction' => [ + 'code' => 'invoke();', + 'error_message' => 'TaintedCallable', + ], + 'querySimpleXMLElement' => [ + 'code' => 'xpath($expression); + }', + 'error_message' => 'TaintedXpath', + ], + 'queryDOMXPath' => [ + 'code' => 'query($expression); + }', + 'error_message' => 'TaintedXpath', + ], + 'evaluateDOMXPath' => [ + 'code' => 'evaluate($expression); + }', + 'error_message' => 'TaintedXpath', + ], + 'taintedSleep' => [ + 'code' => ' 'TaintedSleep', + ], + 'taintedUsleep' => [ + 'code' => ' 'TaintedSleep', + ], + 'taintedTimeNanosleepSeconds' => [ + 'code' => ' 'TaintedSleep', + ], + 'taintedTimeNanosleepNanoseconds' => [ + 'code' => ' 'TaintedSleep', + ], + 'taintedTimeSleepUntil' => [ + 'code' => ' 'TaintedSleep', + ], ]; } diff --git a/tests/Template/ClassTemplateExtendsTest.php b/tests/Template/ClassTemplateExtendsTest.php index d7ae80c63f3..130c178d504 100644 --- a/tests/Template/ClassTemplateExtendsTest.php +++ b/tests/Template/ClassTemplateExtendsTest.php @@ -1390,7 +1390,7 @@ class A {}', 'extendArrayObjectWithTemplateParams' => [ 'code' => ' */ diff --git a/tests/Template/ConditionalReturnTypeTest.php b/tests/Template/ConditionalReturnTypeTest.php index cafcb7b8668..18669470af7 100644 --- a/tests/Template/ConditionalReturnTypeTest.php +++ b/tests/Template/ConditionalReturnTypeTest.php @@ -885,6 +885,72 @@ function getSomethingElse() 'ignored_issues' => [], 'php_version' => '7.2', ], + 'ineritedConditionalTemplatedReturnType' => [ + 'code' => '|string $name + * @return ($name is class-string ? TRequestedInstance : InstanceType) + */ + public function build(string $name): mixed; + } + + /** + * @template InstanceType + * @template-implements ContainerInterface + */ + abstract class MixedContainer implements ContainerInterface + { + /** @param InstanceType $instance */ + public function __construct(private readonly mixed $instance) + {} + + public function build(string $name): mixed + { + return $this->instance; + } + } + + /** + * @template InstanceType of object + * @template-extends MixedContainer + */ + abstract class ObjectContainer extends MixedContainer + { + public function build(string $name): object + { + return parent::build($name); + } + } + + /** @template-extends ObjectContainer */ + final class SpecificObjectContainer extends ObjectContainer + { + } + + final class SpecificObject extends stdClass {} + + $container = new SpecificObjectContainer(new stdClass()); + $object = $container->build(SpecificObject::class); + $nonSpecificObject = $container->build("whatever"); + + /** @var ObjectContainer $container */ + $container = null; + $justObject = $container->build("whatever"); + $specificObject = $container->build(stdClass::class); + ', + 'assertions' => [ + '$object===' => 'SpecificObject', + '$nonSpecificObject===' => 'stdClass', + '$justObject===' => 'object', + '$specificObject===' => 'stdClass', + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 00739cbccb0..92c7300b17d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -51,7 +51,7 @@ public static function setUpBeforeClass(): void } parent::setUpBeforeClass(); - self::$src_dir_path = getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR; + self::$src_dir_path = (string) getcwd() . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR; } protected function makeConfig(): Config diff --git a/tests/TestConfig.php b/tests/TestConfig.php index 967e4db1de6..07dc439f11e 100644 --- a/tests/TestConfig.php +++ b/tests/TestConfig.php @@ -27,8 +27,10 @@ public function __construct() $this->use_docblock_types = true; $this->level = 1; $this->cache_directory = null; + $this->ignore_internal_falsable_issues = true; + $this->ignore_internal_nullable_issues = true; - $this->base_dir = getcwd() . DIRECTORY_SEPARATOR; + $this->base_dir = (string) getcwd() . DIRECTORY_SEPARATOR; if (!self::$cached_project_files) { self::$cached_project_files = ProjectFileFilter::loadFromXMLElement( diff --git a/tests/TraitTest.php b/tests/TraitTest.php index bd53bd603c8..4b04b7c4e31 100644 --- a/tests/TraitTest.php +++ b/tests/TraitTest.php @@ -1236,6 +1236,15 @@ trait A { const B = 0; } 'ignored_issues' => [], 'php_version' => '8.1', ], + 'duplicateTraitProperty' => [ + 'code' => ' 'DuplicateProperty', + ], ]; } } diff --git a/tests/Traits/InvalidCodeAnalysisTestTrait.php b/tests/Traits/InvalidCodeAnalysisTestTrait.php index 17a5453556f..537f8931d6b 100644 --- a/tests/Traits/InvalidCodeAnalysisTestTrait.php +++ b/tests/Traits/InvalidCodeAnalysisTestTrait.php @@ -80,7 +80,7 @@ public function testInvalidCode( $file_path = self::$src_dir_path . 'somefile.php'; - // $error_message = preg_replace('/ src[\/\\\\]somefile\.php/', ' src/somefile.php', $error_message); + // $error_message = (string) preg_replace('/ src[\/\\\\]somefile\.php/', ' src/somefile.php', $error_message); $this->expectException(CodeException::class); diff --git a/tests/TypeParseTest.php b/tests/TypeParseTest.php index b65e63d2410..3aecbf7b08b 100644 --- a/tests/TypeParseTest.php +++ b/tests/TypeParseTest.php @@ -480,7 +480,7 @@ public function testTKeyedArrayNonList(): void public function testTKeyedCallableArrayNonList(): void { $this->assertSame( - 'callable-array{0: class-string, 1: string}', + 'callable-array{class-string, string}', (string)Type::parseString('callable-array{0: class-string, 1: string}'), ); } diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index bee97444ceb..21fcc16385e 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -2429,7 +2429,7 @@ function splitDocLine($return_block) continue; } - $remaining = trim(preg_replace(\'@^[ \t]*\* *@m\', \' \', substr($return_block, $i + 1))); + $remaining = trim((string) preg_replace(\'@^[ \t]*\* *@m\', \' \', substr($return_block, $i + 1))); if ($remaining) { /** @var array */ @@ -2817,7 +2817,6 @@ function getIntOrFalse() {return false;} $lilstring = ""; $n = new SimpleXMLElement($lilstring); - /** @psalm-suppress MixedAssignment */ $n = $n->b; if (!$n instanceof SimpleXMLElement) { @@ -2905,7 +2904,11 @@ function b(B $_b): void { $lilstring = ""; $n = new SimpleXMLElement($lilstring); - $n = $n->children(); + $n = $n->b; + + if (!$n instanceof SimpleXMLIterator) { + return; + } if (!$n) { echo "false"; diff --git a/tests/TypeReconciliation/ReconcilerTest.php b/tests/TypeReconciliation/ReconcilerTest.php index 10ca3b22e3a..53c79074dd9 100644 --- a/tests/TypeReconciliation/ReconcilerTest.php +++ b/tests/TypeReconciliation/ReconcilerTest.php @@ -158,7 +158,7 @@ public function providerTestReconcilation(): array 'nullableClassStringTruthy' => ['class-string', new Truthy(), 'class-string|null'], 'iterableToArray' => ['array', new IsType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'iterable'], 'iterableToTraversable' => ['Traversable', new IsType(new TNamedObject('Traversable')), 'iterable'], - 'callableToCallableArray' => ['callable-array{0: class-string|object, 1: string}', new IsType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'callable'], + 'callableToCallableArray' => ['callable-array{class-string|object, non-empty-string}', new IsType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'callable'], 'SmallKeyedArrayAndCallable' => ['array{test: string}', new IsType(new TKeyedArray(['test' => Type::getString()])), 'callable'], 'BigKeyedArrayAndCallable' => ['array{foo: string, test: string, thing: string}', new IsType(new TKeyedArray(['foo' => Type::getString(), 'test' => Type::getString(), 'thing' => Type::getString()])), 'callable'], 'callableOrArrayToCallableArray' => ['array', new IsType(new TArray([Type::getArrayKey(), Type::getMixed()])), 'callable|array'], diff --git a/tests/UnusedCodeTest.php b/tests/UnusedCodeTest.php index 82fe731886a..4915ffdb6b3 100644 --- a/tests/UnusedCodeTest.php +++ b/tests/UnusedCodeTest.php @@ -105,7 +105,7 @@ public function testInvalidCode(string $code, string $error_message, array $igno public function testSeesClassesUsedAfterUnevaluatedCodeIssue(): void { $this->project_analyzer->getConfig()->throw_exception = false; - $file_path = getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php'; + $file_path = (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php'; $this->addFile( $file_path, @@ -137,7 +137,7 @@ function bar(): void{ public function testSeesUnusedClassReferencedByUnevaluatedCode(): void { $this->project_analyzer->getConfig()->throw_exception = false; - $file_path = getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php'; + $file_path = (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php'; $this->addFile( $file_path, diff --git a/tests/VariadicTest.php b/tests/VariadicTest.php index e77b0a142bb..a7aba476e6b 100644 --- a/tests/VariadicTest.php +++ b/tests/VariadicTest.php @@ -58,7 +58,7 @@ public function testVariadicFunctionFromAutoloadFile(): void ), ); - $file_path = getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . '/src/somefile.php'; $this->addFile( $file_path, diff --git a/tests/fixtures/performance/a.test b/tests/fixtures/performance/a.test index fd8f1ecea94..fcb14f8bb41 100644 --- a/tests/fixtures/performance/a.test +++ b/tests/fixtures/performance/a.test @@ -845,7 +845,7 @@ class PHPMailer case 'echo': default: //Normalize line breaks - $str = preg_replace('/\r\n|\r/ms', "\n", $str); + $str = (string) preg_replace('/\r\n|\r/ms', "\n", $str); echo gmdate('Y-m-d H:i:s'), "\t", //Trim trailing space @@ -990,7 +990,7 @@ class PHPMailer protected function addOrEnqueueAnAddress($kind, $address, $name) { $address = trim($address); - $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim + $name = trim((string) preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim $pos = strrpos($address, '@'); if (false === $pos) { // At-sign is missing. @@ -1160,7 +1160,7 @@ class PHPMailer public function setFrom($address, $name = '', $auto = true) { $address = trim($address); - $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim + $name = trim((string) preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim // Don't validate now addresses with IDN. Will be done in send(). $pos = strrpos($address, '@'); if (false === $pos or @@ -3090,7 +3090,7 @@ class PHPMailer $maxlen -= $maxlen % 4; $encoded = trim(chunk_split($encoded, $maxlen, "\n")); } - $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded); + $encoded = (string) preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded); } elseif ($matchcount > 0) { //1 or more chars need encoding, use Q-encode $encoding = 'Q'; @@ -3099,7 +3099,7 @@ class PHPMailer $encoded = $this->encodeQ($str, $position); $encoded = $this->wrapText($encoded, $maxlen, true); $encoded = str_replace('=' . static::$LE, "\n", trim($encoded)); - $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded); + $encoded = (string) preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded); } elseif (strlen($str) > $maxlen) { //No chars need encoding, but line is too long, so fold it $encoded = trim($this->wrapText($str, $maxlen, false)); @@ -3108,7 +3108,7 @@ class PHPMailer $encoded = trim(chunk_split($str, static::STD_LINE_LENGTH, static::$LE)); } $encoded = str_replace(static::$LE, "\n", trim($encoded)); - $encoded = preg_replace('/^(.*)$/m', ' \\1', $encoded); + $encoded = (string) preg_replace('/^(.*)$/m', ' \\1', $encoded); } else { //No reformatting needed return $str; @@ -3784,7 +3784,7 @@ class PHPMailer static::_mime_types((string) static::mb_pathinfo($filename, PATHINFO_EXTENSION)) ) ) { - $message = preg_replace( + $message = (string) preg_replace( '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui', $images[1][$imgindex] . '="cid:' . $cid . '"', $message @@ -4215,7 +4215,7 @@ class PHPMailer //Note PCRE \s is too broad a definition of whitespace; RFC5322 defines it as `[ \t]` //@see https://tools.ietf.org/html/rfc5322#section-2.2 //That means this may break if you do something daft like put vertical tabs in your headers. - $signHeader = preg_replace('/\r\n[ \t]+/', ' ', $signHeader); + $signHeader = (string) preg_replace('/\r\n[ \t]+/', ' ', $signHeader); $lines = explode("\r\n", $signHeader); foreach ($lines as $key => $line) { //If the header is missing a :, skip it as it's invalid @@ -4228,7 +4228,7 @@ class PHPMailer //Lower-case header name $heading = strtolower($heading); //Collapse white space within the value - $value = preg_replace('/[ \t]{2,}/', ' ', $value); + $value = (string) preg_replace('/[ \t]{2,}/', ' ', $value); //RFC6376 is slightly unclear here - it says to delete space at the *end* of each value //But then says to delete space before and after the colon. //Net result is the same as trimming both ends of the value. diff --git a/tests/fixtures/performance/b.test b/tests/fixtures/performance/b.test index 1e607f088bf..c750465932f 100644 --- a/tests/fixtures/performance/b.test +++ b/tests/fixtures/performance/b.test @@ -845,7 +845,7 @@ class PHPMailer case 'echo': default: //Normalize line breaks - $str = preg_replace('/\r\n|\r/ms', "\n", $str); + $str = (string) preg_replace('/\r\n|\r/ms', "\n", $str); echo gmdate('Y-m-d H:i:s'), "\t", //Trim trailing space @@ -990,7 +990,7 @@ class PHPMailer protected function addOrEnqueueAnAddress($kind, $address, $name) { $address = trim($address); - $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim + $name = trim((string) preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim $pos = strrpos($address, '@'); if (false === $pos) { // At-sign is missing. @@ -1160,7 +1160,7 @@ class PHPMailer public function setFrom($address, $name = '', $auto = true) { $address = trim($address); - $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim + $name = trim((string) preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim // Don't validate now addresses with IDN. Will be done in send(). $pos = strrpos($address, '@'); if (false === $pos or @@ -3090,7 +3090,7 @@ class PHPMailer $maxlen -= $maxlen % 4; $encoded = trim(chunk_split($encoded, $maxlen, "\n")); } - $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded); + $encoded = (string) preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded); } elseif ($matchcount > 0) { //1 or more chars need encoding, use Q-encode $encoding = 'Q'; @@ -3099,7 +3099,7 @@ class PHPMailer $encoded = $this->encodeQ($str, $position); $encoded = $this->wrapText($encoded, $maxlen, true); $encoded = str_replace('=' . static::$LE, "\n", trim($encoded)); - $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded); + $encoded = (string) preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded); } elseif (strlen($str) > $maxlen) { //No chars need encoding, but line is too long, so fold it $encoded = trim($this->wrapText($str, $maxlen, false)); @@ -3108,7 +3108,7 @@ class PHPMailer $encoded = trim(chunk_split($str, static::STD_LINE_LENGTH, static::$LE)); } $encoded = str_replace(static::$LE, "\n", trim($encoded)); - $encoded = preg_replace('/^(.*)$/m', ' \\1', $encoded); + $encoded = (string) preg_replace('/^(.*)$/m', ' \\1', $encoded); } else { //No reformatting needed return $str; @@ -3784,7 +3784,7 @@ class PHPMailer static::_mime_types((string) static::mb_pathinfo($filename, PATHINFO_EXTENSION)) ) ) { - $message = preg_replace( + $message = (string) preg_replace( '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui', $images[1][$imgindex] . '="cid:' . $cid . '"', $message @@ -4215,7 +4215,7 @@ class PHPMailer //Note PCRE \s is too broad a definition of whitespace; RFC5322 defines it as `[ \t]` //@see https://tools.ietf.org/html/rfc5322#section-2.2 //That means this may break if you do something daft like put vertical tabs in your headers. - $signHeader = preg_replace('/\r\n[ \t]+/', ' ', $signHeader); + $signHeader = (string) preg_replace('/\r\n[ \t]+/', ' ', $signHeader); $lines = explode("\r\n", $signHeader); foreach ($lines as $key => $line) { //If the header is missing a :, skip it as it's invalid @@ -4228,7 +4228,7 @@ class PHPMailer //Lower-case header name $heading = strtolower($heading); //Collapse white space within the value - $value = preg_replace('/[ \t]{2,}/', ' ', $value); + $value = (string) preg_replace('/[ \t]{2,}/', ' ', $value); //RFC6376 is slightly unclear here - it says to delete space at the *end* of each value //But then says to delete space before and after the colon. //Net result is the same as trimming both ends of the value. diff --git a/tests/fixtures/stubs/custom_taint_source.php b/tests/fixtures/stubs/custom_taint_source.php new file mode 100644 index 00000000000..59eb33da49d --- /dev/null +++ b/tests/fixtures/stubs/custom_taint_source.php @@ -0,0 +1,3 @@ +