diff --git a/.circleci/config.yml b/.circleci/config.yml index fb7edfe00e2..0c4d5026289 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,5 @@ # Use the latest 2.1 version of CircleCI pipeline processing engine, see https://circleci.com/docs/2.0/configuration-reference/ +# See https://app.circleci.com/pipelines/github/vimeo/psalm version: 2.1 executors: php-74: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0693fb9c4a0..cf9fbf7b147 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,51 @@ jobs: - run: | git ls-files | grep \\\.php$ | grep -v ^dictionaries/scripts/* | ./vendor/bin/parallel-lint --stdin + + code-style: + name: Code Style Analysis + runs-on: ubuntu-latest + steps: + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + tools: composer:v2 + coverage: none + env: + fail-fast: true + + - uses: actions/checkout@v3 + + - name: Get Composer Cache Directories + id: composer-cache + run: | + echo "files_cache=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + echo "vcs_cache=$(composer config cache-vcs-dir)" >> $GITHUB_OUTPUT + + - name: Generate composer.lock + run: composer update --no-install + env: + COMPOSER_ROOT_VERSION: dev-master + + - name: Cache composer cache + uses: actions/cache@v3 + with: + path: | + ${{ steps.composer-cache.outputs.files_cache }} + ${{ steps.composer-cache.outputs.vcs_cache }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Run composer install + run: composer install -o + env: + COMPOSER_ROOT_VERSION: dev-master + + - name: Code Style Analysis with PHPCS + run: vendor/bin/phpcs -d memory_limit=512M + chunk-matrix: permissions: contents: none @@ -70,7 +115,7 @@ jobs: echo "chunks=$(php -r 'echo json_encode(range(1, ${{ env.CHUNK_COUNT }} ));')" >> $GITHUB_OUTPUT tests: - name: "Unit Tests - ${{ matrix.chunk }}" + name: "Unit Tests - PHP ${{ matrix.php-version }} ${{ matrix.chunk }}/${{ matrix.count }}" runs-on: ubuntu-latest needs: @@ -79,6 +124,11 @@ jobs: strategy: fail-fast: false matrix: + php-version: + - "8.0" + - "8.1" + - "8.2" + - "8.3" count: ${{ fromJson(needs.chunk-matrix.outputs.count) }} chunk: ${{ fromJson(needs.chunk-matrix.outputs.chunks) }} @@ -91,7 +141,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.1' + php-version: "${{ matrix.php-version }}" ini-values: zend.assertions=1, assert.exception=1, opcache.enable_cli=1, opcache.jit=function, opcache.jit_buffer_size=512M tools: composer:v2 coverage: none diff --git a/composer.json b/composer.json index 7f4a9f9c8a9..7df7c97af38 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0", + "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", "ext-SimpleXML": "*", "ext-ctype": "*", "ext-dom": "*", diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 4145bf41e22..7d8c477b1a1 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -337,7 +337,6 @@ 'AppendIterator::next' => ['void'], 'AppendIterator::rewind' => ['void'], 'AppendIterator::valid' => ['bool'], -'ArgumentCountError::__clone' => ['void'], 'ArgumentCountError::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'ArgumentCountError::__toString' => ['string'], 'ArgumentCountError::__wakeup' => ['void'], @@ -348,7 +347,6 @@ 'ArgumentCountError::getPrevious' => ['?Throwable'], 'ArgumentCountError::getTrace' => ['list\',args?:array}>'], 'ArgumentCountError::getTraceAsString' => ['string'], -'ArithmeticError::__clone' => ['void'], 'ArithmeticError::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'ArithmeticError::__toString' => ['string'], 'ArithmeticError::__wakeup' => ['void'], @@ -491,7 +489,6 @@ 'atan' => ['float', 'num'=>'float'], 'atan2' => ['float', 'y'=>'float', 'x'=>'float'], 'atanh' => ['float', 'num'=>'float'], -'BadFunctionCallException::__clone' => ['void'], 'BadFunctionCallException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'BadFunctionCallException::__toString' => ['string'], 'BadFunctionCallException::getCode' => ['int'], @@ -501,7 +498,6 @@ 'BadFunctionCallException::getPrevious' => ['?Throwable'], 'BadFunctionCallException::getTrace' => ['list\',args?:array}>'], 'BadFunctionCallException::getTraceAsString' => ['string'], -'BadMethodCallException::__clone' => ['void'], 'BadMethodCallException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'BadMethodCallException::__toString' => ['string'], 'BadMethodCallException::getCode' => ['int'], @@ -661,7 +657,6 @@ 'clearstatcache' => ['void', 'clear_realpath_cache='=>'bool', 'filename='=>'string'], 'cli_get_process_title' => ['?string'], 'cli_set_process_title' => ['bool', 'title'=>'string'], -'ClosedGeneratorException::__clone' => ['void'], 'ClosedGeneratorException::__toString' => ['string'], 'ClosedGeneratorException::getCode' => ['int'], 'ClosedGeneratorException::getFile' => ['string'], @@ -1352,7 +1347,7 @@ 'datefmt_set_calendar' => ['bool', 'formatter'=>'IntlDateFormatter', 'calendar'=>'IntlCalendar|int|null'], 'datefmt_set_lenient' => ['void', 'formatter'=>'IntlDateFormatter', 'lenient'=>'bool'], 'datefmt_set_pattern' => ['bool', 'formatter'=>'IntlDateFormatter', 'pattern'=>'string'], -'datefmt_set_timezone' => ['false|null', 'formatter'=>'IntlDateFormatter', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], +'datefmt_set_timezone' => ['bool', 'formatter'=>'IntlDateFormatter', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], 'DateInterval::__construct' => ['void', 'duration'=>'string'], 'DateInterval::__set_state' => ['DateInterval', 'array'=>'array'], 'DateInterval::__wakeup' => ['void'], @@ -1633,7 +1628,6 @@ 'dom_xpath_query' => ['DOMNodeList', 'expr'=>'string', 'context'=>'DOMNode', 'registernodens'=>'bool'], 'dom_xpath_register_ns' => ['bool', 'prefix'=>'string', 'uri'=>'string'], 'dom_xpath_register_php_functions' => [''], -'DomainException::__clone' => ['void'], 'DomainException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'DomainException::__toString' => ['string'], 'DomainException::__wakeup' => ['void'], @@ -2093,7 +2087,6 @@ 'error_get_last' => ['?array{type:int,message:string,file:string,line:int}'], 'error_log' => ['bool', 'message'=>'string', 'message_type='=>'int', 'destination='=>'?string', 'additional_headers='=>'?string'], 'error_reporting' => ['int', 'error_level='=>'?int'], -'ErrorException::__clone' => ['void'], 'ErrorException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'severity='=>'int', 'filename='=>'?string', 'line='=>'?int', 'previous='=>'?Throwable'], 'ErrorException::__toString' => ['string'], 'ErrorException::getCode' => ['int'], @@ -3925,7 +3918,7 @@ 'hash_hmac_algos' => ['list'], 'hash_hmac_file' => ['non-empty-string', 'algo'=>'string', 'filename'=>'string', 'key'=>'string', 'binary='=>'bool'], 'hash_init' => ['HashContext', 'algo'=>'string', 'flags='=>'int', 'key='=>'string', 'options='=>'array{seed:scalar}'], -'hash_pbkdf2' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool'], +'hash_pbkdf2' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool', 'options=' => 'array'], 'hash_update' => ['bool', 'context'=>'HashContext', 'data'=>'string'], 'hash_update_file' => ['bool', 'context'=>'HashContext', 'filename'=>'string', 'stream_context='=>'?resource'], 'hash_update_stream' => ['int', 'context'=>'HashContext', 'stream'=>'resource', 'length='=>'int'], @@ -5581,21 +5574,21 @@ 'imap_body' => ['string|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'flags='=>'int'], 'imap_bodystruct' => ['stdClass|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'section'=>'string'], 'imap_check' => ['stdClass|false', 'imap'=>'IMAP\Connection'], -'imap_clearflag_full' => ['bool', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flag'=>'string', 'options='=>'int'], -'imap_close' => ['bool', 'imap'=>'IMAP\Connection', 'flags='=>'int'], +'imap_clearflag_full' => ['true', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flag'=>'string', 'options='=>'int'], +'imap_close' => ['true', 'imap'=>'IMAP\Connection', 'flags='=>'int'], 'imap_create' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], 'imap_createmailbox' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], -'imap_delete' => ['bool', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], +'imap_delete' => ['true', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], 'imap_deletemailbox' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], 'imap_errors' => ['array|false'], -'imap_expunge' => ['bool', 'imap'=>'IMAP\Connection'], +'imap_expunge' => ['true', 'imap'=>'IMAP\Connection'], 'imap_fetch_overview' => ['array|false', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flags='=>'int'], 'imap_fetchbody' => ['string|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'section'=>'string', 'flags='=>'int'], 'imap_fetchheader' => ['string|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'flags='=>'int'], 'imap_fetchmime' => ['string|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'section'=>'string', 'flags='=>'int'], 'imap_fetchstructure' => ['stdClass|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'flags='=>'int'], 'imap_fetchtext' => ['string|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'flags='=>'int'], -'imap_gc' => ['bool', 'imap'=>'IMAP\Connection', 'flags'=>'int'], +'imap_gc' => ['true', 'imap'=>'IMAP\Connection', 'flags'=>'int'], 'imap_get_quota' => ['array|false', 'imap'=>'IMAP\Connection', 'quota_root'=>'string'], 'imap_get_quotaroot' => ['array|false', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], 'imap_getacl' => ['array|false', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], @@ -5636,14 +5629,14 @@ 'imap_search' => ['array|false', 'imap'=>'IMAP\Connection', 'criteria'=>'string', 'flags='=>'int', 'charset='=>'string'], 'imap_set_quota' => ['bool', 'imap'=>'IMAP\Connection', 'quota_root'=>'string', 'mailbox_size'=>'int'], 'imap_setacl' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string', 'user_id'=>'string', 'rights'=>'string'], -'imap_setflag_full' => ['bool', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flag'=>'string', 'options='=>'int'], +'imap_setflag_full' => ['true', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flag'=>'string', 'options='=>'int'], 'imap_sort' => ['array|false', 'imap'=>'IMAP\Connection', 'criteria'=>'int', 'reverse'=>'bool', 'flags='=>'int', 'search_criteria='=>'?string', 'charset='=>'?string'], 'imap_status' => ['stdClass|false', 'imap'=>'IMAP\Connection', 'mailbox'=>'string', 'flags'=>'int'], 'imap_subscribe' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], 'imap_thread' => ['array|false', 'imap'=>'IMAP\Connection', 'flags='=>'int'], 'imap_timeout' => ['int|bool', 'timeout_type'=>'int', 'timeout='=>'int'], 'imap_uid' => ['int|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int'], -'imap_undelete' => ['bool', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], +'imap_undelete' => ['true', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], 'imap_unsubscribe' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], 'imap_utf7_decode' => ['string|false', 'string'=>'string'], 'imap_utf7_encode' => ['string', 'string'=>'string'], @@ -5738,11 +5731,11 @@ 'IntlBreakIterator::next' => ['int', 'offset='=>'?int'], 'IntlBreakIterator::preceding' => ['int', 'offset'=>'int'], 'IntlBreakIterator::previous' => ['int'], -'IntlBreakIterator::setText' => ['?bool', 'text'=>'string'], +'IntlBreakIterator::setText' => ['bool', 'text'=>'string'], 'intlcal_add' => ['bool', 'calendar'=>'IntlCalendar', 'field'=>'int', 'value'=>'int'], 'intlcal_after' => ['bool', 'calendar'=>'IntlCalendar', 'other'=>'IntlCalendar'], 'intlcal_before' => ['bool', 'calendar'=>'IntlCalendar', 'other'=>'IntlCalendar'], -'intlcal_clear' => ['bool', 'calendar'=>'IntlCalendar', 'field='=>'?int'], +'intlcal_clear' => ['true', 'calendar'=>'IntlCalendar', 'field='=>'?int'], 'intlcal_create_instance' => ['?IntlCalendar', 'timezone='=>'mixed', 'locale='=>'?string'], 'intlcal_equals' => ['bool', 'calendar'=>'IntlCalendar', 'other'=>'IntlCalendar'], 'intlcal_field_difference' => ['int|false', 'calendar'=>'IntlCalendar', 'timestamp'=>'float', 'field'=>'int'], @@ -5775,8 +5768,8 @@ 'intlcal_roll' => ['bool', 'calendar'=>'IntlCalendar', 'field'=>'int', 'value'=>'mixed'], 'intlcal_set' => ['bool', 'calendar'=>'IntlCalendar', 'year'=>'int', 'month'=>'int'], 'intlcal_set\'1' => ['bool', 'calendar'=>'IntlCalendar', 'year'=>'int', 'month'=>'int', 'dayOfMonth='=>'int', 'hour='=>'int', 'minute='=>'int', 'second='=>'int'], -'intlcal_set_first_day_of_week' => ['bool', 'calendar'=>'IntlCalendar', 'dayOfWeek'=>'int'], -'intlcal_set_lenient' => ['bool', 'calendar'=>'IntlCalendar', 'lenient'=>'bool'], +'intlcal_set_first_day_of_week' => ['true', 'calendar'=>'IntlCalendar', 'dayOfWeek'=>'int'], +'intlcal_set_lenient' => ['true', 'calendar'=>'IntlCalendar', 'lenient'=>'bool'], 'intlcal_set_repeated_wall_time_option' => ['true', 'calendar'=>'IntlCalendar', 'option'=>'int'], 'intlcal_set_skipped_wall_time_option' => ['true', 'calendar'=>'IntlCalendar', 'option'=>'int'], 'intlcal_set_time' => ['bool', 'calendar'=>'IntlCalendar', 'timestamp'=>'float'], @@ -5838,7 +5831,7 @@ 'IntlChar::charType' => ['?int', 'codepoint'=>'int|string'], 'IntlChar::chr' => ['?string', 'codepoint'=>'int|string'], 'IntlChar::digit' => ['int|false|null', 'codepoint'=>'int|string', 'base='=>'int'], -'IntlChar::enumCharNames' => ['?bool', 'start'=>'string|int', 'end'=>'string|int', 'callback'=>'callable(int,int,int):void', 'type='=>'int'], +'IntlChar::enumCharNames' => ['bool', 'start'=>'string|int', 'end'=>'string|int', 'callback'=>'callable(int,int,int):void', 'type='=>'int'], 'IntlChar::enumCharTypes' => ['void', 'callback'=>'callable(int,int,int):void'], 'IntlChar::foldCase' => ['int|string|null', 'codepoint'=>'int|string', 'options='=>'int'], 'IntlChar::forDigit' => ['int', 'digit'=>'int', 'base='=>'int'], @@ -5888,7 +5881,6 @@ 'IntlChar::tolower' => ['int|string|null', 'codepoint'=>'int|string'], 'IntlChar::totitle' => ['int|string|null', 'codepoint'=>'int|string'], 'IntlChar::toupper' => ['int|string|null', 'codepoint'=>'int|string'], -'IntlCodePointBreakIterator::__construct' => ['void'], 'IntlCodePointBreakIterator::createCharacterInstance' => ['?IntlRuleBasedBreakIterator', 'locale='=>'?string'], 'IntlCodePointBreakIterator::createCodePointInstance' => ['IntlCodePointBreakIterator'], 'IntlCodePointBreakIterator::createLineInstance' => ['?IntlRuleBasedBreakIterator', 'locale='=>'?string'], @@ -5909,7 +5901,7 @@ 'IntlCodePointBreakIterator::next' => ['int', 'offset='=>'?int'], 'IntlCodePointBreakIterator::preceding' => ['int', 'offset'=>'int'], 'IntlCodePointBreakIterator::previous' => ['int'], -'IntlCodePointBreakIterator::setText' => ['?bool', 'text'=>'string'], +'IntlCodePointBreakIterator::setText' => ['bool', 'text'=>'string'], 'IntlDateFormatter::__construct' => ['void', 'locale'=>'?string', 'dateType='=>'int', 'timeType='=>'int', 'timezone='=>'IntlTimeZone|DateTimeZone|string|null', 'calendar='=>'IntlCalendar|int|null', 'pattern='=>'?string'], 'IntlDateFormatter::create' => ['?IntlDateFormatter', 'locale'=>'?string', 'dateType='=>'int', 'timeType='=>'int', 'timezone='=>'IntlTimeZone|DateTimeZone|string|null', 'calendar='=>'IntlCalendar|int|null', 'pattern='=>'?string'], 'IntlDateFormatter::format' => ['string|false', 'datetime'=>'IntlCalendar|DateTimeInterface|array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int}|array{tm_sec: int, tm_min: int, tm_hour: int, tm_mday: int, tm_mon: int, tm_year: int, tm_wday: int, tm_yday: int, tm_isdst: int}|string|int|float'], @@ -5930,8 +5922,7 @@ 'IntlDateFormatter::setCalendar' => ['bool', 'calendar'=>'IntlCalendar|int|null'], 'IntlDateFormatter::setLenient' => ['void', 'lenient'=>'bool'], 'IntlDateFormatter::setPattern' => ['bool', 'pattern'=>'string'], -'IntlDateFormatter::setTimeZone' => ['null|false', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], -'IntlException::__clone' => ['void'], +'IntlDateFormatter::setTimeZone' => ['bool', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], 'IntlException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'IntlException::__toString' => ['string'], 'IntlException::__wakeup' => ['void'], @@ -6027,7 +6018,7 @@ 'IntlRuleBasedBreakIterator::next' => ['int', 'offset='=>'?int'], 'IntlRuleBasedBreakIterator::preceding' => ['int', 'offset'=>'int'], 'IntlRuleBasedBreakIterator::previous' => ['int'], -'IntlRuleBasedBreakIterator::setText' => ['?bool', 'text'=>'string'], +'IntlRuleBasedBreakIterator::setText' => ['bool', 'text'=>'string'], 'IntlTimeZone::countEquivalentIDs' => ['int|false', 'timezoneId'=>'string'], 'IntlTimeZone::createDefault' => ['IntlTimeZone'], 'IntlTimeZone::createEnumeration' => ['IntlIterator|false', 'countryOrRawOffset='=>'IntlTimeZone|string|int|float|null'], @@ -6072,7 +6063,6 @@ 'intltz_use_daylight_time' => ['bool', 'timezone'=>'IntlTimeZone'], 'intlz_create_default' => ['IntlTimeZone'], 'intval' => ['int', 'value'=>'mixed', 'base='=>'int'], -'InvalidArgumentException::__clone' => ['void'], 'InvalidArgumentException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'InvalidArgumentException::__toString' => ['string'], 'InvalidArgumentException::getCode' => ['int'], @@ -6158,7 +6148,6 @@ 'json_last_error' => ['int'], 'json_last_error_msg' => ['string'], 'json_validate' => ['bool', 'json'=>'string', 'depth='=>'positive-int', 'flags='=>'int'], -'JsonException::__clone' => ['void'], 'JsonException::__construct' => ['void', "message="=>"string", 'code='=>'int', 'previous='=>'?Throwable'], 'JsonException::__toString' => ['string'], 'JsonException::__wakeup' => ['void'], @@ -6385,7 +6374,6 @@ 'legendObj::free' => ['void'], 'legendObj::set' => ['int', 'property_name'=>'string', 'new_value'=>''], 'legendObj::updateFromString' => ['int', 'snippet'=>'string'], -'LengthException::__clone' => ['void'], 'LengthException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'LengthException::__toString' => ['string'], 'LengthException::getCode' => ['int'], @@ -6498,7 +6486,6 @@ 'log' => ['float', 'num'=>'float', 'base='=>'float'], 'log10' => ['float', 'num'=>'float'], 'log1p' => ['float', 'num'=>'float'], -'LogicException::__clone' => ['void'], 'LogicException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'LogicException::__toString' => ['string'], 'LogicException::getCode' => ['int'], @@ -7652,7 +7639,7 @@ 'mt_getrandmax' => ['int'], 'mt_rand' => ['int', 'min'=>'int', 'max'=>'int'], 'mt_rand\'1' => ['int'], -'mt_srand' => ['void', 'seed='=>'int', 'mode='=>'int'], +'mt_srand' => ['void', 'seed='=>'?int', 'mode='=>'int'], 'MultipleIterator::__construct' => ['void', 'flags='=>'int'], 'MultipleIterator::attachIterator' => ['void', 'iterator'=>'Iterator', 'info='=>'string|int|null'], 'MultipleIterator::containsIterator' => ['bool', 'iterator'=>'Iterator'], @@ -8129,8 +8116,8 @@ 'MysqlndUhPreparedStatement::__construct' => ['void'], 'MysqlndUhPreparedStatement::execute' => ['bool', 'statement'=>'mysqlnd_prepared_statement'], 'MysqlndUhPreparedStatement::prepare' => ['bool', 'statement'=>'mysqlnd_prepared_statement', 'query'=>'string'], -'natcasesort' => ['bool', '&rw_array'=>'array'], -'natsort' => ['bool', '&rw_array'=>'array'], +'natcasesort' => ['true', '&rw_array'=>'array'], +'natsort' => ['true', '&rw_array'=>'array'], 'net_get_interfaces' => ['array>|false'], 'newrelic_add_custom_parameter' => ['bool', 'key'=>'string', 'value'=>'bool|float|int|string'], 'newrelic_add_custom_tracer' => ['bool', 'function_name'=>'string'], @@ -8539,7 +8526,6 @@ 'OuterIterator::next' => ['void'], 'OuterIterator::rewind' => ['void'], 'OuterIterator::valid' => ['bool'], -'OutOfBoundsException::__clone' => ['void'], 'OutOfBoundsException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'OutOfBoundsException::__toString' => ['string'], 'OutOfBoundsException::getCode' => ['int'], @@ -8549,7 +8535,6 @@ 'OutOfBoundsException::getPrevious' => ['?Throwable'], 'OutOfBoundsException::getTrace' => ['list\',args?:array}>'], 'OutOfBoundsException::getTraceAsString' => ['string'], -'OutOfRangeException::__clone' => ['void'], 'OutOfRangeException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'OutOfRangeException::__toString' => ['string'], 'OutOfRangeException::getCode' => ['int'], @@ -8576,7 +8561,6 @@ 'outputformatObj::set' => ['int', 'property_name'=>'string', 'new_value'=>''], 'outputformatObj::setOption' => ['void', 'property_name'=>'string', 'new_value'=>'string'], 'outputformatObj::validate' => ['int'], -'OverflowException::__clone' => ['void'], 'OverflowException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'OverflowException::__toString' => ['string'], 'OverflowException::getCode' => ['int'], @@ -8668,7 +8652,6 @@ 'parse_ini_string' => ['array|false', 'ini_string'=>'string', 'process_sections='=>'bool', 'scanner_mode='=>'int'], 'parse_str' => ['void', 'string'=>'string', '&w_result'=>'array'], 'parse_url' => ['int|string|array|null|false', 'url'=>'string', 'component='=>'int'], -'ParseError::__clone' => ['void'], 'ParseError::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'ParseError::__toString' => ['string'], 'ParseError::getCode' => ['int'], @@ -9400,7 +9383,7 @@ 'posix_getppid' => ['int'], 'posix_getpwnam' => ['array{name: string, passwd: string, uid: int, gid: int, gecos: string, dir: string, shell: string}|false', 'username'=>'string'], 'posix_getpwuid' => ['array{name: string, passwd: string, uid: int, gid: int, gecos: string, dir: string, shell: string}|false', 'user_id'=>'int'], -'posix_getrlimit' => ['array{"soft core": string, "hard core": string, "soft data": string, "hard data": string, "soft stack": integer, "hard stack": string, "soft totalmem": string, "hard totalmem": string, "soft rss": string, "hard rss": string, "soft maxproc": integer, "hard maxproc": integer, "soft memlock": integer, "hard memlock": integer, "soft cpu": string, "hard cpu": string, "soft filesize": string, "hard filesize": string, "soft openfiles": integer, "hard openfiles": integer}|false'], +'posix_getrlimit' => ['array{"soft core": string, "hard core": string, "soft data": string, "hard data": string, "soft stack": integer, "hard stack": string, "soft totalmem": string, "hard totalmem": string, "soft rss": string, "hard rss": string, "soft maxproc": integer, "hard maxproc": integer, "soft memlock": integer, "hard memlock": integer, "soft cpu": string, "hard cpu": string, "soft filesize": string, "hard filesize": string, "soft openfiles": integer, "hard openfiles": integer}|false', 'resource=' => '?int'], 'posix_getsid' => ['int|false', 'process_id'=>'int'], 'posix_getuid' => ['int'], 'posix_initgroups' => ['bool', 'username'=>'string', 'group_id'=>'int'], @@ -9661,7 +9644,6 @@ 'random_bytes' => ['non-empty-string', 'length'=>'positive-int'], 'random_int' => ['int', 'min'=>'int', 'max'=>'int'], 'range' => ['non-empty-array', 'start'=>'string|int|float', 'end'=>'string|int|float', 'step='=>'int<1, max>|float'], -'RangeException::__clone' => ['void'], 'RangeException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'RangeException::__toString' => ['string'], 'RangeException::getCode' => ['int'], @@ -10661,12 +10643,10 @@ 'ReflectionMethod::isVariadic' => ['bool'], 'ReflectionMethod::returnsReference' => ['bool'], 'ReflectionMethod::setAccessible' => ['void', 'accessible'=>'bool'], -'ReflectionNamedType::__clone' => ['void'], 'ReflectionNamedType::__toString' => ['string'], 'ReflectionNamedType::allowsNull' => ['bool'], 'ReflectionNamedType::getName' => ['string'], 'ReflectionNamedType::isBuiltin' => ['bool'], -'ReflectionObject::__clone' => ['void'], 'ReflectionObject::__construct' => ['void', 'object'=>'object'], 'ReflectionObject::__toString' => ['string'], 'ReflectionObject::getConstant' => ['mixed', 'name'=>'string'], @@ -10851,7 +10831,7 @@ 'RRDGraph::setOptions' => ['void', 'options'=>'array'], 'RRDUpdater::__construct' => ['void', 'path'=>'string'], 'RRDUpdater::update' => ['bool', 'values'=>'array', 'time='=>'string'], -'rsort' => ['bool', '&rw_array'=>'array', 'flags='=>'int'], +'rsort' => ['true', '&rw_array'=>'array', 'flags='=>'int'], 'rtrim' => ['string', 'string'=>'string', 'characters='=>'string'], 'runkit7_constant_add' => ['bool', 'constant_name'=>'string', 'value'=>'mixed', 'new_visibility='=>'int'], 'runkit7_constant_redefine' => ['bool', 'constant_name'=>'string', 'value'=>'mixed', 'new_visibility='=>'?int'], @@ -10898,7 +10878,6 @@ 'Runkit_Sandbox_Parent::__construct' => ['void'], 'runkit_superglobals' => ['array'], 'runkit_zval_inspect' => ['array', 'value'=>'mixed'], -'RuntimeException::__clone' => ['void'], 'RuntimeException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'RuntimeException::__toString' => ['string'], 'RuntimeException::getCode' => ['int'], @@ -12758,7 +12737,7 @@ 'sqlsrv_send_stream_data' => ['bool', 'stmt'=>'resource'], 'sqlsrv_server_info' => ['array', 'conn'=>'resource'], 'sqrt' => ['float', 'num'=>'float'], -'srand' => ['void', 'seed='=>'int', 'mode='=>'int'], +'srand' => ['void', 'seed='=>'?int', 'mode='=>'int'], 'sscanf' => ['list|int|null', 'string'=>'string', 'format'=>'string', '&...w_vars='=>'string|int|float|null'], 'ssdeep_fuzzy_compare' => ['int', 'signature1'=>'string', 'signature2'=>'string'], 'ssdeep_fuzzy_hash' => ['string', 'to_hash'=>'string'], @@ -14010,7 +13989,6 @@ 'transliterator_transliterate' => ['string|false', 'transliterator'=>'Transliterator|string', 'string'=>'string', 'start='=>'int', 'end='=>'int'], 'trigger_error' => ['bool', 'message'=>'string', 'error_level='=>'256|512|1024|16384'], 'trim' => ['string', 'string'=>'string', 'characters='=>'string'], -'TypeError::__clone' => ['void'], 'TypeError::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'TypeError::__toString' => ['string'], 'TypeError::getCode' => ['int'], @@ -14233,7 +14211,6 @@ 'ui\window::setTitle' => ['', 'title'=>'string'], 'uksort' => ['true', '&rw_array'=>'array', 'callback'=>'callable(mixed,mixed):int'], 'umask' => ['int', 'mask='=>'?int'], -'UnderflowException::__clone' => ['void'], 'UnderflowException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'UnderflowException::__toString' => ['string'], 'UnderflowException::getCode' => ['int'], @@ -14243,7 +14220,6 @@ 'UnderflowException::getPrevious' => ['?Throwable'], 'UnderflowException::getTrace' => ['list\',args?:array}>'], 'UnderflowException::getTraceAsString' => ['string'], -'UnexpectedValueException::__clone' => ['void'], 'UnexpectedValueException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'UnexpectedValueException::__toString' => ['string'], 'UnexpectedValueException::getCode' => ['int'], diff --git a/dictionaries/CallMap_83_delta.php b/dictionaries/CallMap_83_delta.php index 2d27020ddc5..9bcf76deece 100644 --- a/dictionaries/CallMap_83_delta.php +++ b/dictionaries/CallMap_83_delta.php @@ -25,8 +25,124 @@ '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}'], ], + 'srand' => [ + 'old' => ['void', 'seed='=>'int', 'mode='=>'int'], + 'new' => ['void', 'seed='=>'?int', 'mode='=>'int'], + ], + 'mt_srand' => [ + 'old' => ['void', 'seed='=>'int', 'mode='=>'int'], + 'new' =>['void', 'seed='=>'?int', 'mode='=>'int'], + ], + 'posix_getrlimit' => [ + 'old' => ['array{"soft core": string, "hard core": string, "soft data": string, "hard data": string, "soft stack": integer, "hard stack": string, "soft totalmem": string, "hard totalmem": string, "soft rss": string, "hard rss": string, "soft maxproc": integer, "hard maxproc": integer, "soft memlock": integer, "hard memlock": integer, "soft cpu": string, "hard cpu": string, "soft filesize": string, "hard filesize": string, "soft openfiles": integer, "hard openfiles": integer}|false'], + 'new' => ['array{"soft core": string, "hard core": string, "soft data": string, "hard data": string, "soft stack": integer, "hard stack": string, "soft totalmem": string, "hard totalmem": string, "soft rss": string, "hard rss": string, "soft maxproc": integer, "hard maxproc": integer, "soft memlock": integer, "hard memlock": integer, "soft cpu": string, "hard cpu": string, "soft filesize": string, "hard filesize": string, "soft openfiles": integer, "hard openfiles": integer}|false', 'resource=' => '?int'], + ], + 'natcasesort' => [ + 'old' => ['bool', '&rw_array'=>'array'], + 'new' => ['true', '&rw_array'=>'array'], + ], + 'natsort' => [ + 'old' => ['bool', '&rw_array'=>'array'], + 'new' => ['true', '&rw_array'=>'array'], + ], + 'rsort' => [ + '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'], + ], + 'imap_expunge' => [ + 'old' => ['bool', 'imap'=>'IMAP\Connection'], + 'new' => ['true', 'imap'=>'IMAP\Connection'], + ], + 'imap_gc' => [ + 'old' => ['bool', 'imap'=>'IMAP\Connection', 'flags'=>'int'], + 'new' => ['true', 'imap'=>'IMAP\Connection', 'flags'=>'int'], + ], + 'imap_undelete' => [ + 'old' => ['bool', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], + 'new' => ['true', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], + ], + 'imap_delete' => [ + 'old' => ['bool', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], + 'new' => ['true', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], + ], + 'imap_clearflag_full' => [ + 'old' => ['bool', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flag'=>'string', 'options='=>'int'], + 'new' => ['true', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flag'=>'string', 'options='=>'int'], + ], + 'imap_close' => [ + 'old' => ['bool', 'imap'=>'IMAP\Connection', 'flags='=>'int'], + 'new' => ['true', 'imap'=>'IMAP\Connection', 'flags='=>'int'], + ], + 'intlcal_clear' => [ + 'old' => ['bool', 'calendar'=>'IntlCalendar', 'field='=>'?int'], + 'new' => ['true', 'calendar'=>'IntlCalendar', 'field='=>'?int'], + ], + 'intlcal_set_lenient' => [ + 'old' => ['bool', 'calendar'=>'IntlCalendar', 'lenient'=>'bool'], + 'new' => ['true', 'calendar'=>'IntlCalendar', 'lenient'=>'bool'], + ], + 'intlcal_set_first_day_of_week' => [ + 'old' => ['bool', 'calendar'=>'IntlCalendar', 'dayOfWeek'=>'int'], + 'new' => ['true', 'calendar'=>'IntlCalendar', 'dayOfWeek'=>'int'], + ], + 'datefmt_set_timezone' => [ + 'old' => ['false|null', 'formatter'=>'IntlDateFormatter', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], + 'new' => ['bool', 'formatter'=>'IntlDateFormatter', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], + ], + 'IntlRuleBasedBreakIterator::setText' => [ + 'old' => ['?bool', 'text'=>'string'], + 'new' => ['bool', 'text'=>'string'], + ], + 'IntlCodePointBreakIterator::setText' => [ + 'old' => ['?bool', 'text'=>'string'], + 'new' => ['bool', 'text'=>'string'], + ], + 'IntlDateFormatter::setTimeZone' => [ + 'old' => ['null|false', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], + 'new' => ['bool', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], + ], + 'IntlChar::enumCharNames' => [ + 'old' => ['?bool', 'start'=>'string|int', 'end'=>'string|int', 'callback'=>'callable(int,int,int):void', 'type='=>'int'], + 'new' => ['bool', 'start'=>'string|int', 'end'=>'string|int', 'callback'=>'callable(int,int,int):void', 'type='=>'int'], + ], + 'IntlBreakIterator::setText' => [ + 'old' => ['?bool', 'text'=>'string'], + 'new' => ['bool', 'text'=>'string'], + ], ], 'removed' => [ + 'OutOfBoundsException::__clone' => ['void'], + 'ArgumentCountError::__clone' => ['void'], + 'ArithmeticError::__clone' => ['void'], + 'BadFunctionCallException::__clone' => ['void'], + 'BadMethodCallException::__clone' => ['void'], + 'ClosedGeneratorException::__clone' => ['void'], + 'DomainException::__clone' => ['void'], + 'ErrorException::__clone' => ['void'], + 'IntlException::__clone' => ['void'], + 'InvalidArgumentException::__clone' => ['void'], + 'JsonException::__clone' => ['void'], + 'LengthException::__clone' => ['void'], + 'LogicException::__clone' => ['void'], + 'OutOfRangeException::__clone' => ['void'], + 'OverflowException::__clone' => ['void'], + 'ParseError::__clone' => ['void'], + 'RangeException::__clone' => ['void'], + 'ReflectionNamedType::__clone' => ['void'], + 'ReflectionObject::__clone' => ['void'], + 'RuntimeException::__clone' => ['void'], + 'TypeError::__clone' => ['void'], + 'UnderflowException::__clone' => ['void'], + 'UnexpectedValueException::__clone' => ['void'], + 'IntlCodePointBreakIterator::__construct' => ['void'], ], ]; diff --git a/src/Psalm/Internal/LanguageServer/Client/Progress/LegacyProgress.php b/src/Psalm/Internal/LanguageServer/Client/Progress/LegacyProgress.php new file mode 100644 index 00000000000..13eba280c7e --- /dev/null +++ b/src/Psalm/Internal/LanguageServer/Client/Progress/LegacyProgress.php @@ -0,0 +1,83 @@ +handler = $handler; + } + + public function begin(string $title, ?string $message = null, ?int $percentage = null): void + { + + if ($this->status === self::STATUS_ACTIVE) { + throw new LogicException('Progress has already been started'); + } + + if ($this->status === self::STATUS_FINISHED) { + throw new LogicException('Progress has already been finished'); + } + + $this->title = $title; + + $this->notify($message); + + $this->status = self::STATUS_ACTIVE; + } + + public function update(?string $message = null, ?int $percentage = null): void + { + if ($this->status === self::STATUS_FINISHED) { + throw new LogicException('Progress has already been finished'); + } + + if ($this->status === self::STATUS_INACTIVE) { + throw new LogicException('Progress has not been started yet'); + } + + $this->notify($message); + } + + public function end(?string $message = null): void + { + if ($this->status === self::STATUS_FINISHED) { + throw new LogicException('Progress has already been finished'); + } + + if ($this->status === self::STATUS_INACTIVE) { + throw new LogicException('Progress has not been started yet'); + } + + $this->notify($message); + + $this->status = self::STATUS_FINISHED; + } + + private function notify(?string $message): void + { + $this->handler->notify( + 'telemetry/event', + new LogMessage( + MessageType::INFO, + $this->title . (empty($message) ? '' : (': ' . $message)), + ), + ); + } +} diff --git a/src/Psalm/Internal/LanguageServer/Client/Progress/Progress.php b/src/Psalm/Internal/LanguageServer/Client/Progress/Progress.php new file mode 100644 index 00000000000..8a1ca5da3a9 --- /dev/null +++ b/src/Psalm/Internal/LanguageServer/Client/Progress/Progress.php @@ -0,0 +1,121 @@ +handler = $handler; + $this->token = $token; + } + + public function begin( + string $title, + ?string $message = null, + ?int $percentage = null + ): void { + if ($this->status === self::STATUS_ACTIVE) { + throw new LogicException('Progress has already been started'); + } + + if ($this->status === self::STATUS_FINISHED) { + throw new LogicException('Progress has already been finished'); + } + + $notification = [ + 'token' => $this->token, + 'value' => [ + 'kind' => 'begin', + 'title' => $title, + ], + ]; + + if ($message !== null) { + $notification['value']['message'] = $message; + } + + if ($percentage !== null) { + $notification['value']['percentage'] = $percentage; + $this->withPercentage = true; + } + + $this->handler->notify('$/progress', $notification); + + $this->status = self::STATUS_ACTIVE; + } + + public function end(?string $message = null): void + { + if ($this->status === self::STATUS_FINISHED) { + throw new LogicException('Progress has already been finished'); + } + + if ($this->status === self::STATUS_INACTIVE) { + throw new LogicException('Progress has not been started yet'); + } + + $notification = [ + 'token' => $this->token, + 'value' => [ + 'kind' => 'end', + ], + ]; + + if ($message !== null) { + $notification['value']['message'] = $message; + } + + $this->handler->notify('$/progress', $notification); + + $this->status = self::STATUS_FINISHED; + } + + public function update(?string $message = null, ?int $percentage = null): void + { + if ($this->status === self::STATUS_FINISHED) { + throw new LogicException('Progress has already been finished'); + } + + if ($this->status === self::STATUS_INACTIVE) { + throw new LogicException('Progress has not been started yet'); + } + + $notification = [ + 'token' => $this->token, + 'value' => [ + 'kind' => 'report', + ], + ]; + + if ($message !== null) { + $notification['value']['message'] = $message; + } + + if ($percentage !== null) { + if (!$this->withPercentage) { + throw new LogicException( + 'Cannot update percentage for progress ' + . 'that was started without percentage', + ); + } + $notification['value']['percentage'] = $percentage; + } + + $this->handler->notify('$/progress', $notification); + } +} diff --git a/src/Psalm/Internal/LanguageServer/Client/Progress/ProgressInterface.php b/src/Psalm/Internal/LanguageServer/Client/Progress/ProgressInterface.php new file mode 100644 index 00000000000..78f045c2c53 --- /dev/null +++ b/src/Psalm/Internal/LanguageServer/Client/Progress/ProgressInterface.php @@ -0,0 +1,16 @@ +server->clientCapabilities->window->workDoneProgress ?? false) { + return new Progress($this->handler, $token); + } else { + return new LegacyProgress($this->handler); + } + } + /** * Configuration Refreshed from Client * diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 3fa4261c8d3..cc358753644 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -88,6 +88,7 @@ use function strpos; use function substr; use function trim; +use function uniqid; use function urldecode; use const JSON_PRETTY_PRINT; @@ -377,29 +378,19 @@ public static function run( * The initialize request is sent as the first request from the client to the server. * * @param ClientCapabilities $capabilities The capabilities provided by the client (editor) - * @param int|null $processId The process Id of the parent process that started the server. * Is null if the process has not been started by another process. If the parent process is * not alive then the server should exit (see exit notification) its process. * @param ClientInfo|null $clientInfo Information about the client - * @param string|null $locale The locale the client is currently showing the user interface - * in. This must not necessarily be the locale of the operating - * system. - * @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open. - * @param mixed $initializationOptions * @param string|null $trace The initial trace setting. If omitted trace is disabled ('off'). + * @param string|null $workDoneToken The token to be used to report progress during init. * @psalm-return Promise - * @psalm-suppress PossiblyUnusedParam */ public function initialize( ClientCapabilities $capabilities, - ?int $processId = null, ?ClientInfo $clientInfo = null, - ?string $locale = null, - ?string $rootPath = null, ?string $rootUri = null, - $initializationOptions = null, - ?string $trace = null - //?array $workspaceFolders = null //error in json-dispatcher + ?string $trace = null, + ?string $workDoneToken = null ): Promise { $this->clientInfo = $clientInfo; $this->clientCapabilities = $capabilities; @@ -412,9 +403,11 @@ public function initialize( return call( /** @return Generator */ - function () { + function () use ($workDoneToken) { + $progress = $this->client->makeProgress($workDoneToken ?? uniqid('tkn', true)); + $this->logInfo("Initializing..."); - $this->clientStatus('initializing'); + $progress->begin('Psalm', 'initializing'); // Eventually, this might block on something. Leave it as a generator. /** @psalm-suppress TypeDoesNotContainType */ @@ -425,14 +418,14 @@ function () { $this->project_analyzer->serverMode($this); $this->logInfo("Initializing: Getting code base..."); - $this->clientStatus('initializing', 'getting code base'); + $progress->update('getting code base'); $this->logInfo("Initializing: Scanning files ({$this->project_analyzer->threads} Threads)..."); - $this->clientStatus('initializing', 'scanning files'); + $progress->update('scanning files'); $this->codebase->scanFiles($this->project_analyzer->threads); $this->logInfo("Initializing: Registering stub files..."); - $this->clientStatus('initializing', 'registering stub files'); + $progress->update('registering stub files'); $this->codebase->config->visitStubFiles($this->codebase, $this->project_analyzer->progress); if ($this->textDocument === null) { @@ -572,7 +565,7 @@ function () { } $this->logInfo("Initializing: Complete."); - $this->clientStatus('initialized'); + $progress->end('initialized'); /** * Information about the server. diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index b24862e7460..508af5aeb9f 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -398,10 +398,8 @@ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $po * The code action request is sent from the client to the server to compute commands * for a given text document and range. These commands are typically code fixes to * either fix problems or to beautify/refactor code. - * - * @psalm-suppress PossiblyUnusedParam */ - public function codeAction(TextDocumentIdentifier $textDocument, Range $range, CodeActionContext $context): Promise + public function codeAction(TextDocumentIdentifier $textDocument, CodeActionContext $context): Promise { if (!$this->server->client->clientConfiguration->provideCodeActions) { return new Success(null); diff --git a/src/Psalm/Internal/LanguageServer/Server/Workspace.php b/src/Psalm/Internal/LanguageServer/Server/Workspace.php index 113a8f17974..6d2e1622575 100644 --- a/src/Psalm/Internal/LanguageServer/Server/Workspace.php +++ b/src/Psalm/Internal/LanguageServer/Server/Workspace.php @@ -108,10 +108,9 @@ public function didChangeWatchedFiles(array $changes): void /** * A notification sent from the client to the server to signal the change of configuration settings. * - * @param mixed $settings - * @psalm-suppress PossiblyUnusedMethod, PossiblyUnusedParam + * @psalm-suppress PossiblyUnusedMethod */ - public function didChangeConfiguration($settings): void + public function didChangeConfiguration(): void { $this->server->logDebug( 'workspace/didChangeConfiguration', diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index 0e09aa85a7c..3edc2342bd1 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -27,88 +27,268 @@ public static function getClassLikeNames(): array public static function getMethodReturnType(MethodReturnTypeProviderEvent $event): ?Union { $config = Config::getInstance(); + $method_name_lowercase = $event->getMethodNameLowercase(); + + if (!$config->php_extensions["pdo"]) { + return null; + } + + if ($method_name_lowercase === 'fetch') { + return self::handleFetch($event); + } + + if ($method_name_lowercase === 'fetchall') { + return self::handleFetchAll($event); + } + + return null; + } + + private static function handleFetch(MethodReturnTypeProviderEvent $event): ?Union + { $source = $event->getSource(); $call_args = $event->getCallArgs(); - $method_name_lowercase = $event->getMethodNameLowercase(); - if ($method_name_lowercase === 'fetch' - && $config->php_extensions["pdo"] - && isset($call_args[0]) + $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; + } - switch ($fetch_mode) { - case 2: // PDO::FETCH_ASSOC - array|false - return new Union([ - new TArray([ - Type::getString(), - new Union([ - new TScalar(), - new TNull(), - ]), + switch ($fetch_mode) { + case 2: // PDO::FETCH_ASSOC - array|false + return new Union([ + new TArray([ + Type::getString(), + new Union([ + new TScalar(), + new TNull(), ]), - new TFalse(), - ]); - - case 4: // PDO::FETCH_BOTH - array|false - return new Union([ - new TArray([ - Type::getArrayKey(), - new Union([ - new TScalar(), - new TNull(), + ]), + new TFalse(), + ]); + + case 4: // PDO::FETCH_BOTH - array|false + return new Union([ + new TArray([ + Type::getArrayKey(), + new Union([ + new TScalar(), + new TNull(), + ]), + ]), + new TFalse(), + ]); + + case 6: // PDO::FETCH_BOUND - bool + return Type::getBool(); + + case 7: // PDO::FETCH_COLUMN - scalar|null|false + return new Union([ + new TScalar(), + new TNull(), + new TFalse(), + ]); + + case 8: // PDO::FETCH_CLASS - object|false + return new Union([ + new TObject(), + new TFalse(), + ]); + + case 1: // PDO::FETCH_LAZY - object|false + // This actually returns a PDORow object, but that class is + // undocumented, and its attributes are all dynamic anyway + return new Union([ + new TObject(), + new TFalse(), + ]); + + case 11: // PDO::FETCH_NAMED - array>|false + return new Union([ + new TArray([ + Type::getString(), + new Union([ + new TScalar(), + new TNull(), + Type::getListAtomic( + new Union([ + new TScalar(), + new TNull(), + ]), + ), + ]), + ]), + new TFalse(), + ]); + + case 12: // PDO::FETCH_KEY_PAIR - array + return new Union([ + new TArray([ + Type::getArrayKey(), + new Union([ + new TScalar(), + new TNull(), + ]), + ]), + ]); + + case 3: // PDO::FETCH_NUM - list|false + return new Union([ + Type::getListAtomic( + new Union([ + new TScalar(), + new TNull(), + ]), + ), + new TFalse(), + ]); + + case 5: // PDO::FETCH_OBJ - stdClass|false + return new Union([ + new TNamedObject('stdClass'), + new TFalse(), + ]); + } + + return null; + } + + private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?Union + { + $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; + } + + $fetch_class_name = null; + + if (isset($call_args[1]) + && ($second_arg_type = $source->getNodeTypeProvider()->getType($call_args[1]->value)) + && $second_arg_type->isSingleStringLiteral() + ) { + $fetch_class_name = $second_arg_type->getSingleStringLiteral()->value; + } + + switch ($fetch_mode) { + case 2: // PDO::FETCH_ASSOC - list> + return new Union([ + Type::getListAtomic( + new Union([ + new TArray([ + Type::getString(), + new Union([ + new TScalar(), + new TNull(), + ]), ]), ]), - new TFalse(), - ]); - - case 6: // PDO::FETCH_BOUND - bool - return Type::getBool(); - - case 8: // PDO::FETCH_CLASS - object|false - return new Union([ - new TObject(), - new TFalse(), - ]); - - case 1: // PDO::FETCH_LAZY - object|false - // This actually returns a PDORow object, but that class is - // undocumented, and its attributes are all dynamic anyway - return new Union([ - new TObject(), - new TFalse(), - ]); - - case 11: // PDO::FETCH_NAMED - array>|false - return new Union([ - new TArray([ - Type::getString(), - new Union([ - new TScalar(), - Type::getListAtomic(Type::getScalar()), + ), + ]); + + case 4: // PDO::FETCH_BOTH - list> + return new Union([ + Type::getListAtomic( + new Union([ + new TArray([ + Type::getArrayKey(), + new Union([ + new TScalar(), + new TNull(), + ]), ]), ]), - new TFalse(), - ]); - - case 3: // PDO::FETCH_NUM - list|false - return new Union([ - Type::getListAtomic( - new Union([ - new TScalar(), - new TNull(), + ), + ]); + + case 6: // PDO::FETCH_BOUND - list + return new Union([ + Type::getListAtomic( + Type::getBool(), + ), + ]); + + case 7: // PDO::FETCH_COLUMN - list + return new Union([ + Type::getListAtomic( + new Union([ + new TScalar(), + new TNull(), + ]), + ), + ]); + + case 8: // PDO::FETCH_CLASS - list + return new Union([ + Type::getListAtomic( + new Union([ + $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject(), + ]), + ), + ]); + + case 11: // PDO::FETCH_NAMED - list>> + return new Union([ + Type::getListAtomic( + new Union([ + new TArray([ + Type::getString(), + new Union([ + new TScalar(), + new TNull(), + Type::getListAtomic( + new Union([ + new TScalar(), + new TNull(), + ]), + ), + ]), ]), - ), - new TFalse(), - ]); - - case 5: // PDO::FETCH_OBJ - stdClass|false - return new Union([ - new TNamedObject('stdClass'), - new TFalse(), - ]); - } + ]), + ), + ]); + + case 12: // PDO::FETCH_KEY_PAIR - array + return new Union([ + new TArray([ + Type::getArrayKey(), + new Union([ + new TScalar(), + new TNull(), + ]), + ]), + ]); + + case 3: // PDO::FETCH_NUM - list> + return new Union([ + Type::getListAtomic( + new Union([ + Type::getListAtomic( + new Union([ + new TScalar(), + new TNull(), + ]), + ), + ]), + ), + ]); + + case 5: // PDO::FETCH_OBJ - list + return new Union([ + Type::getListAtomic( + new Union([ + new TNamedObject('stdClass'), + ]), + ), + ]); } return null; diff --git a/tests/AnnotationTest.php b/tests/AnnotationTest.php index c6f0f0bc47c..b06bd217e7f 100644 --- a/tests/AnnotationTest.php +++ b/tests/AnnotationTest.php @@ -25,17 +25,28 @@ public function setUp(): void public function testLessSpecificImplementedReturnTypeWithDocblockOnMultipleLines(): void { $this->expectException(CodeException::class); - $this->expectExceptionMessage('LessSpecificImplementedReturnType - somefile.php:5:'); + $this->expectExceptionMessage('LessSpecificImplementedReturnType - somefile.php:16:'); $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); @@ -50,23 +61,23 @@ public function testLessSpecificImplementedReturnTypeWithDocblockOnMultipleLines 'somefile.php', 'analyzeFile('somefile.php', new Context()); @@ -75,19 +86,31 @@ class BreakingThings extends ParentClass public function testLessSpecificImplementedReturnTypeWithDescription(): void { $this->expectException(CodeException::class); - $this->expectExceptionMessage('LessSpecificImplementedReturnType - somefile.php:7:'); + $this->expectExceptionMessage('LessSpecificImplementedReturnType - somefile.php:19:'); $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); diff --git a/tests/DateTimeTest.php b/tests/DateTimeTest.php index ce9e926ff3a..d8c60882079 100644 --- a/tests/DateTimeTest.php +++ b/tests/DateTimeTest.php @@ -2,12 +2,82 @@ namespace Psalm\Tests; +use Exception; +use Psalm\Context; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; +use const PHP_VERSION_ID; + class DateTimeTest extends TestCase { use ValidCodeAnalysisTestTrait; + public function testModifyWithInvalidConstant(): void + { + $context = new Context(); + + if (PHP_VERSION_ID >= 8_03_00) { + $this->expectException(Exception::class); + $this->expectExceptionMessage('DateTime::modify(): Failed to parse time string (foo) at position 0 (f)'); + } + + $this->addFile( + 'somefile.php', + 'modify(getString()); + $b = $dateTimeImmutable->modify(getString());', + ); + + $this->analyzeFile('somefile.php', $context); + + $this->assertSame('false', $context->vars_in_scope['$a']->getId(true)); + $this->assertSame('false', $context->vars_in_scope['$b']->getId(true)); + } + + public function testModifyWithBothConstant(): void + { + $context = new Context(); + + if (PHP_VERSION_ID >= 8_03_00) { + $this->expectException(Exception::class); + $this->expectExceptionMessage('DateTime::modify(): Failed to parse time string (bar) at position 0 (b)'); + } + + $this->addFile( + 'somefile.php', + 'modify(getString()); + $b = $dateTimeImmutable->modify(getString());', + ); + + $this->analyzeFile('somefile.php', $context); + + $this->assertSame('DateTime|false', $context->vars_in_scope['$a']->getId(false)); + $this->assertSame('DateTimeImmutable|false', $context->vars_in_scope['$b']->getId(false)); + } + public function providerValidCodeParse(): iterable { return [ @@ -48,46 +118,6 @@ function getString(): string '$b' => 'DateTimeImmutable', ], ], - 'modifyWithInvalidConstant' => [ - 'code' => 'modify(getString()); - $b = $dateTimeImmutable->modify(getString()); - ', - 'assertions' => [ - '$a' => 'false', - '$b' => 'false', - ], - ], - 'modifyWithBothConstant' => [ - 'code' => 'modify(getString()); - $b = $dateTimeImmutable->modify(getString()); - ', - 'assertions' => [ - '$a' => 'DateTime|false', - '$b' => 'DateTimeImmutable|false', - ], - ], 'otherMethodAfterModify' => [ 'code' => '> */ private static array $ignoredReturnTypeOnlyFunctions = [ - 'appenditerator::getinneriterator' => ['8.1', '8.2'], - 'appenditerator::getiteratorindex' => ['8.1', '8.2'], - 'arrayobject::getiterator' => ['8.1', '8.2'], - 'cachingiterator::getinneriterator' => ['8.1', '8.2'], - 'callbackfilteriterator::getinneriterator' => ['8.1', '8.2'], + 'appenditerator::getinneriterator' => ['8.1', '8.2', '8.3'], + 'appenditerator::getiteratorindex' => ['8.1', '8.2', '8.3'], + 'arrayobject::getiterator' => ['8.1', '8.2', '8.3'], + 'cachingiterator::getinneriterator' => ['8.1', '8.2', '8.3'], + 'callbackfilteriterator::getinneriterator' => ['8.1', '8.2', '8.3'], 'curl_multi_getcontent', - 'datetime::add' => ['8.1', '8.2'], // DateTime does not contain static - 'datetime::modify' => ['8.1', '8.2'], // DateTime does not contain static - 'datetime::createfromformat' => ['8.1', '8.2'], // DateTime does not contain static + 'datetime::add' => ['8.1', '8.2', '8.3'], // DateTime does not contain static + 'datetime::modify' => ['8.1', '8.2', '8.3'], // DateTime does not contain static + 'datetime::createfromformat' => ['8.1', '8.2', '8.3'], // DateTime does not contain static 'datetime::createfromimmutable' => ['8.1'], 'datetime::createfrominterface', - 'datetime::setdate' => ['8.1', '8.2'], // DateTime does not contain static - 'datetime::setisodate' => ['8.1', '8.2'], // DateTime does not contain static - 'datetime::settime' => ['8.1', '8.2'], // DateTime does not contain static - 'datetime::settimestamp' => ['8.1', '8.2'], // DateTime does not contain static - 'datetime::settimezone' => ['8.1', '8.2'], // DateTime does not contain static - 'datetime::sub' => ['8.1', '8.2'], // DateTime does not contain static + 'datetime::setdate' => ['8.1', '8.2', '8.3'], // DateTime does not contain static + 'datetime::setisodate' => ['8.1', '8.2', '8.3'], // DateTime does not contain static + 'datetime::settime' => ['8.1', '8.2', '8.3'], // DateTime does not contain static + 'datetime::settimestamp' => ['8.1', '8.2', '8.3'], // DateTime does not contain static + 'datetime::settimezone' => ['8.1', '8.2', '8.3'], // DateTime does not contain static + 'datetime::sub' => ['8.1', '8.2', '8.3'], // DateTime does not contain static 'datetimeimmutable::createfrominterface', 'fiber::getcurrent', - 'filteriterator::getinneriterator' => ['8.1', '8.2'], + 'filteriterator::getinneriterator' => ['8.1', '8.2', '8.3'], 'get_cfg_var', // Ignore array return type - 'infiniteiterator::getinneriterator' => ['8.1', '8.2'], - 'iteratoriterator::getinneriterator' => ['8.1', '8.2'], - 'limititerator::getinneriterator' => ['8.1', '8.2'], - 'locale::canonicalize' => ['8.1', '8.2'], - 'locale::getallvariants' => ['8.1', '8.2'], - 'locale::getkeywords' => ['8.1', '8.2'], - 'locale::getprimarylanguage' => ['8.1', '8.2'], - 'locale::getregion' => ['8.1', '8.2'], - 'locale::getscript' => ['8.1', '8.2'], - 'locale::parselocale' => ['8.1', '8.2'], - 'messageformatter::create' => ['8.1', '8.2'], - 'multipleiterator::current' => ['8.1', '8.2'], - 'mysqli::get_charset' => ['8.1', '8.2'], - 'mysqli_stmt::get_warnings' => ['8.1', '8.2'], + 'infiniteiterator::getinneriterator' => ['8.1', '8.2', '8.3'], + 'iteratoriterator::getinneriterator' => ['8.1', '8.2', '8.3'], + 'limititerator::getinneriterator' => ['8.1', '8.2', '8.3'], + 'locale::canonicalize' => ['8.1', '8.2', '8.3'], + 'locale::getallvariants' => ['8.1', '8.2', '8.3'], + 'locale::getkeywords' => ['8.1', '8.2', '8.3'], + 'locale::getprimarylanguage' => ['8.1', '8.2', '8.3'], + 'locale::getregion' => ['8.1', '8.2', '8.3'], + 'locale::getscript' => ['8.1', '8.2', '8.3'], + 'locale::parselocale' => ['8.1', '8.2', '8.3'], + 'messageformatter::create' => ['8.1', '8.2', '8.3'], + 'multipleiterator::current' => ['8.1', '8.2', '8.3'], + 'mysqli::get_charset' => ['8.1', '8.2', '8.3'], + 'mysqli_stmt::get_warnings' => ['8.1', '8.2', '8.3'], 'mysqli_stmt_get_warnings', 'mysqli_stmt_insert_id', - 'norewinditerator::getinneriterator' => ['8.1', '8.2'], + 'norewinditerator::getinneriterator' => ['8.1', '8.2', '8.3'], 'passthru', - 'recursivecachingiterator::getinneriterator' => ['8.1', '8.2'], - 'recursivecallbackfilteriterator::getinneriterator' => ['8.1', '8.2'], - 'recursivefilteriterator::getinneriterator' => ['8.1', '8.2'], - 'recursiveregexiterator::getinneriterator' => ['8.1', '8.2'], + 'recursivecachingiterator::getinneriterator' => ['8.1', '8.2', '8.3'], + 'recursivecallbackfilteriterator::getinneriterator' => ['8.1', '8.2', '8.3'], + 'recursivefilteriterator::getinneriterator' => ['8.1', '8.2', '8.3'], + 'recursiveregexiterator::getinneriterator' => ['8.1', '8.2', '8.3'], 'reflectionclass::getstaticproperties' => ['8.1', '8.2'], - 'reflectionclass::newinstanceargs' => ['8.1', '8.2'], - 'reflectionfunction::getclosurescopeclass' => ['8.1', '8.2'], - 'reflectionfunction::getclosurethis' => ['8.1', '8.2'], - 'reflectionmethod::getclosurescopeclass' => ['8.1', '8.2'], - 'reflectionmethod::getclosurethis' => ['8.1', '8.2'], + 'reflectionclass::newinstanceargs' => ['8.1', '8.2', '8.3'], + 'reflectionfunction::getclosurescopeclass' => ['8.1', '8.2', '8.3'], + 'reflectionfunction::getclosurethis' => ['8.1', '8.2', '8.3'], + 'reflectionmethod::getclosurescopeclass' => ['8.1', '8.2', '8.3'], + 'reflectionmethod::getclosurethis' => ['8.1', '8.2', '8.3'], 'reflectionobject::getstaticproperties' => ['8.1', '8.2'], - 'reflectionobject::newinstanceargs' => ['8.1', '8.2'], - 'regexiterator::getinneriterator' => ['8.1', '8.2'], + 'reflectionobject::newinstanceargs' => ['8.1', '8.2', '8.3'], + 'regexiterator::getinneriterator' => ['8.1', '8.2', '8.3'], 'register_shutdown_function' => ['8.0', '8.1'], - 'splfileobject::fscanf' => ['8.1', '8.2'], - 'spltempfileobject::fscanf' => ['8.1', '8.2'], - 'xsltprocessor::transformtoxml' => ['8.1', '8.2'], + 'splfileobject::fscanf' => ['8.1', '8.2', '8.3'], + 'spltempfileobject::fscanf' => ['8.1', '8.2', '8.3'], + 'xsltprocessor::transformtoxml' => ['8.1', '8.2', '8.3'], ]; /** diff --git a/tests/LanguageServer/DiagnosticTest.php b/tests/LanguageServer/DiagnosticTest.php index b40ef38ace3..d8814ec3633 100644 --- a/tests/LanguageServer/DiagnosticTest.php +++ b/tests/LanguageServer/DiagnosticTest.php @@ -91,10 +91,21 @@ public function testSnippetSupportDisabled(): void ); $write->on('message', function (Message $message) use ($deferred, $server): void { - /** @psalm-suppress PossiblyNullPropertyFetch,UndefinedPropertyFetch,MixedPropertyFetch */ - if ($message->body->method === 'telemetry/event' && $message->body->params->message === 'initialized') { + /** @psalm-suppress NullPropertyFetch,PossiblyNullPropertyFetch,UndefinedPropertyFetch */ + if ($message->body->method === 'telemetry/event' && ($message->body->params->message ?? null) === 'initialized') { $this->assertFalse($server->clientCapabilities->textDocument->completion->completionItem->snippetSupport); $deferred->resolve(null); + return; + } + + /** @psalm-suppress NullPropertyFetch,PossiblyNullPropertyFetch */ + if ($message->body->method === '$/progress' + && ($message->body->params->value->kind ?? null) === 'end' + && ($message->body->params->value->message ?? null) === 'initialized' + ) { + $this->assertFalse($server->clientCapabilities->textDocument->completion->completionItem->snippetSupport); + $deferred->resolve(null); + return; } }); diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 3b656752bcf..f6cb3e75099 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -503,14 +503,48 @@ class A { /** @var ?string */ public $a; } + class B extends A {} $db = new PDO("sqlite::memory:"); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $stmt = $db->prepare("select \"a\" as a"); $stmt->setFetchMode(PDO::FETCH_CLASS, A::class); + $stmt2 = $db->prepare("select \"a\" as a"); + $stmt2->setFetchMode(PDO::FETCH_ASSOC); + $stmt3 = $db->prepare("select \"a\" as a"); + $stmt3->setFetchMode(PDO::ATTR_DEFAULT_FETCH_MODE); $stmt->execute(); + $stmt2->execute(); + /** @psalm-suppress MixedAssignment */ + $a = $stmt->fetch(); + $b = $stmt->fetchAll(); + $c = $stmt->fetch(PDO::FETCH_CLASS); + $d = $stmt->fetchAll(PDO::FETCH_CLASS); + $e = $stmt->fetchAll(PDO::FETCH_CLASS, B::class); + $f = $stmt->fetch(PDO::FETCH_ASSOC); + $g = $stmt->fetchAll(PDO::FETCH_ASSOC); + /** @psalm-suppress MixedAssignment */ + $h = $stmt2->fetch(); + $i = $stmt2->fetchAll(); + $j = $stmt2->fetch(PDO::FETCH_BOTH); + $k = $stmt2->fetchAll(PDO::FETCH_BOTH); /** @psalm-suppress MixedAssignment */ - $a = $stmt->fetch();', + $l = $stmt3->fetch();', + 'assertions' => [ + '$a' => 'mixed', + '$b' => 'array|false', + '$c' => 'false|object', + '$d' => 'list', + '$e' => 'list', + '$f' => 'array|false', + '$g' => 'list>', + '$h' => 'mixed', + '$i' => 'array|false', + '$j' => 'array|false', + '$k' => 'list>', + '$l' => 'mixed', + ], ], 'datePeriodConstructor' => [ 'code' => ' [ + 'code' => 'prepare("SELECT 1"); + $sth->execute(); + return $sth->fetch(PDO::FETCH_COLUMN); + }', + ], + 'pdoStatementFetchAllColumn' => [ + 'code' => ' */ + function fetch_column() { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_COLUMN); + }', + ], + 'pdoStatementFetchKeyPair' => [ + 'code' => ' */ + function fetch_column() { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetch(PDO::FETCH_KEY_PAIR); + }', + ], + 'pdoStatementFetchAllKeyPair' => [ + 'code' => ' */ + function fetch_column() { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_KEY_PAIR); + }', + ], 'pdoStatementFetchAssoc' => [ 'code' => '|false */ @@ -617,6 +691,16 @@ function fetch_assoc() { return $sth->fetch(PDO::FETCH_ASSOC); }', ], + 'pdoStatementFetchAllAssoc' => [ + 'code' => '> */ + function fetch_assoc() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_ASSOC); + }', + ], 'pdoStatementFetchBoth' => [ 'code' => '|false */ @@ -627,6 +711,16 @@ function fetch_both() { return $sth->fetch(PDO::FETCH_BOTH); }', ], + 'pdoStatementFetchAllBoth' => [ + 'code' => '> */ + function fetch_both() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_BOTH); + }', + ], 'pdoStatementFetchBound' => [ 'code' => 'fetch(PDO::FETCH_BOUND); }', ], + 'pdoStatementFetchAllBound' => [ + 'code' => ' */ + function fetch_both() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_BOUND); + }', + ], 'pdoStatementFetchClass' => [ 'code' => 'fetch(PDO::FETCH_CLASS); }', ], + 'pdoStatementFetchAllClass' => [ + 'code' => ' */ + function fetch_class() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_CLASS); + }', + ], + 'pdoStatementFetchAllNamedClass' => [ + 'code' => ' */ + function fetch_class() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_CLASS, Foo::class); + }', + ], 'pdoStatementFetchLazy' => [ 'code' => ' [ 'code' => '>|false */ + /** @return array>|false */ function fetch_named() { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); @@ -667,6 +793,16 @@ function fetch_named() { return $sth->fetch(PDO::FETCH_NAMED); }', ], + 'pdoStatementFetchAllNamed' => [ + 'code' => '>> */ + function fetch_named() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_NAMED); + }', + ], 'pdoStatementFetchNum' => [ 'code' => '|false */ @@ -677,6 +813,16 @@ function fetch_named() { return $sth->fetch(PDO::FETCH_NUM); }', ], + 'pdoStatementFetchAllNum' => [ + 'code' => '> */ + function fetch_named() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_NUM); + }', + ], 'pdoStatementFetchObj' => [ 'code' => 'fetch(PDO::FETCH_OBJ); }', ], + 'pdoStatementFetchAllObj' => [ + 'code' => ' */ + function fetch_named() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_OBJ); + }', + ], 'dateTimeSecondArg' => [ 'code' => 'enterServerMode(); $codebase->config->visitPreloadedStubFiles($codebase); + // avoid MethodSignatureMismatch for __unserialize/() when extending DateTime + if (PHP_VERSION_ID >= 8_02_00) { + $this->addStubFile( + 'stubOne.phpstub', + 'addFile($file_path, $code);