diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..5ace4600a1f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/build-phar.yml b/.github/workflows/build-phar.yml index e5bd222aff7..357a36011bf 100644 --- a/.github/workflows/build-phar.yml +++ b/.github/workflows/build-phar.yml @@ -15,7 +15,7 @@ jobs: should_skip: ${{ steps.skip_check.outputs.should_skip }} steps: - id: skip_check - uses: fkirc/skip-duplicate-actions@v3.4.0 + uses: fkirc/skip-duplicate-actions@v4.0.0 with: concurrent_skipping: always cancel_others: true @@ -35,7 +35,7 @@ jobs: tools: composer:v2 coverage: none - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Get Composer Cache Directories id: composer-cache @@ -44,7 +44,7 @@ jobs: echo "::set-output name=vcs_cache::$(composer config cache-vcs-dir)" - name: Cache composer cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | ${{ steps.composer-cache.outputs.files_cache }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea9ac8fdc10..b2a300e7a45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,9 @@ name: Run unit tests on: [push, pull_request] +permissions: + contents: read + jobs: lint: name: Check PHP syntax @@ -13,7 +16,7 @@ jobs: tools: composer:v2 coverage: none - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Get Composer Cache Directories id: composer-cache @@ -22,7 +25,7 @@ jobs: echo "::set-output name=vcs_cache::$(composer config cache-vcs-dir)" - name: Cache composer cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | ${{ steps.composer-cache.outputs.files_cache }} @@ -32,13 +35,15 @@ jobs: ${{ runner.os }}-composer- - name: Run composer install - run: composer install -o --ignore-platform-reqs + run: composer install -o env: COMPOSER_ROOT_VERSION: dev-master - run: | git ls-files | grep \\\.php$ | grep -v ^dictionaries/scripts/* | ./vendor/bin/parallel-lint --stdin chunk-matrix: + permissions: + contents: none name: Generate Chunk Matrix runs-on: ubuntu-latest @@ -78,13 +83,13 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.0' + php-version: '8.1' ini-values: zend.assertions=1, assert.exception=1 tools: composer:v2 coverage: none extensions: none, curl, dom, filter, json, libxml, mbstring, openssl, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Get Composer Cache Directories id: composer-cache @@ -93,7 +98,7 @@ jobs: echo "::set-output name=vcs_cache::$(composer config cache-vcs-dir)" - name: Cache composer cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | ${{ steps.composer-cache.outputs.files_cache }} diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index 30761023d08..eabbeac0b1f 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -6,7 +6,7 @@ jobs: label: runs-on: ubuntu-latest steps: - - uses: mheap/github-action-required-labels@v1 + - uses: mheap/github-action-required-labels@v2 with: mode: minimum count: 1 diff --git a/.github/workflows/shepherd.yml b/.github/workflows/shepherd.yml index 7a900c56e71..f8cf2780013 100644 --- a/.github/workflows/shepherd.yml +++ b/.github/workflows/shepherd.yml @@ -2,15 +2,18 @@ name: Run Shepherd on: [push, pull_request] +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.0' - name: Install dependencies run: composer install --prefer-dist --no-progress --no-suggest diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index 3f8842ad797..4450dfb8606 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -2,8 +2,13 @@ name: Run unit tests on Windows on: [push, pull_request] +permissions: + contents: read + jobs: chunk-matrix: + permissions: + contents: none name: Generate Chunk Matrix runs-on: ubuntu-latest @@ -54,7 +59,7 @@ jobs: coverage: none extensions: none, curl, dom, filter, json, libxml, mbstring, openssl, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Get Composer Cache Directories id: composer-cache @@ -63,7 +68,7 @@ jobs: echo "::set-output name=vcs_cache::$(composer config cache-vcs-dir)" - name: Cache composer cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | ${{ steps.composer-cache.outputs.files_cache }} diff --git a/composer.json b/composer.json index b3c2f13dda1..3b9b3ef4c3b 100644 --- a/composer.json +++ b/composer.json @@ -109,6 +109,7 @@ "lint": "parallel-lint ./src ./tests", "phpunit": "paratest --runner=WrapperRunner", "phpunit-std": "phpunit", + "verify-callmap": "phpunit tests/Internal/Codebase/InternalCallMapHandlerTest.php", "psalm": "@php ./psalm --find-dead-code", "tests": [ "@lint", diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index d0f5a6f9e5f..71095ee5b78 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -361,12 +361,12 @@ 'ArithmeticError::getTraceAsString' => ['string'], 'array_change_key_case' => ['associative-array', 'array'=>'array', 'case='=>'int'], 'array_chunk' => ['list', 'array'=>'array', 'length'=>'int', 'preserve_keys='=>'bool'], -'array_column' => ['array', 'array'=>'array', 'column_key'=>'mixed', 'index_key='=>'mixed'], +'array_column' => ['array', 'array'=>'array', 'column_key'=>'int|string|null', 'index_key='=>'int|string|null'], 'array_combine' => ['associative-array', 'keys'=>'string[]|int[]', 'values'=>'array'], 'array_count_values' => ['associative-array', 'array'=>'array'], -'array_diff' => ['associative-array', 'array'=>'array', 'arrays'=>'array', '...args='=>'array'], -'array_diff_assoc' => ['associative-array', 'array'=>'array', 'arrays'=>'array', '...args='=>'array'], -'array_diff_key' => ['associative-array', 'array'=>'array', 'arrays'=>'array', '...args='=>'array'], +'array_diff' => ['associative-array', 'array'=>'array', '...arrays='=>'array'], +'array_diff_assoc' => ['associative-array', 'array'=>'array', '...arrays='=>'array'], +'array_diff_key' => ['associative-array', 'array'=>'array', '...arrays='=>'array'], 'array_diff_uassoc' => ['associative-array', 'array'=>'array', 'rest'=>'array', 'data_comp_func'=>'callable(mixed,mixed):int'], 'array_diff_uassoc\'1' => ['associative-array', 'array'=>'array', 'rest'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'], 'array_diff_ukey' => ['associative-array', 'array'=>'array', 'rest'=>'array', 'key_comp_func'=>'callable(mixed,mixed):int'], @@ -375,31 +375,31 @@ 'array_fill_keys' => ['array', 'keys'=>'array', 'value'=>'mixed'], 'array_filter' => ['associative-array', 'array'=>'array', 'callback='=>'callable(mixed,mixed=):scalar', 'mode='=>'int'], 'array_flip' => ['associative-array|associative-array', 'array'=>'array'], -'array_intersect' => ['associative-array', 'array'=>'array', 'arrays'=>'array', '...args='=>'array'], -'array_intersect_assoc' => ['associative-array', 'array'=>'array', 'arrays'=>'array', '...args='=>'array'], -'array_intersect_key' => ['associative-array', 'array'=>'array', 'arrays'=>'array', '...args='=>'array'], +'array_intersect' => ['associative-array', 'array'=>'array', '...arrays='=>'array'], +'array_intersect_assoc' => ['associative-array', 'array'=>'array', '...arrays='=>'array'], +'array_intersect_key' => ['associative-array', 'array'=>'array', '...arrays='=>'array'], 'array_intersect_uassoc' => ['associative-array', 'array'=>'array', 'rest'=>'array', 'key_compare_func'=>'callable(mixed,mixed):int'], 'array_intersect_uassoc\'1' => ['associative-array', 'array'=>'array', 'rest'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest'=>'array|callable(mixed,mixed):int'], 'array_intersect_ukey' => ['associative-array', 'array'=>'array', 'rest'=>'array', 'key_compare_func'=>'callable(mixed,mixed):int'], 'array_intersect_ukey\'1' => ['associative-array', 'array'=>'array', 'rest'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest'=>'array|callable(mixed,mixed):int'], 'array_is_list' => ['bool', 'array'=>'array'], -'array_key_exists' => ['bool', 'key'=>'string|int', 'array'=>'array|ArrayObject'], +'array_key_exists' => ['bool', 'key'=>'string|int', 'array'=>'array'], 'array_key_first' => ['int|string|null', 'array'=>'array'], 'array_key_last' => ['int|string|null', 'array'=>'array'], 'array_keys' => ['list', 'array'=>'array', 'filter_value='=>'mixed', 'strict='=>'bool'], 'array_map' => ['array', 'callback'=>'?callable', 'array'=>'array', '...arrays='=>'array'], -'array_merge' => ['array', 'arrays'=>'array', '...args='=>'array'], -'array_merge_recursive' => ['array', 'arrays'=>'array', '...args='=>'array'], +'array_merge' => ['array', '...arrays='=>'array'], +'array_merge_recursive' => ['array', '...arrays='=>'array'], 'array_multisort' => ['bool', '&rw_array'=>'array', 'rest='=>'array|int', 'array1_sort_flags='=>'array|int', '...args='=>'array|int'], 'array_pad' => ['array', 'array'=>'array', 'length'=>'int', 'value'=>'mixed'], 'array_pop' => ['mixed', '&rw_array'=>'array'], 'array_product' => ['int|float', 'array'=>'array'], -'array_push' => ['int', '&rw_array'=>'array', 'values'=>'mixed', '...vars='=>'mixed'], +'array_push' => ['int', '&rw_array'=>'array', '...values='=>'mixed'], 'array_rand' => ['int|string|array|array', 'array'=>'non-empty-array', 'num'=>'int'], 'array_rand\'1' => ['int|string', 'array'=>'array'], 'array_reduce' => ['mixed', 'array'=>'array', 'callback'=>'callable(mixed,mixed):mixed', 'initial='=>'mixed'], -'array_replace' => ['array', 'array'=>'array', 'replacements'=>'array', '...args='=>'array'], -'array_replace_recursive' => ['array', 'array'=>'array', 'replacements'=>'array', '...args='=>'array'], +'array_replace' => ['array', 'array'=>'array', '...replacements='=>'array'], +'array_replace_recursive' => ['array', 'array'=>'array', '...replacements='=>'array'], 'array_reverse' => ['array', 'array'=>'array', 'preserve_keys='=>'bool'], 'array_search' => ['int|string|false', 'needle'=>'mixed', 'haystack'=>'array', 'strict='=>'bool'], 'array_shift' => ['mixed|null', '&rw_array'=>'array'], @@ -421,7 +421,7 @@ 'array_unique' => ['array', 'array'=>'array', 'flags='=>'0'], 'array_unique\'1' => ['array', 'array'=>'array', 'flags='=>'1'], 'array_unique\'2' => ['array', 'array'=>'array', 'flags='=>'2|5'], -'array_unshift' => ['int', '&rw_array'=>'array', 'values'=>'mixed', '...vars='=>'mixed'], +'array_unshift' => ['int', '&rw_array'=>'array', '...values='=>'mixed'], 'array_values' => ['list', 'array'=>'array'], 'array_walk' => ['bool', '&rw_array'=>'array', 'callback'=>'callable', 'arg='=>'mixed'], 'array_walk\'1' => ['bool', '&rw_array'=>'object', 'callback'=>'callable', 'arg='=>'mixed'], @@ -1543,7 +1543,7 @@ 'Couchbase\WildcardSearchQuery::jsonSerialize' => ['array'], 'Couchbase\zlibCompress' => ['string', 'data'=>'string'], 'Couchbase\zlibDecompress' => ['string', 'data'=>'string'], -'count' => ['int', 'value'=>'Countable|array|SimpleXMLElement|ResourceBundle', 'mode='=>'int'], +'count' => ['int', 'value'=>'Countable|array', 'mode='=>'int'], 'count_chars' => ['array', 'input'=>'string', 'mode='=>'0|1|2'], 'count_chars\'1' => ['string', 'input'=>'string', 'mode='=>'3|4'], 'Countable::count' => ['int'], @@ -1740,7 +1740,7 @@ 'date_timestamp_set' => ['DateTime|false', 'object'=>'DateTime', 'timestamp'=>'int'], 'date_timezone_get' => ['DateTimeZone|false', 'object'=>'DateTimeInterface'], 'date_timezone_set' => ['DateTime|false', 'object'=>'DateTime', 'timezone'=>'DateTimeZone'], -'datefmt_create' => ['IntlDateFormatter|false', 'locale'=>'?string', 'dateType'=>'?int', 'timeType'=>'?int', 'timezone='=>'string|DateTimeZone|IntlTimeZone|null', 'calendar='=>'int|IntlCalendar|null', 'pattern='=>'string'], +'datefmt_create' => ['?IntlDateFormatter', 'locale'=>'?string', 'dateType='=>'int', 'timeType='=>'int', 'timezone='=>'DateTimeZone|IntlTimeZone|string|null', 'calendar='=>'IntlCalendar|int|null', 'pattern='=>'?string'], 'datefmt_format' => ['string|false', 'formatter'=>'IntlDateFormatter', 'datetime'=>'DateTime|IntlCalendar|array|int'], 'datefmt_format_object' => ['string|false', 'datetime'=>'object', 'format='=>'mixed', 'locale='=>'string'], 'datefmt_get_calendar' => ['int', 'formatter'=>'IntlDateFormatter'], @@ -1751,16 +1751,15 @@ 'datefmt_get_locale' => ['string|false', 'formatter'=>'IntlDateFormatter', 'type='=>'int'], 'datefmt_get_pattern' => ['string', 'formatter'=>'IntlDateFormatter'], 'datefmt_get_timetype' => ['int', 'formatter'=>'IntlDateFormatter'], -'datefmt_get_timezone' => ['IntlTimeZone|false'], -'datefmt_get_timezone_id' => ['string', 'formatter'=>'IntlDateFormatter'], +'datefmt_get_timezone' => ['IntlTimeZone|false', 'formatter'=>'IntlDateFormatter'], +'datefmt_get_timezone_id' => ['string|false', 'formatter'=>'IntlDateFormatter'], 'datefmt_is_lenient' => ['bool', 'formatter'=>'IntlDateFormatter'], -'datefmt_localtime' => ['array|false', 'formatter'=>'IntlDateFormatter', 'string='=>'string', '&rw_offset='=>'int'], -'datefmt_parse' => ['int|false', 'formatter'=>'IntlDateFormatter', 'string='=>'string', '&rw_offset='=>'int'], +'datefmt_localtime' => ['array|false', 'formatter'=>'IntlDateFormatter', 'string'=>'string', '&rw_offset='=>'int'], +'datefmt_parse' => ['int|false', 'formatter'=>'IntlDateFormatter', 'string'=>'string', '&rw_offset='=>'int'], 'datefmt_set_calendar' => ['bool', 'formatter'=>'IntlDateFormatter', 'calendar'=>'int'], 'datefmt_set_lenient' => ['?bool', 'formatter'=>'IntlDateFormatter', 'lenient'=>'bool'], 'datefmt_set_pattern' => ['bool', 'formatter'=>'IntlDateFormatter', 'pattern'=>'string'], -'datefmt_set_timezone' => ['bool', 'formatter'=>'mixed'], -'datefmt_set_timezone_id' => ['bool', 'fmt'=>'IntlDateFormatter', 'zone'=>'string'], +'datefmt_set_timezone' => ['false|null', 'formatter'=>'IntlDateFormatter', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], 'DateInterval::__construct' => ['void', 'spec'=>'string'], 'DateInterval::__set_state' => ['DateInterval', 'array'=>'array'], 'DateInterval::__wakeup' => ['void'], @@ -2521,7 +2520,7 @@ 'enchant_dict_store_replacement' => ['void', 'dictionary'=>'resource', 'misspelled'=>'string', 'correct'=>'string'], 'enchant_dict_suggest' => ['array', 'dictionary'=>'resource', 'word'=>'string'], 'end' => ['mixed|false', '&r_array'=>'array|object'], -'enum_exists' => ['bool', 'class' => 'string', 'autoload=' => 'bool'], +'enum_exists' => ['bool', 'enum' => 'string', 'autoload=' => 'bool'], 'Error::__clone' => ['void'], 'Error::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable|?Error'], 'Error::__toString' => ['string'], @@ -2927,7 +2926,7 @@ 'explode' => ['list', 'separator'=>'string', 'string'=>'string', 'limit='=>'int'], 'expm1' => ['float', 'num'=>'float'], 'extension_loaded' => ['bool', 'extension'=>'string'], -'extract' => ['int', '&rw_array'=>'array', 'flags='=>'int', 'prefix='=>'?string'], +'extract' => ['int', '&rw_array'=>'array', 'flags='=>'int', 'prefix='=>'string'], 'ezmlm_hash' => ['int', 'addr'=>'string'], 'fam_cancel_monitor' => ['bool', 'fam'=>'resource', 'fam_monitor'=>'resource'], 'fam_close' => ['void', 'fam'=>'resource'], @@ -3292,8 +3291,8 @@ 'filter_input' => ['mixed|false', 'type'=>'int', 'var_name'=>'string', 'filter='=>'int', 'options='=>'array|int'], 'filter_input_array' => ['mixed|false', 'type'=>'int', 'options='=>'int|array', 'add_empty='=>'bool'], 'filter_list' => ['array'], -'filter_var' => ['mixed|false', 'value'=>'mixed', 'filter='=>'int', 'options='=>'mixed'], -'filter_var_array' => ['mixed|false', 'array'=>'array', 'options='=>'mixed', 'add_empty='=>'bool'], +'filter_var' => ['mixed|false', 'value'=>'mixed', 'filter='=>'int', 'options='=>'array|int'], +'filter_var_array' => ['array|false|null', 'array'=>'array', 'options='=>'array|int', 'add_empty='=>'bool'], 'FilterIterator::__construct' => ['void', 'iterator'=>'Iterator'], 'FilterIterator::accept' => ['bool'], 'FilterIterator::current' => ['mixed'], @@ -3324,7 +3323,7 @@ 'fpassthru' => ['int|false', 'stream'=>'resource'], 'fpm_get_status' => ['array|false'], 'fprintf' => ['int', 'stream'=>'resource', 'format'=>'string', '...values='=>'string|int|float'], -'fputcsv' => ['int|false', 'stream'=>'resource', 'fields'=>'array', 'separator='=>'string', 'enclosure='=>'string', 'escape='=>'string'], +'fputcsv' => ['int|false', 'stream'=>'resource', 'fields'=>'array', 'separator='=>'string', 'enclosure='=>'string', 'escape='=>'string', 'eol='=>'string'], 'fputs' => ['int|false', 'stream'=>'resource', 'data'=>'string', 'length='=>'int'], 'fread' => ['string|false', 'stream'=>'resource', 'length'=>'int'], 'frenchtojd' => ['int', 'month'=>'int', 'day'=>'int', 'year'=>'int'], @@ -3333,7 +3332,7 @@ 'fscanf\'1' => ['int', 'stream'=>'resource', 'format'=>'string', '&...w_vars='=>'string|int|float'], 'fseek' => ['int', 'stream'=>'resource', 'offset'=>'int', 'whence='=>'int'], 'fsockopen' => ['resource|false', 'hostname'=>'string', 'port='=>'int', '&w_error_code='=>'int', '&w_error_message='=>'string', 'timeout='=>'float'], -'fstat' => ['array|false', 'stream'=>'resource'], +'fstat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'stream'=>'resource'], 'ftell' => ['int|false', 'stream'=>'resource'], 'ftok' => ['int', 'filename'=>'string', 'project_id'=>'string'], 'ftp_alloc' => ['bool', 'ftp'=>'FTP\Connection', 'size'=>'int', '&w_response='=>'string'], @@ -3724,7 +3723,7 @@ 'get_called_class' => ['class-string'], 'get_cfg_var' => ['string|false', 'option'=>'string'], 'get_class' => ['class-string', 'object='=>'object'], -'get_class_methods' => ['list|null', 'object_or_class'=>'mixed'], +'get_class_methods' => ['list', 'object_or_class'=>'object|class-string'], 'get_class_vars' => ['array', 'class'=>'string'], 'get_current_user' => ['string'], 'get_debug_type' => ['string', 'value'=>'mixed'], @@ -3744,7 +3743,7 @@ 'get_magic_quotes_runtime' => ['int|false'], 'get_meta_tags' => ['array', 'filename'=>'string', 'use_include_path='=>'bool'], 'get_object_vars' => ['array', 'object'=>'object'], -'get_parent_class' => ['class-string|false', 'object_or_class='=>'mixed'], +'get_parent_class' => ['class-string|false', 'object_or_class='=>'object|class-string'], 'get_required_files' => ['list'], 'get_resource_id' => ['int', 'resource'=>'resource'], 'get_resource_type' => ['string', 'resource'=>'resource'], @@ -6773,63 +6772,61 @@ 'lchgrp' => ['bool', 'filename'=>'string', 'group'=>'string|int'], 'lchown' => ['bool', 'filename'=>'string', 'user'=>'string|int'], 'ldap_8859_to_t61' => ['string', 'value'=>'string'], -'ldap_add' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'array'], -'ldap_add_ext' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'array'], +'ldap_add' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], +'ldap_add_ext' => ['LDAP\Result|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], 'ldap_bind' => ['bool', 'ldap'=>'LDAP\Connection', 'dn='=>'string|null', 'password='=>'string|null'], -'ldap_bind_ext' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'dn='=>'string|null', 'password='=>'string|null', 'controls='=>'array'], +'ldap_bind_ext' => ['LDAP\Result|false', 'ldap'=>'LDAP\Connection', 'dn='=>'string|null', 'password='=>'string|null', 'controls='=>'?array'], 'ldap_close' => ['bool', 'ldap'=>'LDAP\Connection'], -'ldap_compare' => ['bool|int', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'attribute'=>'string', 'value'=>'string'], +'ldap_compare' => ['bool|int', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'attribute'=>'string', 'value'=>'string', 'controls='=>'?array'], 'ldap_connect' => ['LDAP\Connection|false', 'uri='=>'string', 'port='=>'int', 'wallet='=>'string', 'password='=>'string', 'auth_mode='=>'int'], -'ldap_control_paged_result' => ['bool', 'link_identifier'=>'resource', 'pagesize'=>'int', 'iscritical='=>'bool', 'cookie='=>'string'], -'ldap_control_paged_result_response' => ['bool', 'link_identifier'=>'resource', 'result_identifier'=>'resource', '&w_cookie'=>'string', '&w_estimated'=>'int'], 'ldap_count_entries' => ['int|false', 'ldap'=>'LDAP\Connection', 'result'=>'LDAP\Result'], -'ldap_delete' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string'], -'ldap_delete_ext' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'controls='=>'array'], +'ldap_delete' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'controls='=>'?array'], +'ldap_delete_ext' => ['LDAP\Result|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'controls='=>'?array'], 'ldap_dn2ufn' => ['string', 'dn'=>'string'], 'ldap_err2str' => ['string', 'errno'=>'int'], 'ldap_errno' => ['int', 'ldap'=>'LDAP\Connection'], 'ldap_error' => ['string', 'ldap'=>'LDAP\Connection'], 'ldap_escape' => ['string', 'value'=>'string', 'ignore='=>'string', 'flags='=>'int'], -'ldap_exop' => ['mixed', 'ldap'=>'LDAP\Connection', 'reqoid'=>'string', 'reqdata='=>'string', 'serverctrls='=>'array|null', '&w_response_data='=>'string', '&w_response_oid='=>'string'], +'ldap_exop' => ['mixed', 'ldap'=>'LDAP\Connection', 'request_oid'=>'string', 'request_data='=>'string', 'controls='=>'?array', '&w_response_data='=>'string', '&w_response_oid='=>'string'], 'ldap_exop_passwd' => ['bool|string', 'ldap'=>'LDAP\Connection', 'user='=>'string', 'old_password='=>'string', 'new_password='=>'string', '&w_controls='=>'array|null'], 'ldap_exop_refresh' => ['int|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'ttl'=>'int'], 'ldap_exop_whoami' => ['string|false', 'ldap'=>'LDAP\Connection'], 'ldap_explode_dn' => ['array|false', 'dn'=>'string', 'with_attrib'=>'int'], 'ldap_first_attribute' => ['string|false', 'ldap'=>'LDAP\Connection', 'entry'=>'LDAP\ResultEntry'], -'ldap_first_entry' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'result'=>'LDAP\Result'], -'ldap_first_reference' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'result'=>'LDAP\Result'], -'ldap_free_result' => ['bool', 'ldap'=>'LDAP\Connection'], +'ldap_first_entry' => ['LDAP\ResultEntry|false', 'ldap'=>'LDAP\Connection', 'result'=>'LDAP\Result'], +'ldap_first_reference' => ['LDAP\ResultEntry|false', 'ldap'=>'LDAP\Connection', 'result'=>'LDAP\Result'], +'ldap_free_result' => ['bool', 'result'=>'LDAP\Result'], 'ldap_get_attributes' => ['array|false', 'ldap'=>'LDAP\Connection', 'entry'=>'LDAP\ResultEntry'], 'ldap_get_dn' => ['string|false', 'ldap'=>'LDAP\Connection', 'entry'=>'LDAP\ResultEntry'], 'ldap_get_entries' => ['array|false', 'ldap'=>'LDAP\Connection', 'result'=>'LDAP\Result'], -'ldap_get_option' => ['bool', 'ldap'=>'LDAP\Connection', 'option'=>'int', '&w_value'=>'mixed'], +'ldap_get_option' => ['bool', 'ldap'=>'LDAP\Connection', 'option'=>'int', '&w_value='=>'array|string|int|null'], 'ldap_get_values' => ['array|false', 'ldap'=>'LDAP\Connection', 'entry'=>'LDAP\ResultEntry', 'attribute'=>'string'], 'ldap_get_values_len' => ['array|false', 'ldap'=>'LDAP\Connection', 'entry'=>'LDAP\ResultEntry', 'attribute'=>'string'], -'ldap_list' => ['LDAP\Connection|false', 'ldap'=>'resource|array', 'base'=>'string', 'filter'=>'string', 'attributes='=>'array', 'attributes_only='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int'], -'ldap_mod_add' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array'], -'ldap_mod_add_ext' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'array'], -'ldap_mod_del' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array'], -'ldap_mod_del_ext' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'array'], -'ldap_mod_replace' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array'], -'ldap_mod_replace_ext' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'array'], -'ldap_modify' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array'], -'ldap_modify_batch' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'modifications_info'=>'array'], +'ldap_list' => ['LDAP\Result|LDAP\Result[]|false', 'ldap'=>'LDAP\Connection|LDAP\Connection[]', 'base'=>'string', 'filter'=>'string', 'attributes='=>'array', 'attributes_only='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int', 'controls='=>'?array'], +'ldap_mod_add' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], +'ldap_mod_add_ext' => ['LDAP\Result|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], +'ldap_mod_del' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], +'ldap_mod_del_ext' => ['LDAP\Result|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], +'ldap_mod_replace' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], +'ldap_mod_replace_ext' => ['LDAP\Result|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], +'ldap_modify' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], +'ldap_modify_batch' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'modifications_info'=>'array', 'controls='=>'?array'], 'ldap_next_attribute' => ['string|false', 'ldap'=>'LDAP\Connection', 'entry'=>'LDAP\ResultEntry'], -'ldap_next_entry' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'result'=>'LDAP\Result'], -'ldap_next_reference' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'entry'=>'LDAP\ResultEntry'], +'ldap_next_entry' => ['LDAP\ResultEntry|false', 'ldap'=>'LDAP\Connection', 'entry'=>'LDAP\ResultEntry'], +'ldap_next_reference' => ['LDAP\ResultEntry|false', 'ldap'=>'LDAP\Connection', 'entry'=>'LDAP\ResultEntry'], 'ldap_parse_exop' => ['bool', 'ldap'=>'LDAP\Connection', 'result'=>'LDAP\Result', '&w_response_data='=>'string', '&w_response_oid='=>'string'], -'ldap_parse_reference' => ['bool', 'ldap'=>'LDAP\Connection', 'entry'=>'LDAP\ResultEntry', 'referrals'=>'array'], +'ldap_parse_reference' => ['bool', 'ldap'=>'LDAP\Connection', 'entry'=>'LDAP\ResultEntry', '&w_referrals'=>'array'], 'ldap_parse_result' => ['bool', 'ldap'=>'LDAP\Connection', 'result'=>'LDAP\Result', '&w_error_code'=>'int', '&w_matched_dn='=>'string', '&w_error_message='=>'string', '&w_referrals='=>'array', '&w_controls='=>'array'], -'ldap_read' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection|array', 'base'=>'string', 'filter'=>'string', 'attributes='=>'array', 'attributes_only='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int'], -'ldap_rename' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'new_rdn'=>'string', 'new_parent'=>'string', 'delete_old_rdn'=>'bool'], -'ldap_rename_ext' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'new_rdn'=>'string', 'new_parent'=>'string', 'delete_old_rdn'=>'bool', 'controls='=>'array'], +'ldap_read' => ['LDAP\Result|LDAP\Result[]|false', 'ldap'=>'LDAP\Connection|LDAP\Connection[]', 'base'=>'string', 'filter'=>'string', 'attributes='=>'array', 'attributes_only='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int', 'controls='=>'?array'], +'ldap_rename' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'new_rdn'=>'string', 'new_parent'=>'string', 'delete_old_rdn'=>'bool', 'controls='=>'?array'], +'ldap_rename_ext' => ['LDAP\Result|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'new_rdn'=>'string', 'new_parent'=>'string', 'delete_old_rdn'=>'bool', 'controls='=>'?array'], 'ldap_sasl_bind' => ['bool', 'ldap'=>'LDAP\Connection', 'dn='=>'string', 'password='=>'string', 'mech='=>'string', 'realm='=>'string', 'authc_id='=>'string', 'authz_id='=>'string', 'props='=>'string'], -'ldap_search' => ['LDAP\Result|false', 'ldap'=>'LDAP\Connection|LDAP\Connection[]', 'base'=>'string', 'filter'=>'string', 'attributes='=>'array', 'attributes_only='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int'], +'ldap_search' => ['LDAP\Result|LDAP\Result[]|false', 'ldap'=>'LDAP\Connection|LDAP\Connection[]', 'base'=>'string', 'filter'=>'string', 'attributes='=>'array', 'attributes_only='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int', 'controls='=>'?array'], 'ldap_set_option' => ['bool', 'ldap'=>'LDAP\Connection|null', 'option'=>'int', 'value'=>'mixed'], 'ldap_set_rebind_proc' => ['bool', 'ldap'=>'LDAP\Connection', 'callback'=>'?callable'], -'ldap_start_tls' => ['bool', 'ldap'=>'resource'], +'ldap_start_tls' => ['bool', 'ldap'=>'LDAP\Connection'], 'ldap_t61_to_8859' => ['string', 'value'=>'string'], -'ldap_unbind' => ['bool', 'ldap'=>'resource'], +'ldap_unbind' => ['bool', 'ldap'=>'LDAP\Connection'], 'leak' => ['', 'num_bytes'=>'int'], 'leak_variable' => ['', 'variable'=>'', 'leak_data'=>'bool'], 'legendObj::convertToString' => ['string'], @@ -6959,7 +6956,7 @@ 'LogicException::getTrace' => ['list\',args?:array}>'], 'LogicException::getTraceAsString' => ['string'], 'long2ip' => ['string', 'ip'=>'string|int'], -'lstat' => ['array|false', 'filename'=>'string'], +'lstat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'filename'=>'string'], 'ltrim' => ['string', 'string'=>'string', 'characters='=>'string'], 'Lua::__call' => ['mixed', 'lua_func'=>'callable', 'args='=>'array', 'use_self='=>'int'], 'Lua::__construct' => ['void', 'lua_script_file'=>'string'], @@ -9027,7 +9024,7 @@ 'nsapi_response_headers' => ['array'], 'nsapi_virtual' => ['bool', 'uri'=>'string'], 'nthmac' => ['string', 'clent'=>'string', 'data'=>'string'], -'number_format' => ['string', 'num'=>'float|int', 'decimals='=>'int', 'decimal_separator='=>'string', 'thousands_separator='=>'string'], +'number_format' => ['string', 'num'=>'float|int', 'decimals='=>'int', 'decimal_separator='=>'?string', 'thousands_separator='=>'?string'], 'NumberFormatter::__construct' => ['void', 'locale'=>'string', 'style'=>'int', 'pattern='=>'string'], 'NumberFormatter::create' => ['NumberFormatter|false', 'locale'=>'string', 'style'=>'int', 'pattern='=>'string'], 'NumberFormatter::format' => ['string|false', 'num'=>'', 'type='=>'int'], @@ -9120,7 +9117,7 @@ 'ob_get_status' => ['array', 'full_status='=>'bool'], 'ob_gzhandler' => ['string|false', 'data'=>'string', 'flags'=>'int'], 'ob_iconv_handler' => ['string', 'contents'=>'string', 'status'=>'int'], -'ob_implicit_flush' => ['void', 'enable='=>'int'], +'ob_implicit_flush' => ['void', 'enable='=>'bool'], 'ob_inflatehandler' => ['string', 'data'=>'string', 'mode'=>'int'], 'ob_list_handlers' => ['false|list'], 'ob_start' => ['bool', 'callback='=>'string|array|?callable', 'chunk_size='=>'int', 'flags='=>'int'], @@ -11354,6 +11351,7 @@ 'ReflectionClass::isAbstract' => ['bool'], 'ReflectionClass::isAnonymous' => ['bool'], 'ReflectionClass::isCloneable' => ['bool'], +'ReflectionClass::isEnum' => ['bool'], 'ReflectionClass::isFinal' => ['bool'], 'ReflectionClass::isInstance' => ['bool', 'object'=>'object'], 'ReflectionClass::isInstantiable' => ['bool'], @@ -11455,7 +11453,9 @@ 'ReflectionFunctionAbstract::getShortName' => ['string'], 'ReflectionFunctionAbstract::getStartLine' => ['int|false'], 'ReflectionFunctionAbstract::getStaticVariables' => ['array'], +'ReflectionFunctionAbstract::getTentativeReturnType' => ['?ReflectionType'], 'ReflectionFunctionAbstract::hasReturnType' => ['bool'], +'ReflectionFunctionAbstract::hasTentativeReturnType' => ['bool'], 'ReflectionFunctionAbstract::inNamespace' => ['bool'], 'ReflectionFunctionAbstract::isClosure' => ['bool'], 'ReflectionFunctionAbstract::isDeprecated' => ['bool'], @@ -11560,6 +11560,7 @@ 'ReflectionObject::isAbstract' => ['bool'], 'ReflectionObject::isAnonymous' => ['bool'], 'ReflectionObject::isCloneable' => ['bool'], +'ReflectionObject::isEnum' => ['bool'], 'ReflectionObject::isFinal' => ['bool'], 'ReflectionObject::isInstance' => ['bool', 'object'=>'object'], 'ReflectionObject::isInstantiable' => ['bool'], @@ -13209,15 +13210,15 @@ 'SplFileObject::eof' => ['bool'], 'SplFileObject::fflush' => ['bool'], 'SplFileObject::fgetc' => ['string|false'], -'SplFileObject::fgetcsv' => ['list|array{0: null}|false|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], +'SplFileObject::fgetcsv' => ['list|array{0: null}|false|null', 'seperator='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'SplFileObject::fgets' => ['string|false'], 'SplFileObject::flock' => ['bool', 'operation'=>'int', '&w_wouldblock='=>'int'], 'SplFileObject::fpassthru' => ['int|false'], -'SplFileObject::fputcsv' => ['int|false', 'fields'=>'array', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], +'SplFileObject::fputcsv' => ['int|false', 'fields'=>'array', 'seperator='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'SplFileObject::fread' => ['string|false', 'length'=>'int'], 'SplFileObject::fscanf' => ['array|int', 'format'=>'string', '&...w_vars='=>'string|int|float'], 'SplFileObject::fseek' => ['int', 'pos'=>'int', 'whence='=>'int'], -'SplFileObject::fstat' => ['array|false'], +'SplFileObject::fstat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}'], 'SplFileObject::ftell' => ['int|false'], 'SplFileObject::ftruncate' => ['bool', 'size'=>'int'], 'SplFileObject::fwrite' => ['int', 'string'=>'string', 'length='=>'int'], @@ -13401,16 +13402,16 @@ 'SplTempFileObject::eof' => ['bool'], 'SplTempFileObject::fflush' => ['bool'], 'SplTempFileObject::fgetc' => ['false|string'], -'SplTempFileObject::fgetcsv' => ['list|array{0: null}|false|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], +'SplTempFileObject::fgetcsv' => ['list|array{0: null}|false|null', 'seperator='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'SplTempFileObject::fgets' => ['string'], 'SplTempFileObject::fgetss' => ['string', 'allowable_tags='=>'string'], 'SplTempFileObject::flock' => ['bool', 'operation'=>'int', '&wouldblock='=>'int'], 'SplTempFileObject::fpassthru' => ['int|false'], -'SplTempFileObject::fputcsv' => ['false|int', 'fields'=>'array', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], +'SplTempFileObject::fputcsv' => ['false|int', 'fields'=>'array', 'seperator='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'SplTempFileObject::fread' => ['false|string', 'length'=>'int'], 'SplTempFileObject::fscanf' => ['bool', 'format'=>'string', '&...w_vars='=>'array|array|array'], 'SplTempFileObject::fseek' => ['int', 'pos'=>'int', 'whence='=>'int'], -'SplTempFileObject::fstat' => ['array|false'], +'SplTempFileObject::fstat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}'], 'SplTempFileObject::ftell' => ['int'], 'SplTempFileObject::ftruncate' => ['bool', 'size'=>'int'], 'SplTempFileObject::fwrite' => ['int', 'string'=>'string', 'length='=>'int'], @@ -13647,18 +13648,18 @@ 'ssh2_scp_send' => ['bool', 'session'=>'resource', 'local_file'=>'string', 'remote_file'=>'string', 'create_mode='=>'int'], 'ssh2_sftp' => ['resource|false', 'session'=>'resource'], 'ssh2_sftp_chmod' => ['bool', 'sftp'=>'resource', 'filename'=>'string', 'mode'=>'int'], -'ssh2_sftp_lstat' => ['array|false', 'sftp'=>'resource', 'path'=>'string'], +'ssh2_sftp_lstat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'sftp'=>'resource', 'path'=>'string'], 'ssh2_sftp_mkdir' => ['bool', 'sftp'=>'resource', 'dirname'=>'string', 'mode='=>'int', 'recursive='=>'bool'], 'ssh2_sftp_readlink' => ['string|false', 'sftp'=>'resource', 'link'=>'string'], 'ssh2_sftp_realpath' => ['string|false', 'sftp'=>'resource', 'filename'=>'string'], 'ssh2_sftp_rename' => ['bool', 'sftp'=>'resource', 'from'=>'string', 'to'=>'string'], 'ssh2_sftp_rmdir' => ['bool', 'sftp'=>'resource', 'dirname'=>'string'], -'ssh2_sftp_stat' => ['array|false', 'sftp'=>'resource', 'path'=>'string'], +'ssh2_sftp_stat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'sftp'=>'resource', 'path'=>'string'], 'ssh2_sftp_symlink' => ['bool', 'sftp'=>'resource', 'target'=>'string', 'link'=>'string'], 'ssh2_sftp_unlink' => ['bool', 'sftp'=>'resource', 'filename'=>'string'], 'ssh2_shell' => ['resource|false', 'session'=>'resource', 'term_type='=>'string', 'env='=>'array', 'width='=>'int', 'height='=>'int', 'width_height_type='=>'int'], 'ssh2_tunnel' => ['resource|false', 'session'=>'resource', 'host'=>'string', 'port'=>'int'], -'stat' => ['array|false', 'filename'=>'string'], +'stat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'filename'=>'string'], 'stats_absolute_deviation' => ['float', 'a'=>'array'], 'stats_cdf_beta' => ['float', 'par1'=>'float', 'par2'=>'float', 'par3'=>'float', 'which'=>'int'], 'stats_cdf_binomial' => ['float', 'par1'=>'float', 'par2'=>'float', 'par3'=>'float', 'which'=>'int'], diff --git a/dictionaries/CallMap_73_delta.php b/dictionaries/CallMap_73_delta.php index c54758bbc0d..91002d47056 100644 --- a/dictionaries/CallMap_73_delta.php +++ b/dictionaries/CallMap_73_delta.php @@ -50,6 +50,14 @@ 'socket_wsaprotocol_info_release' => ['bool', 'info_id'=>'string'], ], 'changed' => [ + 'array_push' => [ + 'old' => ['int', '&rw_array'=>'array', '...values'=>'mixed'], + 'new' => ['int', '&rw_array'=>'array', '...values='=>'mixed'], + ], + 'array_unshift' => [ + 'old' => ['int', '&rw_array'=>'array', '...values'=>'mixed'], + 'new' => ['int', '&rw_array'=>'array', '...values='=>'mixed'], + ], 'bcscale' => [ 'old' => ['int', 'scale'=>'int'], 'new' => ['int', 'scale='=>'int'], diff --git a/dictionaries/CallMap_74_delta.php b/dictionaries/CallMap_74_delta.php index 83b9108bd27..af5e0d976bf 100644 --- a/dictionaries/CallMap_74_delta.php +++ b/dictionaries/CallMap_74_delta.php @@ -20,6 +20,14 @@ 'mb_str_split' => ['list|false', 'string'=>'string', 'length='=>'positive-int', 'encoding='=>'string'], ], 'changed' => [ + 'array_merge' => [ + 'old' => ['array', '...arrays'=>'array'], + 'new' => ['array', '...arrays='=>'array'], + ], + 'array_merge_recursive' => [ + 'old' => ['array', '...arrays'=>'array'], + 'new' => ['array', '...arrays='=>'array'], + ], 'gzread' => [ 'old' => ['string|0', 'stream'=>'resource', 'length'=>'int'], 'new' => ['string|false', 'stream'=>'resource', 'length'=>'int'], diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index e282a68c4c5..1125a4349b8 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -145,10 +145,42 @@ 'old' => ['bool', 'name'=>'string', 'content'=>'string', 'isParam'=>'bool', 'publicId'=>'string', 'systemId'=>'string', 'notationData'=>'string'], 'new' => ['bool', 'name'=>'string', 'content'=>'string', 'isParam='=>'bool', 'publicId='=>'?string', 'systemId='=>'?string', 'notationData='=>'?string'], ], + 'array_column' => [ + 'old' => ['array', 'array'=>'array', 'column_key'=>'mixed', 'index_key='=>'mixed'], + 'new' => ['array', 'array'=>'array', 'column_key'=>'int|string|null', 'index_key='=>'int|string|null'], + ], 'array_combine' => [ 'old' => ['associative-array|false', 'keys'=>'string[]|int[]', 'values'=>'array'], 'new' => ['associative-array', 'keys'=>'string[]|int[]', 'values'=>'array'], ], + 'array_diff' => [ + 'old' => ['associative-array', 'array'=>'array', '...arrays'=>'array'], + 'new' => ['associative-array', 'array'=>'array', '...arrays='=>'array'], + ], + 'array_diff_assoc' => [ + 'old' => ['associative-array', 'array'=>'array', '...arrays'=>'array'], + 'new' => ['associative-array', 'array'=>'array', '...arrays='=>'array'], + ], + 'array_diff_key' => [ + 'old' => ['associative-array', 'array'=>'array', '...arrays'=>'array'], + 'new' => ['associative-array', 'array'=>'array', '...arrays='=>'array'], + ], + 'array_key_exists' => [ + 'old' => ['bool', 'key'=>'string|int', 'array'=>'array|object'], + 'new' => ['bool', 'key'=>'string|int', 'array'=>'array'], + ], + 'array_intersect' => [ + 'old' => ['associative-array', 'array'=>'array', '...arrays'=>'array'], + 'new' => ['associative-array', 'array'=>'array', '...arrays='=>'array'], + ], + 'array_intersect_assoc' => [ + 'old' => ['associative-array', 'array'=>'array', '...arrays'=>'array'], + 'new' => ['associative-array', 'array'=>'array', '...arrays='=>'array'], + ], + 'array_intersect_key' => [ + 'old' => ['associative-array', 'array'=>'array', '...arrays'=>'array'], + 'new' => ['associative-array', 'array'=>'array', '...arrays='=>'array'], + ], 'bcadd' => [ 'old' => ['numeric-string', 'num1'=>'numeric-string', 'num2'=>'numeric-string', 'scale='=>'int'], 'new' => ['numeric-string', 'num1'=>'numeric-string', 'num2'=>'numeric-string', 'scale='=>'int|null'], @@ -193,6 +225,10 @@ 'old' => ['bool', 'typelib_name'=>'string', 'case_insensitive='=>'bool'], 'new' => ['bool', 'typelib_name'=>'string', 'case_insensitive='=>'true'], ], + 'count' => [ + 'old' => ['int', 'value'=>'Countable|array|SimpleXMLElement|ResourceBundle', 'mode='=>'int'], + 'new' => ['int', 'value'=>'Countable|array', 'mode='=>'int'], + ], 'count_chars' => [ 'old' => ['array|false', 'input'=>'string', 'mode='=>'0|1|2'], 'new' => ['array', 'input'=>'string', 'mode='=>'0|1|2'], @@ -321,6 +357,10 @@ 'old' => ['string|false', 'object'=>'DateTimeInterface', 'format'=>'string'], 'new' => ['string', 'object'=>'DateTimeInterface', 'format'=>'string'], ], + 'datefmt_create' => [ + 'old' => ['?IntlDateFormatter', 'locale'=>'?string', 'dateType'=>'int', 'timeType'=>'int', 'timezone='=>'DateTimeZone|IntlTimeZone|string|null', 'calendar='=>'IntlCalendar|int|null', 'pattern='=>'string'], + 'new' => ['?IntlDateFormatter', 'locale'=>'?string', 'dateType='=>'int', 'timeType='=>'int', 'timezone='=>'DateTimeZone|IntlTimeZone|string|null', 'calendar='=>'IntlCalendar|int|null', 'pattern='=>'?string'], + ], 'dom_import_simplexml' => [ 'old' => ['DOMElement|null', 'node'=>'SimpleXMLElement'], 'new' => ['DOMElement', 'node'=>'SimpleXMLElement'], @@ -329,6 +369,14 @@ 'old' => ['list|false', 'separator'=>'string', 'string'=>'string', 'limit='=>'int'], 'new' => ['list', 'separator'=>'string', 'string'=>'string', 'limit='=>'int'], ], + 'get_class_methods' => [ + 'old' => ['list|null', 'object_or_class'=>'mixed'], + 'new' => ['list', 'object_or_class'=>'object|class-string'], + ], + 'get_parent_class' => [ + 'old' => ['class-string|false', 'object_or_class='=>'mixed'], + 'new' => ['class-string|false', 'object_or_class='=>'object|class-string'], + ], 'gmdate' => [ 'old' => ['string', 'format'=>'string', 'timestamp='=>'int'], 'new' => ['string', 'format'=>'string', 'timestamp='=>'int|null'], @@ -979,7 +1027,11 @@ ], 'number_format' => [ 'old' => ['string', 'num'=>'float|int', 'decimals='=>'int'], - 'new' => ['string', 'num'=>'float|int', 'decimals='=>'int', 'decimal_separator='=>'string', 'thousands_separator='=>'string'], + 'new' => ['string', 'num'=>'float|int', 'decimals='=>'int', 'decimal_separator='=>'?string', 'thousands_separator='=>'?string'], + ], + 'ob_implicit_flush' => [ + 'old' => ['void', 'enable='=>'int'], + 'new' => ['void', 'enable='=>'bool'], ], 'openssl_csr_export' => [ 'old' => ['bool', 'csr'=>'string|resource', '&w_output'=>'string', 'no_text='=>'bool'], @@ -1622,8 +1674,10 @@ 'gzgetss' => ['string|false', 'zp'=>'resource', 'length'=>'int', 'allowable_tags='=>'string'], 'image2wbmp' => ['bool', 'im'=>'resource', 'filename='=>'?string', 'threshold='=>'int'], 'jpeg2wbmp' => ['bool', 'jpegname'=>'string', 'wbmpname'=>'string', 'dest_height'=>'int', 'dest_width'=>'int', 'threshold'=>'int'], + 'ldap_control_paged_result' => ['bool', 'link_identifier'=>'resource', 'pagesize'=>'int', 'iscritical='=>'bool', 'cookie='=>'string'], + 'ldap_control_paged_result_response' => ['bool', 'link_identifier'=>'resource', 'result_identifier'=>'resource', '&w_cookie'=>'string', '&w_estimated'=>'int'], 'ldap_sort' => ['bool', 'link_identifier'=>'resource', 'result_identifier'=>'resource', 'sortfilter'=>'string'], - 'number_format\'1' => ['string', 'num'=>'float|int', 'decimals'=>'int', 'decimal_separator'=>'string', 'thousands_separator'=>'string'], + 'number_format\'1' => ['string', 'num'=>'float|int', 'decimals'=>'int', 'decimal_separator'=>'?string', 'thousands_separator'=>'?string'], 'png2wbmp' => ['bool', 'pngname'=>'string', 'wbmpname'=>'string', 'dest_height'=>'int', 'dest_width'=>'int', 'threshold'=>'int'], 'read_exif_data' => ['array', 'filename'=>'string', 'sections_needed='=>'string', 'sub_arrays='=>'bool', 'read_thumbnail='=>'bool'], 'SimpleXMLIterator::rewind' => ['void'], diff --git a/dictionaries/CallMap_81_delta.php b/dictionaries/CallMap_81_delta.php index 3195d08f3be..d07d2e44736 100644 --- a/dictionaries/CallMap_81_delta.php +++ b/dictionaries/CallMap_81_delta.php @@ -17,7 +17,7 @@ return [ 'added' => [ 'array_is_list' => ['bool', 'array' => 'array'], - 'enum_exists' => ['bool', 'class' => 'string', 'autoload=' => 'bool'], + 'enum_exists' => ['bool', 'enum' => 'string', 'autoload=' => 'bool'], 'fsync' => ['bool', 'stream' => 'resource'], 'fdatasync' => ['bool', 'stream' => 'resource'], 'imageavif' => ['bool', 'image'=>'GdImage', 'file='=>'resource|string|null', 'quality='=>'int', 'speed='=>'int'], @@ -37,6 +37,7 @@ 'Fiber::getCurrent' => ['?self'], 'Fiber::suspend' => ['mixed', 'value='=>'null|mixed'], 'FiberError::__construct' => ['void'], + 'ReflectionClass::isEnum' => ['bool'], 'ReflectionEnum::getBackingType' => ['?ReflectionType'], 'ReflectionEnum::getCase' => ['ReflectionEnumUnitCase', 'name' => 'string'], 'ReflectionEnum::getCases' => ['list'], @@ -45,7 +46,10 @@ 'ReflectionEnumUnitCase::getEnum' => ['ReflectionEnum'], 'ReflectionEnumUnitCase::getValue' => ['UnitEnum'], 'ReflectionEnumBackedCase::getBackingValue' => ['string|int'], + 'ReflectionFunctionAbstract::getTentativeReturnType' => ['?ReflectionType'], + 'ReflectionFunctionAbstract::hasTentativeReturnType' => ['bool'], 'ReflectionFunctionAbstract::isStatic' => ['bool'], + 'ReflectionObject::isEnum' => ['bool'], ], 'changed' => [ @@ -69,6 +73,10 @@ 'old' => ['bool', 'finfo'=>'resource', 'flags'=>'int'], 'new' => ['bool', 'finfo'=>'finfo', 'flags'=>'int'], ], + 'fputcsv' => [ + 'old' => ['int|false', 'stream'=>'resource', 'fields'=>'array', 'separator='=>'string', 'enclosure='=>'string', 'escape='=>'string'], + 'new' => ['int|false', 'stream'=>'resource', 'fields'=>'array', 'separator='=>'string', 'enclosure='=>'string', 'escape='=>'string', 'eol='=>'string'], + ], 'ftp_connect' => [ 'old' => ['resource|false', 'hostname' => 'string', 'port=' => 'int', 'timeout=' => 'int'], 'new' => ['FTP\Connection|false', 'hostname' => 'string', 'port=' => 'int', 'timeout=' => 'int'], @@ -447,11 +455,11 @@ ], 'ldap_add' => [ 'old' => ['bool', 'ldap'=>'resource', 'dn'=>'string', 'entry'=>'array', 'controls='=>'array'], - 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'array'], + 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], ], 'ldap_add_ext' => [ 'old' => ['resource|false', 'ldap'=>'resource', 'dn'=>'string', 'entry'=>'array', 'controls='=>'array'], - 'new' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'array'], + 'new' => ['LDAP\Result|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], ], 'ldap_bind' => [ 'old' => ['bool', 'ldap'=>'resource', 'dn='=>'string|null', 'password='=>'string|null'], @@ -459,7 +467,7 @@ ], 'ldap_bind_ext' => [ 'old' => ['resource|false', 'ldap'=>'resource', 'dn='=>'string|null', 'password='=>'string|null', 'controls='=>'array'], - 'new' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'dn='=>'string|null', 'password='=>'string|null', 'controls='=>'array'], + 'new' => ['LDAP\Result|false', 'ldap'=>'LDAP\Connection', 'dn='=>'string|null', 'password='=>'string|null', 'controls='=>'?array'], ], 'ldap_close' => [ 'old' => ['bool', 'ldap'=>'resource'], @@ -467,7 +475,7 @@ ], 'ldap_compare' => [ 'old' => ['bool|int', 'ldap'=>'resource', 'dn'=>'string', 'attribute'=>'string', 'value'=>'string'], - 'new' => ['bool|int', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'attribute'=>'string', 'value'=>'string'], + 'new' => ['bool|int', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'attribute'=>'string', 'value'=>'string', 'controls='=>'?array'], ], 'ldap_connect' => [ 'old' => ['resource|false', 'uri='=>'string', 'port='=>'int', 'wallet='=>'string', 'password='=>'string', 'auth_mode='=>'int'], @@ -479,11 +487,11 @@ ], 'ldap_delete' => [ 'old' => ['bool', 'ldap'=>'resource', 'dn'=>'string'], - 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string'], + 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'controls='=>'?array'], ], 'ldap_delete_ext' => [ 'old' => ['resource|false', 'ldap'=>'resource', 'dn'=>'string', 'controls='=>'array'], - 'new' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'controls='=>'array'], + 'new' => ['LDAP\Result|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'controls='=>'?array'], ], 'ldap_errno' => [ 'old' => ['int', 'ldap'=>'resource'], @@ -495,7 +503,7 @@ ], 'ldap_exop' => [ 'old' => ['mixed', 'ldap'=>'resource', 'reqoid'=>'string', 'reqdata='=>'string', 'serverctrls='=>'array|null', '&w_response_data='=>'string', '&w_response_oid='=>'string'], - 'new' => ['mixed', 'ldap'=>'LDAP\Connection', 'reqoid'=>'string', 'reqdata='=>'string', 'serverctrls='=>'array|null', '&w_response_data='=>'string', '&w_response_oid='=>'string'], + 'new' => ['mixed', 'ldap'=>'LDAP\Connection', 'request_oid'=>'string', 'request_data='=>'string', 'controls='=>'?array', '&w_response_data='=>'string', '&w_response_oid='=>'string'], ], 'ldap_exop_passwd' => [ 'old' => ['bool|string', 'ldap'=>'resource', 'user='=>'string', 'old_password='=>'string', 'new_password='=>'string', '&w_controls='=>'array|null'], @@ -515,15 +523,15 @@ ], 'ldap_first_entry' => [ 'old' => ['resource|false', 'ldap'=>'resource', 'result'=>'resource'], - 'new' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'result'=>'LDAP\Result'], + 'new' => ['LDAP\ResultEntry|false', 'ldap'=>'LDAP\Connection', 'result'=>'LDAP\Result'], ], 'ldap_first_reference' => [ 'old' => ['resource|false', 'ldap'=>'resource', 'result'=>'resource'], - 'new' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'result'=>'LDAP\Result'], + 'new' => ['LDAP\ResultEntry|false', 'ldap'=>'LDAP\Connection', 'result'=>'LDAP\Result'], ], 'ldap_free_result' => [ 'old' => ['bool', 'ldap'=>'resource'], - 'new' => ['bool', 'ldap'=>'LDAP\Connection'], + 'new' => ['bool', 'result'=>'LDAP\Result'], ], 'ldap_get_attributes' => [ 'old' => ['array|false', 'ldap'=>'resource', 'entry'=>'resource'], @@ -539,7 +547,7 @@ ], 'ldap_get_option' => [ 'old' => ['bool', 'ldap'=>'resource', 'option'=>'int', '&w_value'=>'mixed'], - 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'option'=>'int', '&w_value'=>'mixed'], + 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'option'=>'int', '&w_value='=>'array|string|int|null'], ], 'ldap_get_values' => [ 'old' => ['array|false', 'ldap'=>'resource', 'entry'=>'resource', 'attribute'=>'string'], @@ -551,39 +559,39 @@ ], 'ldap_list' => [ 'old' => ['resource|false', 'ldap'=>'resource|array', 'base'=>'string', 'filter'=>'string', 'attributes='=>'array', 'attributes_only='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int'], - 'new' => ['LDAP\Connection|false', 'ldap'=>'resource|array', 'base'=>'string', 'filter'=>'string', 'attributes='=>'array', 'attributes_only='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int'], + 'new' => ['LDAP\Result|LDAP\Result[]|false', 'ldap'=>'LDAP\Connection|LDAP\Connection[]', 'base'=>'string', 'filter'=>'string', 'attributes='=>'array', 'attributes_only='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int', 'controls='=>'?array'], ], 'ldap_mod_add' => [ 'old' => ['bool', 'ldap'=>'resource', 'dn'=>'string', 'entry'=>'array'], - 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array'], + 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], ], 'ldap_mod_add_ext' => [ 'old' => ['resource|false', 'ldap'=>'resource', 'dn'=>'string', 'entry'=>'array', 'controls='=>'array'], - 'new' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'array'], + 'new' => ['LDAP\Result|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], ], 'ldap_mod_del' => [ 'old' => ['bool', 'ldap'=>'resource', 'dn'=>'string', 'entry'=>'array'], - 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array'], + 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], ], 'ldap_mod_del_ext' => [ 'old' => ['resource|false', 'ldap'=>'resource', 'dn'=>'string', 'entry'=>'array', 'controls='=>'array'], - 'new' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'array'], + 'new' => ['LDAP\Result|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], ], 'ldap_mod_replace' => [ 'old' => ['bool', 'ldap'=>'resource', 'dn'=>'string', 'entry'=>'array'], - 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array'], + 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], ], 'ldap_mod_replace_ext' => [ 'old' => ['resource|false', 'ldap'=>'resource', 'dn'=>'string', 'entry'=>'array', 'controls='=>'array'], - 'new' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'array'], + 'new' => ['LDAP\Result|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], ], 'ldap_modify' => [ 'old' => ['bool', 'ldap'=>'resource', 'dn'=>'string', 'entry'=>'array'], - 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array'], + 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], ], 'ldap_modify_batch' => [ 'old' => ['bool', 'ldap'=>'resource', 'dn'=>'string', 'modifications_info'=>'array'], - 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'modifications_info'=>'array'], + 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'modifications_info'=>'array', 'controls='=>'?array'], ], 'ldap_next_attribute' => [ 'old' => ['string|false', 'ldap'=>'resource', 'entry'=>'resource'], @@ -591,11 +599,11 @@ ], 'ldap_next_entry' => [ 'old' => ['resource|false', 'ldap'=>'resource', 'result'=>'resource'], - 'new' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'result'=>'LDAP\Result'], + 'new' => ['LDAP\ResultEntry|false', 'ldap'=>'LDAP\Connection', 'entry'=>'LDAP\ResultEntry'], ], 'ldap_next_reference' => [ 'old' => ['resource|false', 'ldap'=>'resource', 'entry'=>'resource'], - 'new' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'entry'=>'LDAP\ResultEntry'], + 'new' => ['LDAP\ResultEntry|false', 'ldap'=>'LDAP\Connection', 'entry'=>'LDAP\ResultEntry'], ], 'ldap_parse_exop' => [ 'old' => ['bool', 'ldap'=>'resource', 'result'=>'resource', '&w_response_data='=>'string', '&w_response_oid='=>'string'], @@ -603,7 +611,7 @@ ], 'ldap_parse_reference' => [ 'old' => ['bool', 'ldap'=>'resource', 'entry'=>'resource', 'referrals'=>'array'], - 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'entry'=>'LDAP\ResultEntry', 'referrals'=>'array'], + 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'entry'=>'LDAP\ResultEntry', '&w_referrals'=>'array'], ], 'ldap_parse_result' => [ 'old' => ['bool', 'ldap'=>'resource', 'result'=>'resource', '&w_error_code'=>'int', '&w_matched_dn='=>'string', '&w_error_message='=>'string', '&w_referrals='=>'array', '&w_controls='=>'array'], @@ -611,15 +619,15 @@ ], 'ldap_read' => [ 'old' => ['resource|false', 'ldap'=>'resource|array', 'base'=>'string', 'filter'=>'string', 'attributes='=>'array', 'attributes_only='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int'], - 'new' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection|array', 'base'=>'string', 'filter'=>'string', 'attributes='=>'array', 'attributes_only='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int'], + 'new' => ['LDAP\Result|LDAP\Result[]|false', 'ldap'=>'LDAP\Connection|LDAP\Connection[]', 'base'=>'string', 'filter'=>'string', 'attributes='=>'array', 'attributes_only='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int', 'controls='=>'?array'], ], 'ldap_rename' => [ 'old' => ['bool', 'ldap'=>'resource', 'dn'=>'string', 'new_rdn'=>'string', 'new_parent'=>'string', 'delete_old_rdn'=>'bool'], - 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'new_rdn'=>'string', 'new_parent'=>'string', 'delete_old_rdn'=>'bool'], + 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'new_rdn'=>'string', 'new_parent'=>'string', 'delete_old_rdn'=>'bool', 'controls='=>'?array'], ], 'ldap_rename_ext' => [ 'old' => ['resource|false', 'ldap'=>'resource', 'dn'=>'string', 'new_rdn'=>'string', 'new_parent'=>'string', 'delete_old_rdn'=>'bool', 'controls='=>'array'], - 'new' => ['LDAP\Connection|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'new_rdn'=>'string', 'new_parent'=>'string', 'delete_old_rdn'=>'bool', 'controls='=>'array'], + 'new' => ['LDAP\Result|false', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'new_rdn'=>'string', 'new_parent'=>'string', 'delete_old_rdn'=>'bool', 'controls='=>'?array'], ], 'ldap_sasl_bind' => [ 'old' => ['bool', 'ldap'=>'resource', 'dn='=>'string', 'password='=>'string', 'mech='=>'string', 'realm='=>'string', 'authc_id='=>'string', 'authz_id='=>'string', 'props='=>'string'], @@ -627,7 +635,7 @@ ], 'ldap_search' => [ 'old' => ['resource|false', 'ldap'=>'resource|resource[]', 'base'=>'string', 'filter'=>'string', 'attributes='=>'array', 'attributes_only='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int'], - 'new' => ['LDAP\Result|false', 'ldap'=>'LDAP\Connection|LDAP\Connection[]', 'base'=>'string', 'filter'=>'string', 'attributes='=>'array', 'attributes_only='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int'], + 'new' => ['LDAP\Result|LDAP\Result[]|false', 'ldap'=>'LDAP\Connection|LDAP\Connection[]', 'base'=>'string', 'filter'=>'string', 'attributes='=>'array', 'attributes_only='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int', 'controls='=>'?array'], ], 'ldap_set_option' => [ 'old' => ['bool', 'ldap'=>'resource|null', 'option'=>'int', 'value'=>'mixed'], @@ -637,6 +645,14 @@ 'old' => ['bool', 'ldap'=>'resource', 'callback'=>'?callable'], 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'callback'=>'?callable'], ], + 'ldap_start_tls' => [ + 'old' => ['bool', 'ldap'=>'resource'], + 'new' => ['bool', 'ldap'=>'LDAP\Connection'], + ], + 'ldap_unbind' => [ + 'old' => ['bool', 'ldap'=>'resource'], + 'new' => ['bool', 'ldap'=>'LDAP\Connection'], + ], 'mysqli::connect' => [ 'old' => ['null|false', 'hostname='=>'string|null', 'username='=>'string|null', 'password='=>'string|null', 'database='=>'string|null', 'port='=>'int|null', 'socket='=>'string|null'], 'new' => ['bool', 'hostname='=>'string|null', 'username='=>'string|null', 'password='=>'string|null', 'database='=>'string|null', 'port='=>'int|null', 'socket='=>'string|null'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 1a48d0e16ad..aa88e0ba221 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -7706,16 +7706,16 @@ 'SplFileObject::eof' => ['bool'], 'SplFileObject::fflush' => ['bool'], 'SplFileObject::fgetc' => ['string|false'], - 'SplFileObject::fgetcsv' => ['list|array{0: null}|false|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], + 'SplFileObject::fgetcsv' => ['list|array{0: null}|false|null', 'seperator='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'SplFileObject::fgets' => ['string|false'], 'SplFileObject::fgetss' => ['string|false', 'allowable_tags='=>'string'], 'SplFileObject::flock' => ['bool', 'operation'=>'int', '&w_wouldblock='=>'int'], 'SplFileObject::fpassthru' => ['int|false'], - 'SplFileObject::fputcsv' => ['int|false', 'fields'=>'array', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], + 'SplFileObject::fputcsv' => ['int|false', 'fields'=>'array', 'seperator='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'SplFileObject::fread' => ['string|false', 'length'=>'int'], 'SplFileObject::fscanf' => ['array|int', 'format'=>'string', '&...w_vars='=>'string|int|float'], 'SplFileObject::fseek' => ['int', 'pos'=>'int', 'whence='=>'int'], - 'SplFileObject::fstat' => ['array|false'], + 'SplFileObject::fstat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}'], 'SplFileObject::ftell' => ['int|false'], 'SplFileObject::ftruncate' => ['bool', 'size'=>'int'], 'SplFileObject::fwrite' => ['int', 'string'=>'string', 'length='=>'int'], @@ -7898,16 +7898,16 @@ 'SplTempFileObject::eof' => ['bool'], 'SplTempFileObject::fflush' => ['bool'], 'SplTempFileObject::fgetc' => ['false|string'], - 'SplTempFileObject::fgetcsv' => ['list|array{0: null}|false|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], + 'SplTempFileObject::fgetcsv' => ['list|array{0: null}|false|null', 'seperator='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'SplTempFileObject::fgets' => ['string'], 'SplTempFileObject::fgetss' => ['string', 'allowable_tags='=>'string'], 'SplTempFileObject::flock' => ['bool', 'operation'=>'int', '&wouldblock='=>'int'], 'SplTempFileObject::fpassthru' => ['int|false'], - 'SplTempFileObject::fputcsv' => ['false|int', 'fields'=>'array', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], + 'SplTempFileObject::fputcsv' => ['false|int', 'fields'=>'array', 'seperator='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'SplTempFileObject::fread' => ['false|string', 'length'=>'int'], 'SplTempFileObject::fscanf' => ['bool', 'format'=>'string', '&...w_vars='=>'array|array|array'], 'SplTempFileObject::fseek' => ['int', 'pos'=>'int', 'whence='=>'int'], - 'SplTempFileObject::fstat' => ['array|false'], + 'SplTempFileObject::fstat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}'], 'SplTempFileObject::ftell' => ['int'], 'SplTempFileObject::ftruncate' => ['bool', 'size'=>'int'], 'SplTempFileObject::fwrite' => ['int', 'string'=>'string', 'length='=>'int'], @@ -9510,9 +9510,9 @@ 'array_column' => ['array', 'array'=>'array', 'column_key'=>'mixed', 'index_key='=>'mixed'], 'array_combine' => ['associative-array|false', 'keys'=>'string[]|int[]', 'values'=>'array'], 'array_count_values' => ['associative-array', 'array'=>'array'], - 'array_diff' => ['associative-array', 'array'=>'array', 'arrays'=>'array', '...args='=>'array'], - 'array_diff_assoc' => ['associative-array', 'array'=>'array', 'arrays'=>'array', '...args='=>'array'], - 'array_diff_key' => ['associative-array', 'array'=>'array', 'arrays'=>'array', '...args='=>'array'], + 'array_diff' => ['associative-array', 'array'=>'array', '...arrays'=>'array'], + 'array_diff_assoc' => ['associative-array', 'array'=>'array', '...arrays'=>'array'], + 'array_diff_key' => ['associative-array', 'array'=>'array', '...arrays'=>'array'], 'array_diff_uassoc' => ['associative-array', 'array'=>'array', 'rest'=>'array', 'data_comp_func'=>'callable(mixed,mixed):int'], 'array_diff_uassoc\'1' => ['associative-array', 'array'=>'array', 'rest'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'], 'array_diff_ukey' => ['associative-array', 'array'=>'array', 'rest'=>'array', 'key_comp_func'=>'callable(mixed,mixed):int'], @@ -9521,28 +9521,28 @@ 'array_fill_keys' => ['array', 'keys'=>'array', 'value'=>'mixed'], 'array_filter' => ['associative-array', 'array'=>'array', 'callback='=>'callable(mixed,mixed=):scalar', 'mode='=>'int'], 'array_flip' => ['associative-array|associative-array', 'array'=>'array'], - 'array_intersect' => ['associative-array', 'array'=>'array', 'arrays'=>'array', '...args='=>'array'], - 'array_intersect_assoc' => ['associative-array', 'array'=>'array', 'arrays'=>'array', '...args='=>'array'], - 'array_intersect_key' => ['associative-array', 'array'=>'array', 'arrays'=>'array', '...args='=>'array'], + 'array_intersect' => ['associative-array', 'array'=>'array', '...arrays'=>'array'], + 'array_intersect_assoc' => ['associative-array', 'array'=>'array', '...arrays'=>'array'], + 'array_intersect_key' => ['associative-array', 'array'=>'array', '...arrays'=>'array'], 'array_intersect_uassoc' => ['associative-array', 'array'=>'array', 'rest'=>'array', 'key_compare_func'=>'callable(mixed,mixed):int'], 'array_intersect_uassoc\'1' => ['associative-array', 'array'=>'array', 'rest'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest'=>'array|callable(mixed,mixed):int'], 'array_intersect_ukey' => ['associative-array', 'array'=>'array', 'rest'=>'array', 'key_compare_func'=>'callable(mixed,mixed):int'], 'array_intersect_ukey\'1' => ['associative-array', 'array'=>'array', 'rest'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest'=>'array|callable(mixed,mixed):int'], - 'array_key_exists' => ['bool', 'key'=>'string|int', 'array'=>'array|ArrayObject'], + 'array_key_exists' => ['bool', 'key'=>'string|int', 'array'=>'array|object'], 'array_keys' => ['list', 'array'=>'array', 'filter_value='=>'mixed', 'strict='=>'bool'], 'array_map' => ['array', 'callback'=>'?callable', 'array'=>'array', '...arrays='=>'array'], - 'array_merge' => ['array', 'arrays'=>'array', '...args='=>'array'], - 'array_merge_recursive' => ['array', 'arrays'=>'array', '...args='=>'array'], + 'array_merge' => ['array', '...arrays'=>'array'], + 'array_merge_recursive' => ['array', '...arrays'=>'array'], 'array_multisort' => ['bool', '&rw_array'=>'array', 'rest='=>'array|int', 'array1_sort_flags='=>'array|int', '...args='=>'array|int'], 'array_pad' => ['array', 'array'=>'array', 'length'=>'int', 'value'=>'mixed'], 'array_pop' => ['mixed', '&rw_array'=>'array'], 'array_product' => ['int|float', 'array'=>'array'], - 'array_push' => ['int', '&rw_array'=>'array', 'values'=>'mixed', '...vars='=>'mixed'], + 'array_push' => ['int', '&rw_array'=>'array', '...values'=>'mixed'], 'array_rand' => ['int|string|array|array', 'array'=>'non-empty-array', 'num'=>'int'], 'array_rand\'1' => ['int|string', 'array'=>'array'], 'array_reduce' => ['mixed', 'array'=>'array', 'callback'=>'callable(mixed,mixed):mixed', 'initial='=>'mixed'], - 'array_replace' => ['array', 'array'=>'array', 'replacements'=>'array', '...args='=>'array'], - 'array_replace_recursive' => ['array', 'array'=>'array', 'replacements'=>'array', '...args='=>'array'], + 'array_replace' => ['array', 'array'=>'array', '...replacements='=>'array'], + 'array_replace_recursive' => ['array', 'array'=>'array', '...replacements='=>'array'], 'array_reverse' => ['array', 'array'=>'array', 'preserve_keys='=>'bool'], 'array_search' => ['int|string|false', 'needle'=>'mixed', 'haystack'=>'array', 'strict='=>'bool'], 'array_shift' => ['mixed|null', '&rw_array'=>'array'], @@ -9564,7 +9564,7 @@ 'array_unique' => ['array', 'array'=>'array', 'flags='=>'0'], 'array_unique\'1' => ['array', 'array'=>'array', 'flags='=>'1'], 'array_unique\'2' => ['array', 'array'=>'array', 'flags='=>'2|5'], - 'array_unshift' => ['int', '&rw_array'=>'array', 'values'=>'mixed', '...vars='=>'mixed'], + 'array_unshift' => ['int', '&rw_array'=>'array', '...values'=>'mixed'], 'array_values' => ['list', 'array'=>'array'], 'array_walk' => ['bool', '&rw_array'=>'array', 'callback'=>'callable', 'arg='=>'mixed'], 'array_walk\'1' => ['bool', '&rw_array'=>'object', 'callback'=>'callable', 'arg='=>'mixed'], @@ -10187,7 +10187,7 @@ 'date_timestamp_set' => ['DateTime|false', 'object'=>'DateTime', 'timestamp'=>'int'], 'date_timezone_get' => ['DateTimeZone|false', 'object'=>'DateTimeInterface'], 'date_timezone_set' => ['DateTime|false', 'object'=>'DateTime', 'timezone'=>'DateTimeZone'], - 'datefmt_create' => ['IntlDateFormatter|false', 'locale'=>'?string', 'dateType'=>'?int', 'timeType'=>'?int', 'timezone='=>'string|DateTimeZone|IntlTimeZone|null', 'calendar='=>'int|IntlCalendar|null', 'pattern='=>'string'], + 'datefmt_create' => ['?IntlDateFormatter', 'locale'=>'?string', 'dateType'=>'int', 'timeType'=>'int', 'timezone='=>'DateTimeZone|IntlTimeZone|string|null', 'calendar='=>'IntlCalendar|int|null', 'pattern='=>'string'], 'datefmt_format' => ['string|false', 'formatter'=>'IntlDateFormatter', 'datetime'=>'DateTime|IntlCalendar|array|int'], 'datefmt_format_object' => ['string|false', 'datetime'=>'object', 'format='=>'mixed', 'locale='=>'string'], 'datefmt_get_calendar' => ['int', 'formatter'=>'IntlDateFormatter'], @@ -10198,16 +10198,15 @@ 'datefmt_get_locale' => ['string|false', 'formatter'=>'IntlDateFormatter', 'type='=>'int'], 'datefmt_get_pattern' => ['string', 'formatter'=>'IntlDateFormatter'], 'datefmt_get_timetype' => ['int', 'formatter'=>'IntlDateFormatter'], - 'datefmt_get_timezone' => ['IntlTimeZone|false'], - 'datefmt_get_timezone_id' => ['string', 'formatter'=>'IntlDateFormatter'], + 'datefmt_get_timezone' => ['IntlTimeZone|false', 'formatter'=>'IntlDateFormatter'], + 'datefmt_get_timezone_id' => ['string|false', 'formatter'=>'IntlDateFormatter'], 'datefmt_is_lenient' => ['bool', 'formatter'=>'IntlDateFormatter'], - 'datefmt_localtime' => ['array|false', 'formatter'=>'IntlDateFormatter', 'string='=>'string', '&rw_offset='=>'int'], - 'datefmt_parse' => ['int|false', 'formatter'=>'IntlDateFormatter', 'string='=>'string', '&rw_offset='=>'int'], + 'datefmt_localtime' => ['array|false', 'formatter'=>'IntlDateFormatter', 'string'=>'string', '&rw_offset='=>'int'], + 'datefmt_parse' => ['int|false', 'formatter'=>'IntlDateFormatter', 'string'=>'string', '&rw_offset='=>'int'], 'datefmt_set_calendar' => ['bool', 'formatter'=>'IntlDateFormatter', 'calendar'=>'int'], 'datefmt_set_lenient' => ['?bool', 'formatter'=>'IntlDateFormatter', 'lenient'=>'bool'], 'datefmt_set_pattern' => ['bool', 'formatter'=>'IntlDateFormatter', 'pattern'=>'string'], - 'datefmt_set_timezone' => ['bool', 'formatter'=>'mixed'], - 'datefmt_set_timezone_id' => ['bool', 'fmt'=>'IntlDateFormatter', 'zone'=>'string'], + 'datefmt_set_timezone' => ['false|null', 'formatter'=>'IntlDateFormatter', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], 'db2_autocommit' => ['mixed', 'connection'=>'resource', 'value='=>'int'], 'db2_bind_param' => ['bool', 'stmt'=>'resource', 'parameter_number'=>'int', 'variable_name'=>'string', 'parameter_type='=>'int', 'data_type='=>'int', 'precision='=>'int', 'scale='=>'int'], 'db2_client_info' => ['object|false', 'connection'=>'resource'], @@ -10545,7 +10544,7 @@ 'explode' => ['list|false', 'separator'=>'string', 'string'=>'string', 'limit='=>'int'], 'expm1' => ['float', 'num'=>'float'], 'extension_loaded' => ['bool', 'extension'=>'string'], - 'extract' => ['int', '&rw_array'=>'array', 'flags='=>'int', 'prefix='=>'?string'], + 'extract' => ['int', '&rw_array'=>'array', 'flags='=>'int', 'prefix='=>'string'], 'ezmlm_hash' => ['int', 'addr'=>'string'], 'fam_cancel_monitor' => ['bool', 'fam'=>'resource', 'fam_monitor'=>'resource'], 'fam_close' => ['void', 'fam'=>'resource'], @@ -10863,8 +10862,8 @@ 'filter_input' => ['mixed|false', 'type'=>'int', 'var_name'=>'string', 'filter='=>'int', 'options='=>'array|int'], 'filter_input_array' => ['mixed|false', 'type'=>'int', 'options='=>'int|array', 'add_empty='=>'bool'], 'filter_list' => ['array'], - 'filter_var' => ['mixed|false', 'value'=>'mixed', 'filter='=>'int', 'options='=>'mixed'], - 'filter_var_array' => ['mixed|false', 'array'=>'array', 'options='=>'mixed', 'add_empty='=>'bool'], + 'filter_var' => ['mixed|false', 'value'=>'mixed', 'filter='=>'int', 'options='=>'array|int'], + 'filter_var_array' => ['array|false|null', 'array'=>'array', 'options='=>'array|int', 'add_empty='=>'bool'], 'finfo::__construct' => ['void', 'options='=>'int', 'magic_file='=>'string'], 'finfo::buffer' => ['string|false', 'string'=>'string', 'options='=>'int', 'context='=>'resource'], 'finfo::file' => ['string|false', 'file_name'=>'string', 'options='=>'int', 'context='=>'resource'], @@ -10895,7 +10894,7 @@ 'fscanf\'1' => ['int', 'stream'=>'resource', 'format'=>'string', '&...w_vars='=>'string|int|float'], 'fseek' => ['int', 'stream'=>'resource', 'offset'=>'int', 'whence='=>'int'], 'fsockopen' => ['resource|false', 'hostname'=>'string', 'port='=>'int', '&w_error_code='=>'int', '&w_error_message='=>'string', 'timeout='=>'float'], - 'fstat' => ['array|false', 'stream'=>'resource'], + 'fstat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'stream'=>'resource'], 'ftell' => ['int|false', 'stream'=>'resource'], 'ftok' => ['int', 'filename'=>'string', 'project_id'=>'string'], 'ftp_alloc' => ['bool', 'ftp'=>'resource', 'size'=>'int', '&w_response='=>'string'], @@ -12582,7 +12581,7 @@ 'log10' => ['float', 'num'=>'float'], 'log1p' => ['float', 'num'=>'float'], 'long2ip' => ['string', 'ip'=>'string|int'], - 'lstat' => ['array|false', 'filename'=>'string'], + 'lstat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'filename'=>'string'], 'ltrim' => ['string', 'string'=>'string', 'characters='=>'string'], 'lzf_compress' => ['string', 'data'=>'string'], 'lzf_decompress' => ['string', 'data'=>'string'], @@ -13897,7 +13896,7 @@ 'nsapi_virtual' => ['bool', 'uri'=>'string'], 'nthmac' => ['string', 'clent'=>'string', 'data'=>'string'], 'number_format' => ['string', 'num'=>'float|int', 'decimals='=>'int'], - 'number_format\'1' => ['string', 'num'=>'float|int', 'decimals'=>'int', 'decimal_separator'=>'string', 'thousands_separator'=>'string'], + 'number_format\'1' => ['string', 'num'=>'float|int', 'decimals'=>'int', 'decimal_separator'=>'?string', 'thousands_separator'=>'?string'], 'numfmt_create' => ['NumberFormatter|false', 'locale'=>'string', 'style'=>'int', 'pattern='=>'string'], 'numfmt_format' => ['string|false', 'formatter'=>'NumberFormatter', 'num'=>'int|float', 'type='=>'int'], 'numfmt_format_currency' => ['string|false', 'formatter'=>'NumberFormatter', 'amount'=>'float', 'currency'=>'string'], @@ -15079,18 +15078,18 @@ 'ssh2_scp_send' => ['bool', 'session'=>'resource', 'local_file'=>'string', 'remote_file'=>'string', 'create_mode='=>'int'], 'ssh2_sftp' => ['resource|false', 'session'=>'resource'], 'ssh2_sftp_chmod' => ['bool', 'sftp'=>'resource', 'filename'=>'string', 'mode'=>'int'], - 'ssh2_sftp_lstat' => ['array|false', 'sftp'=>'resource', 'path'=>'string'], + 'ssh2_sftp_lstat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'sftp'=>'resource', 'path'=>'string'], 'ssh2_sftp_mkdir' => ['bool', 'sftp'=>'resource', 'dirname'=>'string', 'mode='=>'int', 'recursive='=>'bool'], 'ssh2_sftp_readlink' => ['string|false', 'sftp'=>'resource', 'link'=>'string'], 'ssh2_sftp_realpath' => ['string|false', 'sftp'=>'resource', 'filename'=>'string'], 'ssh2_sftp_rename' => ['bool', 'sftp'=>'resource', 'from'=>'string', 'to'=>'string'], 'ssh2_sftp_rmdir' => ['bool', 'sftp'=>'resource', 'dirname'=>'string'], - 'ssh2_sftp_stat' => ['array|false', 'sftp'=>'resource', 'path'=>'string'], + 'ssh2_sftp_stat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'sftp'=>'resource', 'path'=>'string'], 'ssh2_sftp_symlink' => ['bool', 'sftp'=>'resource', 'target'=>'string', 'link'=>'string'], 'ssh2_sftp_unlink' => ['bool', 'sftp'=>'resource', 'filename'=>'string'], 'ssh2_shell' => ['resource|false', 'session'=>'resource', 'term_type='=>'string', 'env='=>'array', 'width='=>'int', 'height='=>'int', 'width_height_type='=>'int'], 'ssh2_tunnel' => ['resource|false', 'session'=>'resource', 'host'=>'string', 'port'=>'int'], - 'stat' => ['array|false', 'filename'=>'string'], + 'stat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'filename'=>'string'], 'stats_absolute_deviation' => ['float', 'a'=>'array'], 'stats_cdf_beta' => ['float', 'par1'=>'float', 'par2'=>'float', 'par3'=>'float', 'which'=>'int'], 'stats_cdf_binomial' => ['float', 'par1'=>'float', 'par2'=>'float', 'par3'=>'float', 'which'=>'int'], diff --git a/docs/annotating_code/type_syntax/scalar_types.md b/docs/annotating_code/type_syntax/scalar_types.md index cb9f9cd7a9b..1b328d1b606 100644 --- a/docs/annotating_code/type_syntax/scalar_types.md +++ b/docs/annotating_code/type_syntax/scalar_types.md @@ -12,7 +12,7 @@ The type `scalar` is the supertype of all scalar types. ### positive-int -`positive-int` allows only positive integers +`positive-int` allows only positive integers (equivalent to `int<1, max>`) ### numeric diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 657597fbea8..a7b98546ed9 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,7 +8,7 @@ beStrictAboutTestsThatDoNotTestAnything="false" beStrictAboutTodoAnnotatedTests="true" colors="true" - verbose="true" + verbose="false" executionOrder="random" > diff --git a/src/Psalm/CodeLocation.php b/src/Psalm/CodeLocation.php index 2b3bf2aabac..82c6bdad744 100644 --- a/src/Psalm/CodeLocation.php +++ b/src/Psalm/CodeLocation.php @@ -296,7 +296,9 @@ private function calculateRealLocation(): void } $this->snippet = mb_strcut($file_contents, $this->preview_start, $this->preview_end - $this->preview_start); - $this->text = mb_strcut($file_contents, $this->selection_start, $this->selection_end - $this->selection_start); + // text is within snippet. It's 50% faster to cut it from the snippet than from the full text + $selection_length = $this->selection_end - $this->selection_start; + $this->text = mb_strcut($this->snippet, $this->selection_start - $this->preview_start, $selection_length); // reset preview start to beginning of line if ($file_contents !== '') { diff --git a/src/Psalm/Internal/Algebra.php b/src/Psalm/Internal/Algebra.php index c43d584536d..1aa6da4861c 100644 --- a/src/Psalm/Internal/Algebra.php +++ b/src/Psalm/Internal/Algebra.php @@ -124,6 +124,7 @@ public static function simplifyCNF(array $clauses): array if (!$clause_a->reconcilable || $clause_a->wedge) { continue; } + $clause_a_keys = array_keys($clause_a->possibilities); if (count($clause_a->possibilities) !== 1 || count(array_values($clause_a->possibilities)[0]) !== 1) { foreach ($cloned_clauses as $clause_b) { @@ -131,7 +132,7 @@ public static function simplifyCNF(array $clauses): array continue; } - if (array_keys($clause_a->possibilities) === array_keys($clause_b->possibilities)) { + if ($clause_a_keys === array_keys($clause_b->possibilities)) { $opposing_keys = []; foreach ($clause_a->possibilities as $key => $a_possibilities) { diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index b96dbdd1a56..ff9bed24448 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -132,6 +132,7 @@ public function __construct(PhpParser\Node\Stmt $class, SourceAnalyzer $source, } } + /** @return non-empty-string */ public static function getAnonymousClassName(PhpParser\Node\Stmt\Class_ $class, string $file_path): string { return preg_replace('/[^A-Za-z0-9]/', '_', $file_path) @@ -248,7 +249,7 @@ public function analyze( } foreach ($storage->docblock_issues as $docblock_issue) { - IssueBuffer::add($docblock_issue); + IssueBuffer::maybeAdd($docblock_issue); } $classlike_storage_provider = $codebase->classlike_storage_provider; @@ -1640,7 +1641,7 @@ private function analyzeClassMethod( $config = Config::getInstance(); if ($stmt->stmts === null && !$stmt->isAbstract()) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new ParseError( 'Non-abstract class method must have statements', new CodeLocation($this, $stmt) @@ -1653,7 +1654,7 @@ private function analyzeClassMethod( try { $method_analyzer = new MethodAnalyzer($stmt, $source); } catch (UnexpectedValueException $e) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new ParseError( 'Problem loading method: ' . $e->getMessage(), new CodeLocation($this, $stmt) @@ -2323,11 +2324,12 @@ private function checkParentClass( ); } - if (!NamespaceAnalyzer::isWithin($fq_class_name, $parent_class_storage->internal)) { + if (!NamespaceAnalyzer::isWithinAny($fq_class_name, $parent_class_storage->internal)) { IssueBuffer::maybeAdd( new InternalClass( - $parent_fq_class_name . ' is internal to ' . $parent_class_storage->internal - . ' but called from ' . $fq_class_name, + $parent_fq_class_name . ' is internal to ' + . InternalClass::listToPhrase($parent_class_storage->internal) + . ' but called from ' . $fq_class_name, $code_location, $parent_fq_class_name ), diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index 7466e5557ce..5af3a43e8a3 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -9,6 +9,7 @@ use Psalm\Exception\IncorrectDocblockException; use Psalm\Exception\TypeParseTreeException; use Psalm\FileSource; +use Psalm\Internal\Scanner\DocblockParser; use Psalm\Internal\Scanner\ParsedDocblock; use Psalm\Internal\Scanner\VarDocblockComment; use Psalm\Internal\Type\TypeAlias; @@ -21,7 +22,6 @@ use function preg_match; use function preg_replace; use function preg_split; -use function reset; use function rtrim; use function str_replace; use function strlen; @@ -236,14 +236,7 @@ private static function decorateVarDocblockComment( } } - if (isset($parsed_docblock->tags['psalm-internal'])) { - $psalm_internal = trim(reset($parsed_docblock->tags['psalm-internal'])); - - if (!$psalm_internal) { - throw new DocblockParseException('psalm-internal annotation used without specifying namespace'); - } - - $var_comment->psalm_internal = $psalm_internal; + if (count($var_comment->psalm_internal = DocblockParser::handlePsalmInternal($parsed_docblock)) !== 0) { $var_comment->internal = true; } diff --git a/src/Psalm/Internal/Analyzer/FileAnalyzer.php b/src/Psalm/Internal/Analyzer/FileAnalyzer.php index e59a1790789..0d57d4c24e1 100644 --- a/src/Psalm/Internal/Analyzer/FileAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FileAnalyzer.php @@ -196,7 +196,7 @@ public function analyze( $statements_analyzer = new StatementsAnalyzer($this, $this->node_data); foreach ($file_storage->docblock_issues as $docblock_issue) { - IssueBuffer::add($docblock_issue); + IssueBuffer::maybeAdd($docblock_issue); } // if there are any leftover statements, evaluate them, diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 2b92d9633c2..29fc51a3080 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -211,7 +211,7 @@ public function analyze( } foreach ($storage->docblock_issues as $docblock_issue) { - IssueBuffer::add($docblock_issue); + IssueBuffer::maybeAdd($docblock_issue); } $function_information = $this->getFunctionInformation( @@ -1271,15 +1271,17 @@ private function processParams( $context->hasVariable('$' . $function_param->name); } - AttributesAnalyzer::analyze( - $this, - $context, - $function_param, - $param_stmts[$offset]->attrGroups, - AttributesAnalyzer::TARGET_PARAMETER - | ($function_param->promoted_property ? AttributesAnalyzer::TARGET_PROPERTY : 0), - $storage->suppressed_issues + $this->getSuppressedIssues() - ); + if (count($param_stmts) === count($params)) { + AttributesAnalyzer::analyze( + $this, + $context, + $function_param, + $param_stmts[$offset]->attrGroups, + AttributesAnalyzer::TARGET_PARAMETER + | ($function_param->promoted_property ? AttributesAnalyzer::TARGET_PROPERTY : 0), + $storage->suppressed_issues + $this->getSuppressedIssues() + ); + } } return $check_stmts; diff --git a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php index 90e04e3301b..c714fa51b63 100644 --- a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php @@ -155,7 +155,7 @@ public function analyze(): void ); } } elseif ($stmt instanceof PhpParser\Node\Stmt\Property) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new ParseError( 'Interfaces cannot have properties', new CodeLocation($this, $stmt) diff --git a/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php b/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php index 436e47bcd7f..06413e6bbbf 100644 --- a/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php @@ -12,11 +12,13 @@ use ReflectionProperty; use UnexpectedValueException; +use function assert; +use function count; use function implode; use function preg_replace; use function strpos; use function strtolower; -use function trim; +use function substr; /** * @internal @@ -152,33 +154,127 @@ public function getFileAnalyzer(): FileAnalyzer } /** - * Returns true if $className is the same as, or starts with $namespace, in a case-insensitive comparison. + * Returns true if $calling_identifier is the same as, or is within with $identifier, in a + * case-insensitive comparison. Identifiers can be namespaces, classlikes, functions, or methods. * + * @psalm-pure + * + * @throws InvalidArgumentException if $identifier is not a valid identifier + */ + public static function isWithin(string $calling_identifier, string $identifier): bool + { + $normalized_calling_ident = self::normalizeIdentifier($calling_identifier); + $normalized_ident = self::normalizeIdentifier($identifier); + + if ($normalized_calling_ident === $normalized_ident) { + return true; + } + + $normalized_calling_ident_parts = self::getIdentifierParts($normalized_calling_ident); + $normalized_ident_parts = self::getIdentifierParts($normalized_ident); + + if (count($normalized_calling_ident_parts) < count($normalized_ident_parts)) { + return false; + } + + for ($i = 0; $i < count($normalized_ident_parts); ++$i) { + if ($normalized_ident_parts[$i] !== $normalized_calling_ident_parts[$i]) { + return false; + } + } + + return true; + } + + /** + * Returns true if $calling_identifier is the same as or is within any identifier + * in $identifiers in a case-insensitive comparison, or if $identifiers is empty. + * Identifiers can be namespaces, classlikes, functions, or methods. * * @psalm-pure + * + * @psalm-assert-if-false !empty $identifiers + * + * @param list $identifiers */ - public static function isWithin(string $calling_namespace, string $namespace): bool + public static function isWithinAny(string $calling_identifier, array $identifiers): bool { - if ($namespace === '') { - return true; // required to prevent a warning from strpos with empty needle in PHP < 8 + if (count($identifiers) === 0) { + return true; } - $calling_namespace = strtolower(trim($calling_namespace, '\\') . '\\'); - $namespace = strtolower(trim($namespace, '\\') . '\\'); + foreach ($identifiers as $identifier) { + if (self::isWithin($calling_identifier, $identifier)) { + return true; + } + } - return $calling_namespace === $namespace - || strpos($calling_namespace, $namespace) === 0; + return false; } /** - * @param string $fullyQualifiedClassName, e.g. '\Psalm\Internal\Analyzer\NamespaceAnalyzer' + * @param non-empty-string $fullyQualifiedClassName, e.g. '\Psalm\Internal\Analyzer\NamespaceAnalyzer' * - * @return string , e.g. 'Psalm' + * @return non-empty-string , e.g. 'Psalm' * * @psalm-pure */ public static function getNameSpaceRoot(string $fullyQualifiedClassName): string { - return preg_replace('/^([^\\\]+).*/', '$1', $fullyQualifiedClassName); + $root_namespace = preg_replace('/^([^\\\]+).*/', '$1', $fullyQualifiedClassName); + if ($root_namespace === "") { + throw new InvalidArgumentException("Invalid classname \"$fullyQualifiedClassName\""); + } + return $root_namespace; + } + + /** + * @return ($lowercase is true ? lowercase-string : string) + * + * @psalm-pure + */ + public static function normalizeIdentifier(string $identifier, bool $lowercase = true): string + { + if ($identifier === "") { + return ""; + } + + $identifier = $identifier[0] === "\\" ? substr($identifier, 1) : $identifier; + return $lowercase ? strtolower($identifier) : $identifier; + } + + /** + * Splits an identifier into parts, eg `Foo\Bar::baz` becomes ["Foo", "\\", "Bar", "::", "baz"]. + * + * @return list + * + * @psalm-pure + */ + public static function getIdentifierParts(string $identifier): array + { + $parts = []; + while (($pos = strpos($identifier, "\\")) !== false) { + if ($pos > 0) { + $part = substr($identifier, 0, $pos); + assert($part !== ""); + $parts[] = $part; + } + $parts[] = "\\"; + $identifier = substr($identifier, $pos + 1); + } + if (($pos = strpos($identifier, "::")) !== false) { + if ($pos > 0) { + $part = substr($identifier, 0, $pos); + assert($part !== ""); + $parts[] = $part; + } + $parts[] = "::"; + $identifier = substr($identifier, $pos + 2); + } + if ($identifier !== "") { + $parts[] = $identifier; + } + + return $parts; } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index b87e344d62c..a5c755a860f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -74,7 +74,7 @@ public static function analyze( foreach ($stmt->items as $item) { if ($item === null) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new ParseError( 'Array element cannot be empty', new CodeLocation($statements_analyzer, $stmt) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index f5723f47918..d49901cc710 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -3,6 +3,7 @@ namespace Psalm\Internal\Analyzer\Statements\Expression; use PhpParser; +use PhpParser\Node\Expr\BinaryOp; use PhpParser\Node\Expr\BinaryOp\Equal; use PhpParser\Node\Expr\BinaryOp\Greater; use PhpParser\Node\Expr\BinaryOp\GreaterOrEqual; @@ -995,7 +996,7 @@ protected static function processCustomAssertion( } } elseif ($assertion->var_id === '$this') { if (!$expr instanceof PhpParser\Node\Expr\MethodCall) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new InvalidDocblock( 'Assertion of $this can be done only on method of a class', new CodeLocation($source, $expr) @@ -1533,50 +1534,35 @@ protected static function hasNonEmptyCountEqualityCheck( PhpParser\Node\Expr\BinaryOp $conditional, ?int &$min_count ) { - $left_count = $conditional->left instanceof PhpParser\Node\Expr\FuncCall + if ($conditional->left instanceof PhpParser\Node\Expr\FuncCall && $conditional->left->name instanceof PhpParser\Node\Name && strtolower($conditional->left->name->parts[0]) === 'count' - && $conditional->left->getArgs(); - - $operator_greater_than_or_equal = - $conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater - || $conditional instanceof PhpParser\Node\Expr\BinaryOp\GreaterOrEqual; - - if ($left_count - && $conditional->right instanceof PhpParser\Node\Scalar\LNumber - && $operator_greater_than_or_equal - && $conditional->right->value >= ( - $conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater - ? 0 - : 1 - ) + && $conditional->left->getArgs() + && ($conditional instanceof BinaryOp\Greater || $conditional instanceof BinaryOp\GreaterOrEqual) ) { - $min_count = $conditional->right->value + - ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater ? 1 : 0); - - return self::ASSIGNMENT_TO_RIGHT; - } - - $right_count = $conditional->right instanceof PhpParser\Node\Expr\FuncCall + $assignment_to = self::ASSIGNMENT_TO_RIGHT; + $compare_to = $conditional->right; + $comparison_adjustment = $conditional instanceof BinaryOp\Greater ? 1 : 0; + } elseif ($conditional->right instanceof PhpParser\Node\Expr\FuncCall && $conditional->right->name instanceof PhpParser\Node\Name && strtolower($conditional->right->name->parts[0]) === 'count' - && $conditional->right->getArgs(); - - $operator_less_than_or_equal = - $conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller - || $conditional instanceof PhpParser\Node\Expr\BinaryOp\SmallerOrEqual; + && $conditional->right->getArgs() + && ($conditional instanceof BinaryOp\Smaller || $conditional instanceof BinaryOp\SmallerOrEqual) + ) { + $assignment_to = self::ASSIGNMENT_TO_LEFT; + $compare_to = $conditional->left; + $comparison_adjustment = $conditional instanceof BinaryOp\Smaller ? 1 : 0; + } else { + return false; + } - if ($right_count - && $conditional->left instanceof PhpParser\Node\Scalar\LNumber - && $operator_less_than_or_equal - && $conditional->left->value >= ( - $conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller ? 0 : 1 - ) + // TODO get node type provider here somehow and check literal ints and int ranges + if ($compare_to instanceof PhpParser\Node\Scalar\LNumber + && $compare_to->value > (-1 * $comparison_adjustment) ) { - $min_count = $conditional->left->value + - ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller ? 1 : 0); + $min_count = $compare_to->value + $comparison_adjustment; - return self::ASSIGNMENT_TO_LEFT; + return $assignment_to; } return false; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php index 462544d55b6..049677a9d36 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php @@ -37,6 +37,7 @@ use Psalm\Issue\ImplicitToStringCast; use Psalm\Issue\ImpurePropertyAssignment; use Psalm\Issue\InaccessibleProperty; +use Psalm\Issue\InternalClass; use Psalm\Issue\InternalProperty; use Psalm\Issue\InvalidPropertyAssignment; use Psalm\Issue\InvalidPropertyAssignmentValue; @@ -420,7 +421,7 @@ public static function analyzeStatement( foreach ($stmt->props as $prop) { if ($prop->default) { if ($stmt->isReadonly()) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new InvalidPropertyAssignment( 'Readonly property ' . $context->self . '::$' . $prop->name->name . ' cannot have a default', @@ -1290,11 +1291,11 @@ private static function analyzeAtomicAssignment( ); } - if ($context->self && !NamespaceAnalyzer::isWithin($context->self, $property_storage->internal)) { + if ($context->self && !NamespaceAnalyzer::isWithinAny($context->self, $property_storage->internal)) { IssueBuffer::maybeAdd( new InternalProperty( - $property_id . ' is internal to ' . $property_storage->internal - . ' but called from ' . $context->self, + $property_id . ' is internal to ' . InternalClass::listToPhrase($property_storage->internal) + . ' but called from ' . $context->self, new CodeLocation($statements_analyzer->getSource(), $stmt), $property_id ), diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php index 80ba8e84fe7..d5a0beda4a8 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php @@ -29,7 +29,6 @@ use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TIntRange; -use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TLowercaseString; use Psalm\Type\Atomic\TNamedObject; @@ -53,6 +52,8 @@ */ class ConcatAnalyzer { + private const MAX_LITERALS = 64; + /** * @param Union|null $result_type */ @@ -156,39 +157,35 @@ public static function analyze( self::analyzeOperand($statements_analyzer, $left, $left_type, 'Left', $context); self::analyzeOperand($statements_analyzer, $right, $right_type, 'Right', $context); - // If one of the types is a single int or string literal, and the other - // type is all string or int literals, combine them into new literal(s). + // If both types are specific literals, combine them into new literals $literal_concat = false; - if (($left_type->allStringLiterals() || $left_type->allIntLiterals()) - && ($right_type->allStringLiterals() || $right_type->allIntLiterals()) - ) { - $literal_concat = true; - $result_type_parts = []; - - foreach ($left_type->getAtomicTypes() as $left_type_part) { - assert($left_type_part instanceof TLiteralString || $left_type_part instanceof TLiteralInt); - foreach ($right_type->getAtomicTypes() as $right_type_part) { - assert($right_type_part instanceof TLiteralString || $right_type_part instanceof TLiteralInt); - $literal = $left_type_part->value . $right_type_part->value; - if (strlen($literal) >= $config->max_string_length) { - // Literal too long, use non-literal type instead - $literal_concat = false; - break 2; + if ($left_type->allSpecificLiterals() && $right_type->allSpecificLiterals()) { + $left_type_parts = $left_type->getAtomicTypes(); + $right_type_parts = $right_type->getAtomicTypes(); + $combinations = count($left_type_parts) * count($right_type_parts); + if ($combinations < self::MAX_LITERALS) { + $literal_concat = true; + $result_type_parts = []; + + foreach ($left_type->getAtomicTypes() as $left_type_part) { + foreach ($right_type->getAtomicTypes() as $right_type_part) { + $literal = $left_type_part->value . $right_type_part->value; + if (strlen($literal) >= $config->max_string_length) { + // Literal too long, use non-literal type instead + $literal_concat = false; + break 2; + } + + $result_type_parts[] = new TLiteralString($literal); } - - $result_type_parts[] = new TLiteralString($literal); } - } - if (!empty($result_type_parts)) { - if ($literal_concat && count($result_type_parts) < 64) { + if ($literal_concat) { + assert(count($result_type_parts) === $combinations); + assert(count($result_type_parts) !== 0); // #8163 $result_type = new Union($result_type_parts); - } else { - $result_type = new Union([new TNonEmptyNonspecificLiteralString]); } - - return; } } 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 c07110f5688..b16fadae07a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -279,7 +279,7 @@ public static function analyze( $codebase, $context, $method_id, - $statements_analyzer->getNamespace(), + $statements_analyzer->getFullyQualifiedFunctionMethodOrNamespaceName(), $name_code_location, $statements_analyzer->getSuppressedIssues() ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallProhibitionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallProhibitionAnalyzer.php index 255bc8af2e4..fcb44cb9e1d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallProhibitionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallProhibitionAnalyzer.php @@ -8,6 +8,7 @@ use Psalm\Internal\Analyzer\NamespaceAnalyzer; use Psalm\Internal\MethodIdentifier; use Psalm\Issue\DeprecatedMethod; +use Psalm\Issue\InternalClass; use Psalm\Issue\InternalMethod; use Psalm\IssueBuffer; @@ -25,7 +26,7 @@ public static function analyze( Codebase $codebase, Context $context, MethodIdentifier $method_id, - ?string $namespace, + ?string $caller_identifier, CodeLocation $code_location, array $suppressed_issues ): ?bool { @@ -54,12 +55,12 @@ public static function analyze( if (!$context->collect_initializations && !$context->collect_mutations ) { - if (!NamespaceAnalyzer::isWithin($namespace ?: '', $storage->internal)) { + if (!NamespaceAnalyzer::isWithinAny($caller_identifier ?? "", $storage->internal)) { IssueBuffer::maybeAdd( new InternalMethod( 'The method ' . $codebase_methods->getCasedMethodId($method_id) - . ' is internal to ' . $storage->internal - . ' but called from ' . ($context->self ?: 'root namespace'), + . ' is internal to ' . InternalClass::listToPhrase($storage->internal) + . ' but called from ' . ($caller_identifier ?: 'root namespace'), $code_location, (string) $method_id ), diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php index bf368dc072f..b5f9c38e7b8 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php @@ -350,12 +350,12 @@ private static function analyzeNamedConstructor( if ($context->self && !$context->collect_initializations && !$context->collect_mutations - && !NamespaceAnalyzer::isWithin($context->self, $storage->internal) + && !NamespaceAnalyzer::isWithinAny($context->self, $storage->internal) ) { IssueBuffer::maybeAdd( new InternalClass( - $fq_class_name . ' is internal to ' . $storage->internal - . ' but called from ' . $context->self, + $fq_class_name . ' is internal to ' . InternalClass::listToPhrase($storage->internal) + . ' but called from ' . $context->self, new CodeLocation($statements_analyzer->getSource(), $stmt), $fq_class_name ), @@ -412,16 +412,13 @@ private static function analyzeNamedConstructor( if ($declaring_method_id) { $method_storage = $codebase->methods->getStorage($declaring_method_id); - $namespace = $statements_analyzer->getNamespace() ?: ''; - if (!NamespaceAnalyzer::isWithin( - $namespace, - $method_storage->internal - )) { + $caller_identifier = $statements_analyzer->getFullyQualifiedFunctionMethodOrNamespaceName() ?: ''; + if (!NamespaceAnalyzer::isWithinAny($caller_identifier, $method_storage->internal)) { IssueBuffer::maybeAdd( new InternalMethod( 'Constructor ' . $codebase->methods->getCasedMethodId($declaring_method_id) - . ' is internal to ' . $method_storage->internal - . ' but called from ' . ($namespace ?: 'root namespace'), + . ' is internal to ' . InternalClass::listToPhrase($method_storage->internal) + . ' but called from ' . ($caller_identifier ?: 'root namespace'), new CodeLocation($statements_analyzer, $stmt), (string) $method_id ), diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php index 296af44417e..619868af958 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -798,10 +798,10 @@ private static function handleNamedCall( ); } - if ($context->self && ! NamespaceAnalyzer::isWithin($context->self, $class_storage->internal)) { + if ($context->self && ! NamespaceAnalyzer::isWithinAny($context->self, $class_storage->internal)) { IssueBuffer::maybeAdd( new InternalClass( - $fq_class_name . ' is internal to ' . $class_storage->internal + $fq_class_name . ' is internal to ' . InternalClass::listToPhrase($class_storage->internal) . ' but called from ' . $context->self, new CodeLocation($statements_analyzer->getSource(), $stmt), $fq_class_name 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 e98cbcdfe67..f7e0d7e09aa 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -75,7 +75,7 @@ public static function analyze( $codebase, $context, $method_id, - $statements_analyzer->getNamespace(), + $statements_analyzer->getFullyQualifiedFunctionMethodOrNamespaceName(), new CodeLocation($statements_analyzer->getSource(), $stmt), $statements_analyzer->getSuppressedIssues() ) === false) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index 5206ea6e830..968e9aef79c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -690,7 +690,7 @@ public static function applyAssertionsToContext( $exploded = explode('->', $var_possibilities->var_id); if (count($exploded) < 2) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new InvalidDocblock( 'Assert notation is malformed', new CodeLocation($statements_analyzer, $expr) @@ -704,7 +704,7 @@ public static function applyAssertionsToContext( $var_id = is_numeric($var_id) ? (int) $var_id : $var_id; if (!is_int($var_id) || !isset($args[$var_id])) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new InvalidDocblock( 'Variable ' . $var_id . ' is not an argument so cannot be asserted', new CodeLocation($statements_analyzer, $expr) @@ -719,7 +719,7 @@ public static function applyAssertionsToContext( $arg_var_id = ExpressionIdentifier::getExtendedVarId($arg_value, null, $statements_analyzer); if (!$arg_var_id) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new InvalidDocblock( 'Variable being asserted as argument ' . ($var_id+1) . ' cannot be found in local scope', new CodeLocation($statements_analyzer, $expr) @@ -737,7 +737,7 @@ public static function applyAssertionsToContext( ); if (null !== $failedMessage) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new InvalidDocblock($failedMessage, new CodeLocation($statements_analyzer, $expr)) ); continue; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index cc017140f8b..16f799609f0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -27,6 +27,7 @@ use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TList; +use Psalm\Type\Atomic\TLiteralFloat; use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; @@ -336,7 +337,7 @@ public static function castStringAttempt( || $atomic_type instanceof TInt || $atomic_type instanceof TNumeric ) { - if ($atomic_type instanceof TLiteralInt) { + if ($atomic_type instanceof TLiteralInt || $atomic_type instanceof TLiteralFloat) { $castable_types[] = new TLiteralString((string) $atomic_type->value); } elseif ($atomic_type instanceof TNonspecificLiteralInt) { $castable_types[] = new TNonspecificLiteralString(); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php index 135bdb80161..923829c562c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php @@ -357,12 +357,12 @@ public static function analyzeFetch( if ($context->self && !$context->collect_initializations && !$context->collect_mutations - && $const_class_storage->internal - && !NamespaceAnalyzer::isWithin($context->self, $const_class_storage->internal) + && !NamespaceAnalyzer::isWithinAny($context->self, $const_class_storage->internal) ) { IssueBuffer::maybeAdd( new InternalClass( - $fq_class_name . ' is internal to ' . $const_class_storage->internal + $fq_class_name . ' is internal to ' + . InternalClass::listToPhrase($const_class_storage->internal) . ' but called from ' . $context->self, new CodeLocation($statements_analyzer->getSource(), $stmt), $fq_class_name @@ -633,13 +633,13 @@ public static function analyzeFetch( if ($context->self && !$context->collect_initializations && !$context->collect_mutations - && $const_class_storage->internal - && !NamespaceAnalyzer::isWithin($context->self, $const_class_storage->internal) + && !NamespaceAnalyzer::isWithinAny($context->self, $const_class_storage->internal) ) { IssueBuffer::maybeAdd( new InternalClass( - $fq_class_name . ' is internal to ' . $const_class_storage->internal - . ' but called from ' . $context->self, + $fq_class_name . ' is internal to ' + . InternalClass::listToPhrase($const_class_storage->internal) + . ' but called from ' . $context->self, new CodeLocation($statements_analyzer->getSource(), $stmt), $fq_class_name ), diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php index d39a94178a0..26b8718c932 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php @@ -3,6 +3,7 @@ namespace Psalm\Internal\Analyzer\Statements\Expression; use PhpParser; +use PhpParser\Node\Scalar\EncapsedStringPart; use Psalm\CodeLocation; use Psalm\Context; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; @@ -10,10 +11,16 @@ use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; use Psalm\Type; +use Psalm\Type\Atomic\TLiteralFloat; +use Psalm\Type\Atomic\TLiteralInt; +use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TNonEmptyNonspecificLiteralString; use Psalm\Type\Atomic\TNonEmptyString; +use Psalm\Type\Atomic\TNonspecificLiteralInt; +use Psalm\Type\Atomic\TNonspecificLiteralString; use Psalm\Type\Union; +use function assert; use function in_array; /** @@ -32,20 +39,16 @@ public static function analyze( $all_literals = true; - foreach ($stmt->parts as $part) { - if ($part instanceof PhpParser\Node\Scalar\EncapsedStringPart - && $part->value - ) { - $non_empty = true; - } + $literal_string = ""; + foreach ($stmt->parts as $part) { if (ExpressionAnalyzer::analyze($statements_analyzer, $part, $context) === false) { return false; } $part_type = $statements_analyzer->node_data->getType($part); - if ($part_type) { + if ($part_type !== null) { $casted_part_type = CastAnalyzer::castStringAttempt( $statements_analyzer, $context, @@ -55,6 +58,28 @@ public static function analyze( if (!$casted_part_type->allLiterals()) { $all_literals = false; + } elseif (!$non_empty) { + // Check if all literals are nonempty + $non_empty = true; + foreach ($casted_part_type->getAtomicTypes() as $atomic_literal) { + if (!$atomic_literal instanceof TLiteralInt + && !$atomic_literal instanceof TNonspecificLiteralInt + && !$atomic_literal instanceof TLiteralFloat + && !$atomic_literal instanceof TNonEmptyNonspecificLiteralString + && !($atomic_literal instanceof TLiteralString && $atomic_literal->value !== "") + ) { + $non_empty = false; + break; + } + } + } + + if ($literal_string !== null) { + if ($casted_part_type->isSingleLiteral()) { + $literal_string .= $casted_part_type->getSingleLiteral()->value; + } else { + $literal_string = null; + } } if ($statements_analyzer->data_flow_graph @@ -85,16 +110,30 @@ public static function analyze( } } } + } elseif ($part instanceof EncapsedStringPart) { + if ($literal_string !== null) { + $literal_string .= $part->value; + } + $non_empty = $non_empty || $part->value !== ""; + } else { + $all_literals = false; + $literal_string = null; } } if ($non_empty) { - if ($all_literals) { + if ($literal_string !== null) { + $new_type = Type::getString($literal_string); + } elseif ($all_literals) { $new_type = new Union([new TNonEmptyNonspecificLiteralString()]); } else { $new_type = new Union([new TNonEmptyString()]); } - + } elseif ($all_literals) { + $new_type = new Union([new TNonspecificLiteralString()]); + } + if (isset($new_type)) { + assert($new_type instanceof Union); $new_type->parent_nodes = $stmt_type->parent_nodes; $stmt_type = $new_type; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index 1576f92f8f2..57bfbdce1f4 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php @@ -508,7 +508,7 @@ public static function getArrayAccessTypeGivenOffset( if ($in_assignment) { $offset_type->removeType('null'); - $offset_type->addType(new TLiteralInt(0)); + $offset_type->addType(new TLiteralString('')); } } @@ -528,7 +528,7 @@ public static function getArrayAccessTypeGivenOffset( $offset_type->removeType('null'); if (!$offset_type->ignore_nullable_issues) { - $offset_type->addType(new TLiteralInt(0)); + $offset_type->addType(new TLiteralString('')); } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index f98f99d5f4d..d9c83bdc4ee 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -29,6 +29,7 @@ use Psalm\Internal\Type\TypeExpander; use Psalm\Issue\DeprecatedProperty; use Psalm\Issue\ImpurePropertyFetch; +use Psalm\Issue\InternalClass; use Psalm\Issue\InternalProperty; use Psalm\Issue\MissingPropertyType; use Psalm\Issue\NoInterfaceProperties; @@ -63,7 +64,7 @@ use function array_keys; use function array_search; -use function array_values; +use function count; use function in_array; use function is_int; use function is_string; @@ -405,10 +406,10 @@ public static function analyze( $property_storage = $declaring_class_storage->properties[$prop_name]; - if ($context->self && !NamespaceAnalyzer::isWithin($context->self, $property_storage->internal)) { + if ($context->self && !NamespaceAnalyzer::isWithinAny($context->self, $property_storage->internal)) { IssueBuffer::maybeAdd( new InternalProperty( - $property_id . ' is internal to ' . $property_storage->internal + $property_id . ' is internal to ' . InternalClass::listToPhrase($property_storage->internal) . ' but called from ' . $context->self, new CodeLocation($statements_analyzer->getSource(), $stmt), $property_id @@ -570,15 +571,9 @@ private static function propertyFetchCanBeAnalyzed( $class_storage->parent_class ); - if ($class_storage->template_types) { + if (count($template_types = $class_storage->getClassTemplateTypes()) !== 0) { if (!$lhs_type_part instanceof TGenericObject) { - $type_params = []; - - foreach ($class_storage->template_types as $type_map) { - $type_params[] = clone array_values($type_map)[0]; - } - - $lhs_type_part = new TGenericObject($lhs_type_part->value, $type_params); + $lhs_type_part = new TGenericObject($lhs_type_part->value, $template_types); } $stmt_type = self::localizePropertyType( @@ -1131,15 +1126,9 @@ private static function handleNonExistentProperty( ) { $stmt_type = clone $class_storage->pseudo_property_get_types['$' . $prop_name]; - if ($class_storage->template_types) { + if (count($template_types = $class_storage->getClassTemplateTypes()) !== 0) { if (!$lhs_type_part instanceof TGenericObject) { - $type_params = []; - - foreach ($class_storage->template_types as $type_map) { - $type_params[] = clone array_values($type_map)[0]; - } - - $lhs_type_part = new TGenericObject($lhs_type_part->value, $type_params); + $lhs_type_part = new TGenericObject($lhs_type_part->value, $template_types); } $stmt_type = self::localizePropertyType( @@ -1231,15 +1220,9 @@ private static function getClassPropertyType( $declaring_class_storage->parent_class ); - if ($declaring_class_storage->template_types) { + if (count($template_types = $declaring_class_storage->getClassTemplateTypes()) !== 0) { if (!$lhs_type_part instanceof TGenericObject) { - $type_params = []; - - foreach ($declaring_class_storage->template_types as $type_map) { - $type_params[] = clone array_values($type_map)[0]; - } - - $lhs_type_part = new TGenericObject($lhs_type_part->value, $type_params); + $lhs_type_part = new TGenericObject($lhs_type_part->value, $template_types); } $class_property_type = self::localizePropertyType( diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index ea6b490bf29..39722e39639 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -70,6 +70,7 @@ use function array_map; use function array_merge; use function array_search; +use function assert; use function count; use function explode; use function fwrite; @@ -1162,4 +1163,26 @@ public function getNodeTypeProvider(): NodeTypeProvider { return $this->node_data; } + + public function getFullyQualifiedFunctionMethodOrNamespaceName(): ?string + { + if ($this->source instanceof MethodAnalyzer) { + $fqcn = $this->getFQCLN(); + $method_name = $this->source->getFunctionLikeStorage($this)->cased_name; + assert($fqcn !== null && $method_name !== null); + + return "$fqcn::$method_name"; + } + + if ($this->source instanceof FunctionAnalyzer) { + $namespace = $this->getNamespace(); + $namespace = $namespace === "" ? "" : "$namespace\\"; + $function_name = $this->source->getFunctionLikeStorage($this)->cased_name; + assert($function_name !== null); + + return "{$namespace}{$function_name}"; + } + + return $this->getNamespace(); + } } diff --git a/src/Psalm/Internal/Clause.php b/src/Psalm/Internal/Clause.php index d61cea5f3bd..30ec50acb0c 100644 --- a/src/Psalm/Internal/Clause.php +++ b/src/Psalm/Internal/Clause.php @@ -12,13 +12,15 @@ use function array_diff; use function array_keys; use function count; +use function hash; use function implode; use function ksort; -use function md5; use function reset; use function serialize; use function substr; +use const PHP_VERSION_ID; + /** * @internal * @@ -100,12 +102,15 @@ public function __construct( $possibility_strings = []; - foreach ($possibilities as $i => $_) { - ksort($possibilities[$i]); - $possibility_strings[$i] = array_keys($possibilities[$i]); + foreach ($possibilities as $i => $v) { + if (count($v) > 1) { + ksort($v); + } + $possibility_strings[$i] = array_keys($v); } - $this->hash = md5(serialize($possibility_strings)); + $data = serialize($possibility_strings); + $this->hash = PHP_VERSION_ID >= 8_01_00 ? hash('xxh128', $data) : hash('md4', $data); } $this->possibilities = $possibilities; @@ -123,8 +128,14 @@ public function contains(Clause $other_clause): bool return false; } + foreach ($other_clause->possibilities as $var => $_) { + if (!isset($this->possibilities[$var])) { + return false; + } + } + foreach ($other_clause->possibilities as $var => $possible_types) { - if (!isset($this->possibilities[$var]) || count(array_diff($possible_types, $this->possibilities[$var]))) { + if (count(array_diff($possible_types, $this->possibilities[$var]))) { return false; } } diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index 458d8c14f26..de22e04a5a2 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -26,7 +26,6 @@ use function in_array; use function key; use function reset; -use function strlen; use function strpos; use function strtolower; @@ -233,16 +232,11 @@ private function populateClassLikeStorage(ClassLikeStorage $storage, array $depe if (!$storage->is_interface && !$storage->is_trait) { foreach ($storage->methods as $method) { - if (strlen($storage->internal) > strlen($method->internal)) { - $method->internal = $storage->internal; - } + $method->internal = array_merge($storage->internal, $method->internal); } - foreach ($storage->properties as $property) { - if (strlen($storage->internal) > strlen($property->internal)) { - $property->internal = $storage->internal; - } + $property->internal = array_merge($storage->internal, $property->internal); } } diff --git a/src/Psalm/Internal/Codebase/Reflection.php b/src/Psalm/Internal/Codebase/Reflection.php index cb2a4addb4c..893a02b7ec4 100644 --- a/src/Psalm/Internal/Codebase/Reflection.php +++ b/src/Psalm/Internal/Codebase/Reflection.php @@ -419,7 +419,7 @@ public function registerFunction(string $function_id): ?bool return null; } - /** @psalm-suppress UndefinedClass,TypeDoesNotContainType 7.4 has no ReflectionUnionType */ + /** @psalm-suppress UnusedPsalmSuppress,UndefinedClass,TypeDoesNotContainType 7.4 has no ReflectionUnionType */ public static function getPsalmTypeFromReflectionType(?ReflectionType $reflection_type = null): Union { if (!$reflection_type) { diff --git a/src/Psalm/Internal/Codebase/VariableUseGraph.php b/src/Psalm/Internal/Codebase/VariableUseGraph.php index d07772d9bad..d92b6568d06 100644 --- a/src/Psalm/Internal/Codebase/VariableUseGraph.php +++ b/src/Psalm/Internal/Codebase/VariableUseGraph.php @@ -21,6 +21,9 @@ class VariableUseGraph extends DataFlowGraph /** @var array */ private $nodes = []; + /** @var array> */ + private $origin_locations_by_id = []; + public function addNode(DataFlowNode $node): void { $this->nodes[$node->id] = $node; @@ -97,6 +100,10 @@ public function isVariableUsed(DataFlowNode $assignment_node): bool */ public function getOriginLocations(DataFlowNode $assignment_node): array { + if (isset($this->origin_locations_by_id[$assignment_node->id])) { + return $this->origin_locations_by_id[$assignment_node->id]; + } + $visited_child_ids = []; $origin_locations = []; @@ -131,6 +138,8 @@ public function getOriginLocations(DataFlowNode $assignment_node): array $child_nodes = $new_parent_nodes; } + $this->origin_locations_by_id[$assignment_node->id] = $origin_locations; + return $origin_locations; } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php index cc913806c19..770e1298779 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php @@ -16,6 +16,7 @@ use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\Provider\StatementsProvider; use Psalm\Internal\Scanner\ClassLikeDocblockComment; +use Psalm\Internal\Scanner\DocblockParser; use Psalm\Internal\Type\ParseTree\MethodParamTree; use Psalm\Internal\Type\ParseTree\MethodTree; use Psalm\Internal\Type\ParseTree\MethodWithReturnTypeTree; @@ -212,14 +213,7 @@ public static function parse( $info->consistent_templates = true; } - if (isset($parsed_docblock->tags['psalm-internal'])) { - $psalm_internal = trim(reset($parsed_docblock->tags['psalm-internal'])); - - if (!$psalm_internal) { - throw new DocblockParseException('psalm-internal annotation used without specifying namespace'); - } - - $info->psalm_internal = $psalm_internal; + if (count($info->psalm_internal = DocblockParser::handlePsalmInternal($parsed_docblock)) !== 0) { $info->internal = true; } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 03532b24117..2a99fff148d 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -68,8 +68,8 @@ use function array_pop; use function array_shift; use function array_values; +use function assert; use function count; -use function explode; use function get_class; use function implode; use function preg_match; @@ -184,6 +184,7 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool $fq_classlike_name = ($this->aliases->namespace ? $this->aliases->namespace . '\\' : '') . $node->name->name; + assert($fq_classlike_name !== ""); $fq_classlike_name_lc = strtolower($fq_classlike_name); @@ -253,7 +254,7 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool && isset($this->aliases->uses[strtolower($class_name)]) && $this->aliases->uses[strtolower($class_name)] !== $fq_classlike_name ) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new ParseError( 'Class name ' . $class_name . ' clashes with a use statement alias', $name_location ?? $class_location @@ -616,13 +617,10 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool $storage->deprecated = $docblock_info->deprecated; - if ($docblock_info->internal - && !$docblock_info->psalm_internal - && $this->aliases->namespace - ) { - $storage->internal = explode('\\', $this->aliases->namespace)[0]; - } else { - $storage->internal = $docblock_info->psalm_internal ?? ''; + if (count($docblock_info->psalm_internal) !== 0) { + $storage->internal = $docblock_info->psalm_internal; + } elseif ($docblock_info->internal && $this->aliases->namespace) { + $storage->internal = [NamespaceAnalyzer::getNameSpaceRoot($this->aliases->namespace)]; } if ($docblock_info->final && !$storage->final) { @@ -738,7 +736,7 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool } if ($attribute->fq_class_name === 'Psalm\\Internal' && !$storage->internal) { - $storage->internal = NamespaceAnalyzer::getNameSpaceRoot($fq_classlike_name); + $storage->internal = [NamespaceAnalyzer::getNameSpaceRoot($fq_classlike_name)]; } if ($attribute->fq_class_name === 'Psalm\\Immutable' @@ -1464,6 +1462,9 @@ private function getAttributeStorageFromStatement( return $storages; } + /** + * @param non-empty-string $fq_classlike_name + */ private function visitPropertyDeclaration( PhpParser\Node\Stmt\Property $stmt, Config $config, @@ -1559,9 +1560,9 @@ private function visitPropertyDeclaration( $property_storage->has_default = (bool)$property->default; $property_storage->deprecated = $var_comment ? $var_comment->deprecated : false; $property_storage->suppressed_issues = $var_comment ? $var_comment->suppressed_issues : []; - $property_storage->internal = $var_comment ? $var_comment->psalm_internal ?? '' : ''; - if (! $property_storage->internal && $var_comment && $var_comment->internal) { - $property_storage->internal = NamespaceAnalyzer::getNameSpaceRoot($fq_classlike_name); + $property_storage->internal = $var_comment ? $var_comment->psalm_internal : []; + if (count($property_storage->internal) === 0 && $var_comment && $var_comment->internal) { + $property_storage->internal = [NamespaceAnalyzer::getNameSpaceRoot($fq_classlike_name)]; } $property_storage->readonly = $stmt->isReadonly() || ($var_comment && $var_comment->readonly); $property_storage->allow_private_mutation = $var_comment ? $var_comment->allow_private_mutation : false; @@ -1673,7 +1674,7 @@ private function visitPropertyDeclaration( } if ($attribute->fq_class_name === 'Psalm\\Internal' && !$property_storage->internal) { - $property_storage->internal = NamespaceAnalyzer::getNameSpaceRoot($fq_classlike_name); + $property_storage->internal = [NamespaceAnalyzer::getNameSpaceRoot($fq_classlike_name)]; } if ($attribute->fq_class_name === 'Psalm\\Readonly') { diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php index 07c3f4ebb71..4de223c0ed7 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php @@ -3,12 +3,16 @@ namespace Psalm\Internal\PhpVisitor\Reflector; use PhpParser; +use Psalm\CodeLocation; use Psalm\DocComment; use Psalm\Exception\DocblockParseException; use Psalm\Exception\IncorrectDocblockException; use Psalm\Internal\Analyzer\CommentAnalyzer; +use Psalm\Internal\Scanner\DocblockParser; use Psalm\Internal\Scanner\FunctionDocblockComment; use Psalm\Internal\Scanner\ParsedDocblock; +use Psalm\Issue\InvalidDocblock; +use Psalm\IssueBuffer; use function array_shift; use function array_unique; @@ -37,8 +41,11 @@ class FunctionLikeDocblockParser /** * @throws DocblockParseException if there was a problem parsing the docblock */ - public static function parse(PhpParser\Comment\Doc $comment): FunctionDocblockComment - { + public static function parse( + PhpParser\Comment\Doc $comment, + CodeLocation $code_location, + string $cased_function_id + ): FunctionDocblockComment { $parsed_docblock = DocComment::parsePreservingLength($comment); $comment_text = $comment->getText(); @@ -52,7 +59,9 @@ public static function parse(PhpParser\Comment\Doc $comment): FunctionDocblockCo self::extractReturnType( $comment, $parsed_docblock->combined_tags['return'], - $info + $info, + $code_location, + $cased_function_id ); } @@ -110,7 +119,12 @@ public static function parse(PhpParser\Comment\Doc $comment): FunctionDocblockCo $info->params[] = $info_param; } } else { - throw new DocblockParseException('Badly-formatted @param'); + IssueBuffer::maybeAdd( + new InvalidDocblock( + 'Badly-formatted @param in docblock for ' . $cased_function_id, + $code_location + ) + ); } } } @@ -155,7 +169,12 @@ public static function parse(PhpParser\Comment\Doc $comment): FunctionDocblockCo ]; } } else { - throw new DocblockParseException('Badly-formatted @param'); + IssueBuffer::maybeAdd( + new InvalidDocblock( + 'Badly-formatted @param in docblock for ' . $cased_function_id, + $code_location + ) + ); } } } @@ -338,7 +357,12 @@ public static function parse(PhpParser\Comment\Doc $comment): FunctionDocblockCo ]; } } else { - throw new DocblockParseException('Badly-formatted @param'); + IssueBuffer::maybeAdd( + new InvalidDocblock( + 'Badly-formatted @param in docblock for ' . $cased_function_id, + $code_location + ) + ); } } } @@ -361,14 +385,7 @@ public static function parse(PhpParser\Comment\Doc $comment): FunctionDocblockCo $info->internal = true; } - if (isset($parsed_docblock->tags['psalm-internal'])) { - $psalm_internal = trim(reset($parsed_docblock->tags['psalm-internal'])); - - if (!$psalm_internal) { - throw new DocblockParseException('@psalm-internal annotation used without specifying namespace'); - } - - $info->psalm_internal = $psalm_internal; + if (count($info->psalm_internal = DocblockParser::handlePsalmInternal($parsed_docblock)) !== 0) { $info->internal = true; } @@ -545,7 +562,9 @@ private static function sanitizeAssertionLineParts(array $line_parts): array private static function extractReturnType( PhpParser\Comment\Doc $comment, array $return_specials, - FunctionDocblockComment $info + FunctionDocblockComment $info, + CodeLocation $code_location, + string $cased_function_id ): void { foreach ($return_specials as $offset => $return_block) { $return_lines = explode("\n", $return_block); @@ -584,7 +603,12 @@ private static function extractReturnType( $info->return_type_start = $offset; $info->return_type_end = $end; } else { - throw new DocblockParseException('Badly-formatted @return type'); + IssueBuffer::maybeAdd( + new InvalidDocblock( + 'Badly-formatted @param in docblock for ' . $cased_function_id, + $code_location + ) + ); } break; diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php index a2c62a76943..084a6ce1bb7 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -10,6 +10,7 @@ use Psalm\Config; use Psalm\Exception\InvalidMethodOverrideException; use Psalm\Exception\TypeParseTreeException; +use Psalm\Internal\Analyzer\NamespaceAnalyzer; use Psalm\Internal\Scanner\FileScanner; use Psalm\Internal\Scanner\FunctionDocblockComment; use Psalm\Internal\Type\Comparator\UnionTypeComparator; @@ -106,17 +107,10 @@ public static function addDocblockInfo( $storage->deprecated = true; } - if ($docblock_info->internal - && !$docblock_info->psalm_internal - && $aliases->namespace - ) { - $storage->internal = explode('\\', $aliases->namespace)[0]; - } elseif (!$classlike_storage - || ($docblock_info->psalm_internal - && strlen($docblock_info->psalm_internal) > strlen($classlike_storage->internal) - ) - ) { - $storage->internal = $docblock_info->psalm_internal ?? ''; + if (count($docblock_info->psalm_internal) !== 0) { + $storage->internal = $docblock_info->psalm_internal; + } elseif ($docblock_info->internal && $aliases->namespace) { + $storage->internal = [NamespaceAnalyzer::getNameSpaceRoot($aliases->namespace)]; } if (($storage->internal || ($classlike_storage && $classlike_storage->internal)) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index fe5e1db2c35..509c4f6907a 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -52,6 +52,7 @@ use UnexpectedValueException; use function array_keys; +use function array_merge; use function array_pop; use function array_search; use function count; @@ -61,7 +62,6 @@ use function in_array; use function is_string; use function spl_object_id; -use function strlen; use function strpos; use function strtolower; @@ -462,16 +462,14 @@ public function start(PhpParser\Node\FunctionLike $stmt, bool $fake_method = fal $doc_comment = $stmt->getDocComment(); - if ($classlike_storage - && !$classlike_storage->is_trait - && strlen($classlike_storage->internal) > strlen($storage->internal) - ) { - $storage->internal = $classlike_storage->internal; + if ($classlike_storage && !$classlike_storage->is_trait) { + $storage->internal = array_merge($classlike_storage->internal, $storage->internal); } if ($doc_comment) { try { - $docblock_info = FunctionLikeDocblockParser::parse($doc_comment); + $code_location = new CodeLocation($this->file_scanner, $stmt, null, true); + $docblock_info = FunctionLikeDocblockParser::parse($doc_comment, $code_location, $cased_function_id); } catch (IncorrectDocblockException $e) { $storage->docblock_issues[] = new MissingDocblockType( $e->getMessage() . ' in docblock for ' . $cased_function_id, @@ -599,7 +597,7 @@ public function start(PhpParser\Node\FunctionLike $stmt, bool $fake_method = fal } if (isset($classlike_storage->properties[$param_storage->name]) && $param_storage->location) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new ParseError( 'Promoted property ' . $param_storage->name . ' clashes with an existing property', $param_storage->location @@ -727,7 +725,7 @@ public function start(PhpParser\Node\FunctionLike $stmt, bool $fake_method = fal } if ($attribute->fq_class_name === 'Psalm\\Internal' && !$storage->internal && $fq_classlike_name) { - $storage->internal = NamespaceAnalyzer::getNameSpaceRoot($fq_classlike_name); + $storage->internal = [NamespaceAnalyzer::getNameSpaceRoot($fq_classlike_name)]; } if ($attribute->fq_class_name === 'Psalm\\ExternalMutationFree' @@ -1047,7 +1045,12 @@ private function createStorageForFunctionLike( if ($doc_comment) { $docblock_info = null; try { - $docblock_info = FunctionLikeDocblockParser::parse($doc_comment); + $code_location = new CodeLocation($this->file_scanner, $stmt, null, true); + $docblock_info = FunctionLikeDocblockParser::parse( + $doc_comment, + $code_location, + $cased_function_id + ); } catch (IncorrectDocblockException|DocblockParseException $e) { } if ($docblock_info) { diff --git a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php index baf0eccc2af..a445de36b81 100644 --- a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php +++ b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php @@ -17,6 +17,7 @@ use function igbinary_serialize; use function igbinary_unserialize; use function is_dir; +use function is_null; use function mkdir; use function serialize; use function strtolower; @@ -75,9 +76,15 @@ public function writeToCache(ClassLikeStorage $storage, string $file_path, strin { $fq_classlike_name_lc = strtolower($storage->name); - $cache_location = $this->getCacheLocationForClass($fq_classlike_name_lc, $file_path, true); $storage->hash = $this->getCacheHash($file_path, $file_contents); + // check if we have it in cache already + $cached_value = $this->loadFromCache($fq_classlike_name_lc, $file_path); + if (!is_null($cached_value) && $cached_value->hash === $storage->hash) { + return; + } + + $cache_location = $this->getCacheLocationForClass($fq_classlike_name_lc, $file_path, true); if ($this->config->use_igbinary) { file_put_contents($cache_location, igbinary_serialize($storage)); } else { @@ -110,9 +117,9 @@ public function getLatestFromCache( return $cached_value; } - private function getCacheHash(?string $file_path, ?string $file_contents): string + private function getCacheHash(?string $_unused_file_path, ?string $file_contents): string { - $data = ($file_path ? $file_contents : '') . $this->modified_timestamps; + $data = $file_contents ? $file_contents : $this->modified_timestamps; return PHP_VERSION_ID >= 8_01_00 ? hash('xxh128', $data) : hash('md4', $data); } diff --git a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php index 74c0804cf26..edb6a7fe19b 100644 --- a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php +++ b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php @@ -117,9 +117,12 @@ public function removeCacheForFile(string $file_path): void } } - private function getCacheHash(string $file_path, string $file_contents): string + private function getCacheHash(string $_unused_file_path, string $file_contents): string { - $data = ($file_path ? $file_contents : '') . $this->modified_timestamps; + // do not concatenate, as $file_contents can be big and performance will be bad + // the timestamp is only needed if we don't have file contents + // as same contents should give same results, independent of when file was modified + $data = $file_contents ? $file_contents : $this->modified_timestamps; return PHP_VERSION_ID >= 8_01_00 ? hash('xxh128', $data) : hash('md4', $data); } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php index 1697d50c389..90257156ecd 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php @@ -217,6 +217,10 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $inner_value_type = null; if ($inner_key_types) { + /** + * Truthy&array-shape-list doesn't reconcile correctly, will be fixed for 5.x by #8050. + * @psalm-suppress InvalidScalarArgument + */ $inner_key_type = TypeCombiner::combine($inner_key_types, $codebase, true); } diff --git a/src/Psalm/Internal/Provider/StatementsProvider.php b/src/Psalm/Internal/Provider/StatementsProvider.php index 92b93997cea..c4e00f748cb 100644 --- a/src/Psalm/Internal/Provider/StatementsProvider.php +++ b/src/Psalm/Internal/Provider/StatementsProvider.php @@ -141,7 +141,7 @@ public function getStatementsForFile( if (!$this->parser_cache_provider || (!$config->isInProjectDirs($file_path) && strpos($file_path, 'vendor')) ) { - $cache_key = "${file_content_hash}:${analysis_php_version_id}"; + $cache_key = "{$file_content_hash}:{$analysis_php_version_id}"; if ($this->statements_volatile_cache->has($cache_key)) { return $this->statements_volatile_cache->get($cache_key); } @@ -479,7 +479,7 @@ public static function parseStatements( foreach ($error_handler->getErrors() as $error) { if ($error->hasColumnInfo()) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new ParseError( $error->getMessage(), new ParseErrorLocation( diff --git a/src/Psalm/Internal/Scanner/ClassLikeDocblockComment.php b/src/Psalm/Internal/Scanner/ClassLikeDocblockComment.php index b91edd73e9a..a9168cc39fd 100644 --- a/src/Psalm/Internal/Scanner/ClassLikeDocblockComment.php +++ b/src/Psalm/Internal/Scanner/ClassLikeDocblockComment.php @@ -33,9 +33,9 @@ class ClassLikeDocblockComment /** * If set, the class is internal to the given namespace. * - * @var null|string + * @var list */ - public $psalm_internal; + public $psalm_internal = []; /** * @var string[] diff --git a/src/Psalm/Internal/Scanner/DocblockParser.php b/src/Psalm/Internal/Scanner/DocblockParser.php index 7c854b7c57f..2322a3b8510 100644 --- a/src/Psalm/Internal/Scanner/DocblockParser.php +++ b/src/Psalm/Internal/Scanner/DocblockParser.php @@ -2,8 +2,16 @@ namespace Psalm\Internal\Scanner; +use Psalm\Exception\DocblockParseException; + +use function array_filter; +use function array_map; +use function array_values; +use function assert; +use function count; use function explode; use function implode; +use function is_string; use function min; use function preg_match; use function preg_replace; @@ -258,4 +266,37 @@ private static function resolveTags(ParsedDocblock $docblock): void + ($docblock->tags['psalm-param-out'] ?? []); } } + + /** + * @return list + * @throws DocblockParseException when a @psalm-internal tag doesn't include a namespace + */ + public static function handlePsalmInternal(ParsedDocblock $parsed_docblock): array + { + if (isset($parsed_docblock->tags['psalm-internal'])) { + $psalm_internal = array_map("trim", $parsed_docblock->tags['psalm-internal']); + + if (count($psalm_internal) !== count(array_filter($psalm_internal))) { + throw new DocblockParseException('psalm-internal annotation used without specifying namespace'); + } + // assert($psalm_internal === array_filter($psalm_internal)); // TODO get this to work + assert(self::assertArrayOfNonEmptyString($psalm_internal)); + + return array_values($psalm_internal); + } + + return []; + } + + /** @psalm-assert-if-true array $arr */ + private static function assertArrayOfNonEmptyString(array $arr): bool + { + foreach ($arr as $val) { + if (!is_string($val) || $val === "") { + return false; + } + } + + return true; + } } diff --git a/src/Psalm/Internal/Scanner/FunctionDocblockComment.php b/src/Psalm/Internal/Scanner/FunctionDocblockComment.php index 89e41dbb8fd..a3819b4acf7 100644 --- a/src/Psalm/Internal/Scanner/FunctionDocblockComment.php +++ b/src/Psalm/Internal/Scanner/FunctionDocblockComment.php @@ -77,9 +77,9 @@ class FunctionDocblockComment /** * If set, the function is internal to the given namespace. * - * @var null|string + * @var list */ - public $psalm_internal; + public $psalm_internal = []; /** * Whether or not the function is internal diff --git a/src/Psalm/Internal/Scanner/VarDocblockComment.php b/src/Psalm/Internal/Scanner/VarDocblockComment.php index 7c38a339be1..8797530a664 100644 --- a/src/Psalm/Internal/Scanner/VarDocblockComment.php +++ b/src/Psalm/Internal/Scanner/VarDocblockComment.php @@ -51,9 +51,9 @@ class VarDocblockComment /** * If set, the property is internal to the given namespace. * - * @var null|string + * @var list */ - public $psalm_internal; + public $psalm_internal = []; /** * Whether or not the property is readonly diff --git a/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php b/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php index 8758bd701a8..e99d00433a4 100644 --- a/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php @@ -4,16 +4,11 @@ use Psalm\Codebase; use Psalm\Internal\Type\TemplateStandinTypeReplacer; -use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Atomic\TGenericObject; use Psalm\Type\Atomic\TIterable; use Psalm\Type\Atomic\TNamedObject; -use function array_fill; -use function array_values; -use function count; - /** * @internal */ @@ -44,38 +39,13 @@ public static function isContainedBy( $container_was_iterable = true; } - if (!$input_type_part instanceof TGenericObject && !$input_type_part instanceof TIterable) { - if ($input_type_part instanceof TNamedObject - && $codebase->classExists($input_type_part->value) - ) { - $class_storage = $codebase->classlike_storage_provider->get($input_type_part->value); - - $container_class = $container_type_part->value; - - // attempt to transform it - if (!empty($class_storage->template_extended_params[$container_class])) { - $input_type_part = new TGenericObject( - $input_type_part->value, - array_values($class_storage->template_extended_params[$container_class]) - ); - } + if (!$input_type_part instanceof TNamedObject && !$input_type_part instanceof TIterable) { + if ($atomic_comparison_result) { + $atomic_comparison_result->type_coerced = true; + $atomic_comparison_result->type_coerced_from_mixed = true; } - if (!$input_type_part instanceof TGenericObject) { - if ($input_type_part instanceof TNamedObject) { - $input_type_part = new TGenericObject( - $input_type_part->value, - array_fill(0, count($container_type_part->type_params), Type::getMixed()) - ); - } else { - if ($atomic_comparison_result) { - $atomic_comparison_result->type_coerced = true; - $atomic_comparison_result->type_coerced_from_mixed = true; - } - - return false; - } - } + return false; } $container_type_params_covariant = []; diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index c9d49188cfc..ab770007085 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -35,6 +35,7 @@ use Psalm\Type\Union; use Throwable; +use function array_fill; use function array_keys; use function array_merge; use function array_search; @@ -44,6 +45,7 @@ use function in_array; use function reset; use function strpos; +use function strtolower; use function substr; use function usort; @@ -1154,7 +1156,7 @@ public static function getMostSpecificTypeFromBounds(array $lower_bounds, ?Codeb } /** - * @param TGenericObject|TIterable $input_type_part + * @param TGenericObject|TNamedObject|TIterable $input_type_part * @param TGenericObject|TIterable $container_type_part * @return list */ @@ -1164,7 +1166,21 @@ public static function getMappedGenericTypeParams( Atomic $container_type_part, ?array &$container_type_params_covariant = null ): array { - $input_type_params = $input_type_part->type_params; + if ($input_type_part instanceof TGenericObject || $input_type_part instanceof TIterable) { + $input_type_params = $input_type_part->type_params; + } else { + $class_storage = $codebase->classlike_storage_provider->get($input_type_part->value); + + $container_class = $container_type_part->value; + + if (strtolower($input_type_part->value) === strtolower($container_type_part->value)) { + $input_type_params = $class_storage->getClassTemplateTypes(); + } elseif (!empty($class_storage->template_extended_params[$container_class])) { + $input_type_params = array_values($class_storage->template_extended_params[$container_class]); + } else { + $input_type_params = array_fill(0, count($class_storage->template_types ?? []), Type::getMixed()); + } + } try { $input_class_storage = $codebase->classlike_storage_provider->get($input_type_part->value); diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 84778f983b5..6d25b1f40c2 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -1214,6 +1214,7 @@ private static function scrapeIntProperties( $combination->value_types['int'] = new TInt(); } } elseif ($type instanceof TIntRange) { + $type = clone $type; if ($combination->ints) { foreach ($combination->ints as $int) { if (!$type->contains($int->value)) { diff --git a/src/Psalm/Issue/InternalClass.php b/src/Psalm/Issue/InternalClass.php index 5332579ed89..827c9013419 100644 --- a/src/Psalm/Issue/InternalClass.php +++ b/src/Psalm/Issue/InternalClass.php @@ -2,8 +2,31 @@ namespace Psalm\Issue; +use function array_pop; +use function count; +use function implode; +use function reset; + final class InternalClass extends ClassIssue { public const ERROR_LEVEL = 4; public const SHORTCODE = 174; + + /** @param non-empty-list $words */ + public static function listToPhrase(array $words): string + { + if (count($words) === 1) { + return reset($words); + } + + if (count($words) === 2) { + return implode(" and ", $words); + } + + $last_word = array_pop($words); + $phrase = implode(", ", $words); + $phrase = "$phrase, and $last_word"; + + return $phrase; + } } diff --git a/src/Psalm/IssueBuffer.php b/src/Psalm/IssueBuffer.php index 2f07b0a81aa..bb93e1f4511 100644 --- a/src/Psalm/IssueBuffer.php +++ b/src/Psalm/IssueBuffer.php @@ -244,7 +244,11 @@ public static function isSuppressed(CodeIssue $e, array $suppressed_issues = []) } /** - * Add an issue to be emitted + * Add an issue to be emitted. This method should normally not be used! Use IssueBuffer::maybeAdd instead. + * + * @psalm-internal Psalm\IssueBuffer + * @psalm-internal Psalm\Type\Reconciler::getValueForKey + * * @throws CodeException */ public static function add(CodeIssue $e, bool $is_fixable = false): bool diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php index e5190cf10b1..d9954713107 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -11,6 +11,8 @@ use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; +use function array_values; + final class ClassLikeStorage implements HasAttributesInterface { use CustomMetadataTrait; @@ -43,9 +45,9 @@ final class ClassLikeStorage implements HasAttributesInterface public $deprecated = false; /** - * @var string + * @var list */ - public $internal = ''; + public $internal = []; /** * @var TTemplateParam[] @@ -470,4 +472,20 @@ public function getAttributeStorages(): array { return $this->attributes; } + + /** + * Get the template constraint types for the class. + * + * @return list + */ + public function getClassTemplateTypes(): array + { + $type_params = []; + + foreach ($this->template_types ?? [] as $type_map) { + $type_params[] = clone array_values($type_map)[0]; + } + + return $type_params; + } } diff --git a/src/Psalm/Storage/FunctionLikeStorage.php b/src/Psalm/Storage/FunctionLikeStorage.php index aea1600e1c3..0ddabfabbb7 100644 --- a/src/Psalm/Storage/FunctionLikeStorage.php +++ b/src/Psalm/Storage/FunctionLikeStorage.php @@ -74,9 +74,9 @@ abstract class FunctionLikeStorage implements HasAttributesInterface public $deprecated; /** - * @var string + * @var list */ - public $internal = ''; + public $internal = []; /** * @var bool diff --git a/src/Psalm/Storage/PropertyStorage.php b/src/Psalm/Storage/PropertyStorage.php index a77b2dc514f..745e1e50ea3 100644 --- a/src/Psalm/Storage/PropertyStorage.php +++ b/src/Psalm/Storage/PropertyStorage.php @@ -78,9 +78,9 @@ final class PropertyStorage implements HasAttributesInterface public $allow_private_mutation = false; /** - * @var string + * @var list */ - public $internal = ''; + public $internal = []; /** * @var ?string diff --git a/src/Psalm/Type/Atomic/TFalse.php b/src/Psalm/Type/Atomic/TFalse.php index e4b43a3170b..dbcb4ec8a6b 100644 --- a/src/Psalm/Type/Atomic/TFalse.php +++ b/src/Psalm/Type/Atomic/TFalse.php @@ -7,6 +7,9 @@ */ final class TFalse extends TBool { + /** @var false */ + public $value = false; + public function getKey(bool $include_extra = true): string { return 'false'; diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index b72e6a62321..b92926ffb1b 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -368,13 +368,15 @@ public function getAssertionString(): string return $this->getKey(); } - public function getList(): TNonEmptyList + public function getList(): TList { if (!$this->is_list) { throw new UnexpectedValueException('Object-like array must be a list for conversion'); } - return new TNonEmptyList($this->getGenericValueType()); + return $this->isNonEmpty() + ? new TNonEmptyList($this->getGenericValueType()) + : new TList($this->getGenericValueType()); } /** diff --git a/src/Psalm/Type/Atomic/TTrue.php b/src/Psalm/Type/Atomic/TTrue.php index 56b5b5ed63c..64545045e17 100644 --- a/src/Psalm/Type/Atomic/TTrue.php +++ b/src/Psalm/Type/Atomic/TTrue.php @@ -7,6 +7,9 @@ */ final class TTrue extends TBool { + /** @var true */ + public $value = true; + public function getKey(bool $include_extra = true): string { return 'true'; diff --git a/src/Psalm/Type/Union.php b/src/Psalm/Type/Union.php index 8262e14f272..6036ab10acf 100644 --- a/src/Psalm/Type/Union.php +++ b/src/Psalm/Type/Union.php @@ -280,6 +280,7 @@ public function replaceTypes(array $types): void } /** + * @psalm-mutation-free * @return non-empty-array */ public function getAtomicTypes(): array @@ -1278,6 +1279,34 @@ public function allFloatLiterals(): bool return true; } + /** + * @psalm-assert-if-true array< + * array-key, + * TLiteralString|TLiteralInt|TLiteralFloat|TFalse|TTrue + * > $this->getAtomicTypes() + */ + public function allSpecificLiterals(): bool + { + foreach ($this->types as $atomic_key_type) { + if (!$atomic_key_type instanceof TLiteralString + && !$atomic_key_type instanceof TLiteralInt + && !$atomic_key_type instanceof TLiteralFloat + && !$atomic_key_type instanceof TFalse + && !$atomic_key_type instanceof TTrue + ) { + return false; + } + } + + return true; + } + + /** + * @psalm-assert-if-true array< + * array-key, + * TLiteralString|TLiteralInt|TLiteralFloat|TNonspecificLiteralString|TNonSpecificLiteralInt|TFalse|TTrue + * > $this->getAtomicTypes() + */ public function allLiterals(): bool { foreach ($this->types as $atomic_key_type) { @@ -1305,6 +1334,32 @@ public function hasLiteralValue(): bool || isset($this->types['true']); } + public function isSingleLiteral(): bool + { + return count($this->types) === 1 + && count($this->literal_int_types) + + count($this->literal_string_types) + + count($this->literal_float_types) === 1 + ; + } + + /** + * @return TLiteralInt|TLiteralString|TLiteralFloat + */ + public function getSingleLiteral() + { + if (!$this->isSingleLiteral()) { + throw new InvalidArgumentException("Not a single literal"); + } + + return ($literal = reset($this->literal_int_types)) !== false + ? $literal + : (($literal = reset($this->literal_string_types)) !== false + ? $literal + : reset($this->literal_float_types)) + ; + } + public function hasLiteralString(): bool { return count($this->literal_string_types) > 0; diff --git a/stubs/CoreGenericFunctions.phpstub b/stubs/CoreGenericFunctions.phpstub index b7ee9996220..32203cd33ef 100644 --- a/stubs/CoreGenericFunctions.phpstub +++ b/stubs/CoreGenericFunctions.phpstub @@ -566,6 +566,8 @@ function trim(string $string, string $characters = " \t\n\r\0\x0B") : string {} /** * @psalm-pure * + * @return ($string is class-string ? ($characters is '\\' ? class-string : string) : string) + * * @psalm-flow ($string) -> return */ function ltrim(string $string, string $characters = " \t\n\r\0\x0B") : string {} diff --git a/stubs/Reflection.phpstub b/stubs/Reflection.phpstub index 3f67294cc4f..0809e0996b9 100644 --- a/stubs/Reflection.phpstub +++ b/stubs/Reflection.phpstub @@ -191,7 +191,7 @@ class ReflectionNamedType extends ReflectionType public function getName(): string {} /** - * @psalm-assert-if-false class-string $this->getName() + * @psalm-assert-if-false class-string|'self'|'static' $this->getName() */ public function isBuiltin(): bool {} } diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index e10802a8257..377886620eb 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -1080,26 +1080,26 @@ function bar(array $value): void { if (is_int($value["a"])) {} }' ], - 'coercePossiblyNullKeyToZero' => [ + 'coercePossiblyNullKeyToEmptyString' => [ 'code' => ' + * @return array */ function foo(): array { $array = []; /** @psalm-suppress PossiblyNullArrayOffset */ - $array[int_or_null()] = null; + $array[string_or_null()] = null; return $array; }' ], - 'coerceNullKeyToZero' => [ + 'coerceNullKeyToEmptyString' => [ 'code' => ' + * @return array */ function foo(): array { $array = []; diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index a937ed395ca..155fd994401 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -397,6 +397,16 @@ class Foo {} class Bar {} ', ], + 'dontCrashWhenRedefiningStubbedMethodWithFewerParams' => [ + 'code' => ' [ + 'code' => ' ['$interpolated===' => "'12.3foobar'"], + ], + 'concatenatedStringIsInferredAsLiteral' => [ + 'code' => ' ['$concatenated===' => "'12.3foobar'"], + ], + 'encapsedNonEmptyNonSpecificLiteralString' => [ + 'code' => ' ['$interpolated===' => 'non-empty-literal-string'], + ], + 'concatenatedNonEmptyNonSpecificLiteralString' => [ + 'code' => ' ['$concatenated===' => 'non-empty-literal-string'], + ], + 'encapsedPossiblyEmptyLiteralString' => [ + 'code' => ' ['$interpolated===' => 'literal-string'], + ], 'literalIntConcatCreatesLiteral' => [ 'code' => 'test_code_location = new CodeLocation($file_analyzer, $stmt); } public function testDocblockDescription(): void @@ -29,7 +61,11 @@ public function testDocblockDescription(): void */ '; $php_parser_doc = new Doc($doc); - $function_docblock = FunctionLikeDocblockParser::parse($php_parser_doc); + $function_docblock = FunctionLikeDocblockParser::parse( + $php_parser_doc, + $this->test_code_location, + $this->test_cased_function_id + ); $this->assertSame('Some Description', $function_docblock->description); } @@ -49,7 +85,11 @@ public function testDocblockParamDescription(): void */ '; $php_parser_doc = new Doc($doc); - $function_docblock = FunctionLikeDocblockParser::parse($php_parser_doc); + $function_docblock = FunctionLikeDocblockParser::parse( + $php_parser_doc, + $this->test_code_location, + $this->test_cased_function_id + ); $this->assertTrue(isset($function_docblock->params[0]['description'])); $this->assertSame('The BLI tag to iterate over.', $function_docblock->params[0]['description']); @@ -67,7 +107,11 @@ public function testMisplacedVariableOnNextLine(): void $php_parser_doc = new Doc($doc); $this->expectException(IncorrectDocblockException::class); $this->expectExceptionMessage('Misplaced variable'); - FunctionLikeDocblockParser::parse($php_parser_doc); + FunctionLikeDocblockParser::parse( + $php_parser_doc, + $this->test_code_location, + $this->test_cased_function_id + ); } public function testPreferPsalmPrefixedAnnotationsOverPhpstanOnes(): void @@ -78,7 +122,11 @@ public function testPreferPsalmPrefixedAnnotationsOverPhpstanOnes(): void */ '; $php_parser_doc = new Doc($doc); - $function_docblock = FunctionLikeDocblockParser::parse($php_parser_doc); + $function_docblock = FunctionLikeDocblockParser::parse( + $php_parser_doc, + $this->test_code_location, + $this->test_cased_function_id + ); $this->assertSame([['T', 'of', 'string', false]], $function_docblock->templates); } @@ -90,7 +138,11 @@ public function testReturnsUnexpectedTags(): void */ '; $php_parser_doc = new Doc($doc, 0); - $function_docblock = FunctionLikeDocblockParser::parse($php_parser_doc); + $function_docblock = FunctionLikeDocblockParser::parse( + $php_parser_doc, + $this->test_code_location, + $this->test_cased_function_id + ); $this->assertEquals( [ 'psalm-import-type' => ['lines' => [1]], diff --git a/tests/IntRangeTest.php b/tests/IntRangeTest.php index cd91da19122..48181a66af7 100644 --- a/tests/IntRangeTest.php +++ b/tests/IntRangeTest.php @@ -2,14 +2,26 @@ namespace Psalm\Tests; +use Psalm\Internal\Type\TypeCombiner; use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; +use Psalm\Type\Atomic\TIntRange; +use Psalm\Type\Atomic\TLiteralInt; class IntRangeTest extends TestCase { use InvalidCodeAnalysisTestTrait; use ValidCodeAnalysisTestTrait; + public function testCombineIntRangeDoesntAffectOriginal(): void + { + $range = new TIntRange(5, 10); + TypeCombiner::combine([new TLiteralInt(1), new TLiteralInt(50), $range]); + + $this->assertEquals(5, $range->min_bound); + $this->assertEquals(10, $range->max_bound); + } + /** * @return iterable,ignored_issues?:list}> */ diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index 2c6d98ca691..e69385648bc 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -2,11 +2,533 @@ namespace Psalm\Tests\Internal\Codebase; +use InvalidArgumentException; +use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\ExpectationFailedException; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\Codebase\InternalCallMapHandler; +use Psalm\Internal\Codebase\Reflection; +use Psalm\Internal\Provider\FakeFileProvider; +use Psalm\Internal\Provider\Providers; +use Psalm\Internal\Type\Comparator\UnionTypeComparator; +use Psalm\Tests\Internal\Provider\FakeParserCacheProvider; use Psalm\Tests\TestCase; +use Psalm\Tests\TestConfig; +use Psalm\Type; +use ReflectionFunction; +use ReflectionParameter; +use ReflectionType; + +use function array_shift; +use function class_exists; +use function count; +use function explode; +use function function_exists; +use function in_array; +use function is_array; +use function is_int; +use function json_encode; +use function preg_match; +use function print_r; +use function strcmp; +use function strncmp; +use function strpos; +use function substr; +use function version_compare; + +use const PHP_MAJOR_VERSION; +use const PHP_MINOR_VERSION; +use const PHP_VERSION; class InternalCallMapHandlerTest extends TestCase { + /** + * Specify a function name as value, or a function name as key and + * an array containing the PHP versions in which to ignore this function as values. + * @var array> + */ + private static $ignoredFunctions = [ + 'apcu_entry', + 'array_multisort', + 'bcdiv', + 'bcmod', + 'bcpowmod', + 'bzdecompress', + 'crypt', + 'date_isodate_set', + 'debug_zval_dump', + 'deflate_add', + 'dns_get_mx', + 'easter_date', + 'enchant_broker_describe', + 'enchant_broker_dict_exists', + 'enchant_broker_free', + 'enchant_broker_free_dict', + 'enchant_broker_get_dict_path', + 'enchant_broker_get_error', + 'enchant_broker_list_dicts', + 'enchant_broker_request_dict', + 'enchant_broker_request_pwl_dict', + 'enchant_broker_set_dict_path', + 'enchant_broker_set_ordering', + 'enchant_dict_add_to_personal', + 'enchant_dict_add_to_session', + 'enchant_dict_check', + 'enchant_dict_describe', + 'enchant_dict_get_error', + 'enchant_dict_is_in_session', + 'enchant_dict_quick_check', + 'enchant_dict_store_replacement', + 'enchant_dict_suggest', + 'get_headers', + 'gmp_clrbit', + 'gmp_div', + 'gmp_setbit', + 'gnupg_adddecryptkey', + 'gnupg_addencryptkey', + 'gnupg_addsignkey', + 'gnupg_cleardecryptkeys', + 'gnupg_clearencryptkeys', + 'gnupg_clearsignkeys', + 'gnupg_decrypt', + 'gnupg_decryptverify', + 'gnupg_encrypt', + 'gnupg_encryptsign', + 'gnupg_export', + 'gnupg_geterror', + 'gnupg_getprotocol', + 'gnupg_import', + 'gnupg_init', + 'gnupg_keyinfo', + 'gnupg_setarmor', + 'gnupg_seterrormode', + 'gnupg_setsignmode', + 'gnupg_sign', + 'gnupg_verify', + 'hash_hmac_file', + 'igbinary_unserialize', + 'imagefilledpolygon', + 'imagefilter', + 'imagegd', + 'imagegd2', + 'imageinterlace', + 'imageopenpolygon', + 'imagepolygon', + 'imagerotate', + 'imagesetinterpolation', + 'imagettfbbox', + 'imagettftext', + 'imagexbm', + 'imap_delete', + 'imap_open', + 'imap_rfc822_write_address', + 'imap_sort', + 'imap_undelete', + 'inflate_add', + 'inflate_get_read_len', + 'inflate_get_status', + 'inotify_rm_watch', + 'intlcal_from_date_time', + 'intlcal_get_weekend_transition', + 'intlgregcal_create_instance', + 'intlgregcal_is_leap_year', + 'intltz_create_enumeration', + 'intltz_get_canonical_id', + 'intltz_get_display_name', + 'long2ip', + 'lzf_compress', + 'lzf_decompress', + 'mail', + 'mailparse_msg_extract_part', + 'mailparse_msg_extract_part_file', + 'mailparse_msg_extract_whole_part_file', + 'mailparse_msg_free', + 'mailparse_msg_get_part', + 'mailparse_msg_get_part_data', + 'mailparse_msg_get_structure', + 'mailparse_msg_parse', + 'mailparse_stream_encode', + 'memcache_add', + 'memcache_add_server', + 'memcache_append', + 'memcache_cas', + 'memcache_close', + 'memcache_connect', + 'memcache_decrement', + 'memcache_delete', + 'memcache_flush', + 'memcache_get_extended_stats', + 'memcache_get_server_status', + 'memcache_get_stats', + 'memcache_get_version', + 'memcache_increment', + 'memcache_pconnect', + 'memcache_prepend', + 'memcache_replace', + 'memcache_set', + 'memcache_set_compress_threshold', + 'memcache_set_failure_callback', + 'memcache_set_server_params', + 'mongodb\bson\tophp', + 'msg_receive', + 'msg_remove_queue', + 'msg_send', + 'msg_set_queue', + 'msg_stat_queue', + 'mysqli_poll', + 'mysqli_real_connect', + 'mysqli_stmt_bind_param', + 'normalizer_get_raw_decomposition', + 'oauth_get_sbs', + 'oci_collection_append', + 'oci_collection_assign', + 'oci_collection_element_assign', + 'oci_collection_element_get', + 'oci_collection_max', + 'oci_collection_size', + 'oci_collection_trim', + 'oci_fetch_object', + 'oci_field_is_null', + 'oci_field_name', + 'oci_field_precision', + 'oci_field_scale', + 'oci_field_size', + 'oci_field_type', + 'oci_field_type_raw', + 'oci_free_collection', + 'oci_free_descriptor', + 'oci_lob_append', + 'oci_lob_eof', + 'oci_lob_erase', + 'oci_lob_export', + 'oci_lob_flush', + 'oci_lob_import', + 'oci_lob_load', + 'oci_lob_read', + 'oci_lob_rewind', + 'oci_lob_save', + 'oci_lob_seek', + 'oci_lob_size', + 'oci_lob_tell', + 'oci_lob_truncate', + 'oci_lob_write', + 'oci_register_taf_callback', + 'oci_result', + 'ocigetbufferinglob', + 'ocisetbufferinglob', + 'odbc_procedurecolumns', + 'odbc_procedures', + 'odbc_result', + 'openssl_pkcs7_read', + 'pg_exec', + 'pg_fetch_all', + 'pg_get_notify', + 'pg_get_result', + 'pg_pconnect', + 'pg_select', + 'pg_send_execute', + 'preg_filter', + 'preg_replace_callback_array', + 'sapi_windows_cp_get', + 'sem_acquire', + 'sem_get', + 'sem_release', + 'sem_remove', + 'shm_detach', + 'shm_get_var', + 'shm_has_var', + 'shm_put_var', + 'shm_remove', + 'shm_remove_var', + 'shmop_close', + 'shmop_delete', + 'shmop_read', + 'shmop_size', + 'shmop_write', + 'snmp_set_enum_print', + 'snmp_set_valueretrieval', + 'snmpset', + 'socket_addrinfo_lookup', + 'socket_bind', + 'socket_cmsg_space', + 'socket_connect', + 'socket_create_pair', + 'socket_get_option', + 'socket_getopt', + 'socket_getpeername', + 'socket_getsockname', + 'socket_read', + 'socket_recv', + 'socket_recvfrom', + 'socket_recvmsg', + 'socket_select', + 'socket_send', + 'socket_sendmsg', + 'socket_sendto', + 'socket_set_blocking', + 'socket_set_option', + 'socket_setopt', + 'socket_shutdown', + 'socket_strerror', + 'sodium_crypto_generichash', + 'sodium_crypto_generichash_final', + 'sodium_crypto_generichash_init', + 'sodium_crypto_generichash_update', + 'sodium_crypto_kx_client_session_keys', + 'sodium_crypto_secretstream_xchacha20poly1305_rekey', + 'sqlsrv_connect', + 'sqlsrv_errors', + 'sqlsrv_fetch_array', + 'sqlsrv_fetch_object', + 'sqlsrv_get_field', + 'sqlsrv_prepare', + 'sqlsrv_query', + 'sqlsrv_server_info', + 'stomp_abort', + 'stomp_ack', + 'stomp_begin', + 'stomp_commit', + 'stomp_read_frame', + 'stomp_send', + 'stomp_set_read_timeout', + 'stomp_subscribe', + 'stomp_unsubscribe', + 'stream_select' => ['8.0'], + 'substr_replace', + 'tidy_getopt', + 'uopz_allow_exit', + 'uopz_get_mock', + 'uopz_get_property', + 'uopz_get_return', + 'uopz_get_static', + 'uopz_set_mock', + 'uopz_set_property', + 'uopz_set_static', + 'uopz_unset_mock', + 'xdiff_file_bdiff', + 'xdiff_file_bdiff_size', + 'xdiff_file_diff', + 'xdiff_file_diff_binary', + 'xdiff_file_merge3', + 'xdiff_file_rabdiff', + 'xdiff_string_bdiff', + 'xdiff_string_bdiff_size', + 'xdiff_string_bpatch', + 'xdiff_string_diff', + 'xdiff_string_diff_binary', + 'xdiff_string_merge3', + 'xdiff_string_patch', + 'xdiff_string_patch_binary', + 'xdiff_string_rabdiff', + 'xmlrpc_server_add_introspection_data', + 'xmlrpc_server_call_method', + 'xmlrpc_server_destroy', + 'xmlrpc_server_register_introspection_callback', + 'xmlrpc_server_register_method', + 'yaml_emit', + 'yaml_emit_file', + 'zip_entry_close', + 'zlib_encode', + ]; + + /** + * List of function names to ignore only for return type checks. + * + * @var list + */ + private static $ignoredReturnTypeOnlyFunctions = [ + 'bcsqrt', + 'bzopen', + 'cal_from_jd', + 'collator_get_strength', + 'curl_multi_init', + 'date_add', + 'date_date_set', + 'date_diff', + 'date_offset_get', + 'date_parse', + 'date_sub', + 'date_sun_info', + 'date_sunrise', + 'date_sunset', + 'date_time_set', + 'date_timestamp_set', + 'date_timezone_set', + 'datefmt_set_lenient', + 'dba_open', + 'dba_popen', + 'deflate_init', + 'enchant_broker_init', + 'fgetcsv', + 'filter_input_array', + 'fopen', + 'fpassthru', + 'fsockopen', + 'ftp_get_option', + 'get_declared_traits', + 'gmp_export', + 'gmp_hamdist', + 'gmp_import', + 'gzeof', + 'gzopen', + 'gzpassthru', + 'hash', + 'hash_hkdf', + 'hash_hmac', + 'iconv_get_encoding', + 'igbinary_serialize', + 'imagecolorclosest', + 'imagecolorclosestalpha', + 'imagecolorclosesthwb', + 'imagecolorexact', + 'imagecolorexactalpha', + 'imagecolorresolve', + 'imagecolorresolvealpha', + 'imagecolorset', + 'imagecolorsforindex', + 'imagecolorstotal', + 'imagecolortransparent', + 'imageloadfont', + 'imagesx', + 'imagesy', + 'imap_mailboxmsginfo', + 'imap_msgno', + 'imap_num_recent', + 'inflate_init', + 'intlcal_get', + 'intlcal_get_keyword_values_for_locale', + 'intlgregcal_set_gregorian_change', + 'intltz_get_offset', + 'jddayofweek', + 'jdtounix', + 'ldap_count_entries', + 'ldap_exop', + 'ldap_get_attributes', + 'mb_encoding_aliases', + 'metaphone', + 'mongodb\\bson\\fromjson', + 'mongodb\\bson\\fromphp', + 'mongodb\\bson\\tojson', + 'msg_get_queue', + 'mysqli_stmt_get_warnings', + 'mysqli_stmt_insert_id', + 'numfmt_create', + 'ob_list_handlers', + 'odbc_autocommit', + 'odbc_columnprivileges', + 'odbc_columns', + 'odbc_connect', + 'odbc_do', + 'odbc_exec', + 'odbc_fetch_object', + 'odbc_foreignkeys', + 'odbc_gettypeinfo', + 'odbc_pconnect', + 'odbc_prepare', + 'odbc_primarykeys', + 'odbc_specialcolumns', + 'odbc_statistics', + 'odbc_tableprivileges', + 'odbc_tables', + 'opendir', + 'openssl_random_pseudo_bytes', + 'openssl_spki_export', + 'openssl_spki_export_challenge', + 'pack', + 'parse_url', + 'passthru', + 'pcntl_exec', + 'pcntl_signal_get_handler', + 'pcntl_strerror', + 'pfsockopen', + 'pg_port', + 'pg_socket', + 'popen', + 'proc_open', + 'pspell_config_create', + 'pspell_new', + 'pspell_new_config', + 'pspell_new_personal', + 'register_shutdown_function', + 'rewinddir', + 'set_error_handler', + 'set_exception_handler', + 'shm_attach', + 'shmop_open', + 'simplexml_import_dom', + 'sleep', + 'snmp_set_oid_numeric_print', + 'socket_export_stream', + 'socket_import_stream', + 'sodium_crypto_aead_chacha20poly1305_encrypt', + 'sodium_crypto_aead_chacha20poly1305_ietf_encrypt', + 'sodium_crypto_aead_xchacha20poly1305_ietf_encrypt', + 'spl_autoload_functions', + 'stream_bucket_new', + 'stream_context_create', + 'stream_context_get_default', + 'stream_context_set_default', + 'stream_filter_append', + 'stream_filter_prepend', + 'stream_set_chunk_size', + 'stream_socket_accept', + 'stream_socket_client', + 'stream_socket_server', + 'substr', + 'substr_compare', + 'timezone_abbreviations_list', + 'timezone_offset_get', + 'tmpfile', + 'user_error', + 'xml_get_current_byte_index', + 'xml_get_current_column_number', + 'xml_get_current_line_number', + 'xml_get_error_code', + 'xml_parser_get_option', + 'yaml_parse', + 'yaml_parse_file', + 'yaml_parse_url', + 'zip_open', + 'zip_read', + ]; + + /** + * + * @var Codebase + */ + private static $codebase; + + public static function setUpBeforeClass(): void + { + $project_analyzer = new ProjectAnalyzer( + new TestConfig(), + new Providers( + new FakeFileProvider(), + new FakeParserCacheProvider() + ) + ); + self::$codebase = $project_analyzer->getCodebase(); + } + + + public function testIgnoresAreSortedAndUnique(): void + { + $previousFunction = ""; + foreach (self::$ignoredFunctions as $key => $value) { + /** @var string */ + $function = is_int($key) ? $value : $key; + + $this->assertGreaterThan(0, strcmp($function, $previousFunction)); + $previousFunction = $function; + } + } + + public static function tearDownAfterClass(): void + { + self::$codebase = null; + } + /** * @covers \Psalm\Internal\Codebase\InternalCallMapHandler::getCallMap */ @@ -23,4 +545,252 @@ public function testGetcallmapReturnsAValidCallmap(): void } } } + + /** + * + * @return iterable}> + */ + public function callMapEntryProvider(): iterable + { + /** + * This call is needed since InternalCallMapHandler uses the singleton that is initialized by it. + **/ + new ProjectAnalyzer( + new TestConfig(), + new Providers( + new FakeFileProvider(), + new FakeParserCacheProvider() + ) + ); + $callMap = InternalCallMapHandler::getCallMap(); + foreach ($callMap as $function => $entry) { + // Skip class methods + if (strpos($function, '::') !== false || !function_exists($function)) { + continue; + } + // Skip functions with alternate signatures + if (isset($callMap["$function'1"]) || preg_match("/\'\d$/", $function)) { + continue; + } + // if ($function != 'fprintf') continue; + yield "$function: " . json_encode($entry) => [$function, $entry]; + } + } + + /** + */ + private function isIgnored(string $functionName): bool + { + if (in_array($functionName, self::$ignoredFunctions)) { + return true; + } + + if (isset(self::$ignoredFunctions[$functionName]) + && is_array(self::$ignoredFunctions[$functionName]) + && in_array(PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION, self::$ignoredFunctions[$functionName])) { + return true; + } + + return false; + } + + /** + */ + private function isReturnTypeOnlyIgnored(string $functionName): bool + { + return in_array($functionName, static::$ignoredReturnTypeOnlyFunctions, true); + } + + /** + * @depends testIgnoresAreSortedAndUnique + * @depends testGetcallmapReturnsAValidCallmap + * @dataProvider callMapEntryProvider + * @coversNothing + * @psalm-param callable-string $functionName + * @param array $callMapEntry + */ + public function testIgnoredFunctionsStillFail(string $functionName, array $callMapEntry): void + { + $functionIgnored = $this->isIgnored($functionName); + if (!$functionIgnored && !$this->isReturnTypeOnlyIgnored($functionName)) { + // Dummy assertion to mark it as passed + $this->assertTrue(true); + return; + } + + $function = new ReflectionFunction($functionName); + /** @var string $entryReturnType */ + $entryReturnType = array_shift($callMapEntry); + + if ($functionIgnored) { + try { + /** @var array $callMapEntry */ + $this->assertEntryParameters($function, $callMapEntry); + $this->assertEntryReturnType($function, $entryReturnType); + } catch (AssertionFailedError $e) { + $this->assertTrue(true); + return; + } catch (ExpectationFailedException $e) { + $this->assertTrue(true); + return; + } + $this->fail("Remove '{$functionName}' from InternalCallMapHandlerTest::\$ignoredFunctions"); + } + + try { + $this->assertEntryReturnType($function, $entryReturnType); + } catch (AssertionFailedError $e) { + $this->assertTrue(true); + return; + } catch (ExpectationFailedException $e) { + $this->assertTrue(true); + return; + } + $this->fail("Remove '{$functionName}' from InternalCallMapHandlerTest::\$ignoredReturnTypeOnlyFunctions"); + } + + /** + * This function will test functions that are in the callmap AND currently defined + * @coversNothing + * @depends testGetcallmapReturnsAValidCallmap + * @depends testIgnoresAreSortedAndUnique + * @dataProvider callMapEntryProvider + * @psalm-param callable-string $functionName + * @param array $callMapEntry + */ + public function testCallMapCompliesWithReflection(string $functionName, array $callMapEntry): void + { + if ($this->isIgnored($functionName)) { + $this->markTestSkipped("Function $functionName is ignored in config"); + } + + $function = new ReflectionFunction($functionName); + /** @var string $entryReturnType */ + $entryReturnType = array_shift($callMapEntry); + + /** @var array $callMapEntry */ + $this->assertEntryParameters($function, $callMapEntry); + + if (!$this->isReturnTypeOnlyIgnored($functionName)) { + $this->assertEntryReturnType($function, $entryReturnType); + } + } + + /** + * + * @param array $entryParameters + */ + private function assertEntryParameters(ReflectionFunction $function, array $entryParameters): void + { + /** + * Parse the parameter names from the map. + * @var array + */ + $normalizedEntries = []; + + foreach ($entryParameters as $key => $entry) { + $normalizedKey = $key; + /** + * + * @var array{byRef: bool, refMode: 'rw'|'w', variadic: bool, optional: bool, type: string} $normalizedEntry + */ + $normalizedEntry = [ + 'variadic' => false, + 'byRef' => false, + 'optional' => false, + 'type' => $entry, + ]; + if (strncmp($normalizedKey, '&', 1) === 0) { + $normalizedEntry['byRef'] = true; + $normalizedKey = substr($normalizedKey, 1); + } + + if (strncmp($normalizedKey, '...', 3) === 0) { + $normalizedEntry['variadic'] = true; + $normalizedKey = substr($normalizedKey, 3); + } + + // Read the reference mode + if ($normalizedEntry['byRef']) { + $parts = explode('_', $normalizedKey, 2); + if (count($parts) === 2) { + $normalizedEntry['refMode'] = $parts[0]; + $normalizedKey = $parts[1]; + } else { + $normalizedEntry['refMode'] = 'rw'; + } + } + + // Strip prefixes. + if (substr($normalizedKey, -1, 1) === "=") { + $normalizedEntry['optional'] = true; + $normalizedKey = substr($normalizedKey, 0, -1); + } + + $normalizedEntry['name'] = $normalizedKey; + $normalizedEntries[$normalizedKey] = $normalizedEntry; + } + foreach ($function->getParameters() as $parameter) { + $this->assertArrayHasKey($parameter->getName(), $normalizedEntries, "Callmap is missing entry for param {$parameter->getName()} in {$function->getName()}: " . print_r($normalizedEntries, true)); + $this->assertParameter($normalizedEntries[$parameter->getName()], $parameter); + } + } + + /** + * + * @param array{byRef: bool, refMode: 'rw'|'w', variadic: bool, optional: bool, type: string} $normalizedEntry + */ + private function assertParameter(array $normalizedEntry, ReflectionParameter $param): void + { + $name = $param->getName(); + $this->assertSame($param->isOptional(), $normalizedEntry['optional'], "Expected param '{$name}' to " . ($param->isOptional() ? "be" : "not be") . " optional"); + $this->assertSame($param->isVariadic(), $normalizedEntry['variadic'], "Expected param '{$name}' to " . ($param->isVariadic() ? "be" : "not be") . " variadic"); + $this->assertSame($param->isPassedByReference(), $normalizedEntry['byRef'], "Expected param '{$name}' to " . ($param->isPassedByReference() ? "be" : "not be") . " by reference"); + + $expectedType = $param->getType(); + + if (isset($expectedType) && !empty($normalizedEntry['type'])) { + $this->assertTypeValidity($expectedType, $normalizedEntry['type'], "Param '{$name}' has incorrect type"); + } + } + + /** + * + * @psalm-suppress UndefinedMethod + */ + public function assertEntryReturnType(ReflectionFunction $function, string $entryReturnType): void + { + if (version_compare(PHP_VERSION, '8.1.0', '>=')) { + /** @var ReflectionType|null $expectedType */ + $expectedType = $function->hasTentativeReturnType() ? $function->getTentativeReturnType() : $function->getReturnType(); + } else { + $expectedType = $function->getReturnType(); + } + if ($expectedType === null) { + $this->assertSame('', $entryReturnType, 'CallMap entry has incorrect return type'); + return; + } + + $this->assertTypeValidity($expectedType, $entryReturnType, 'CallMap entry has incorrect return type'); + } + + /** + * Since string equality is too strict, we do some extra checking here + */ + private function assertTypeValidity(ReflectionType $reflected, string $specified, string $message): void + { + $expectedType = Reflection::getPsalmTypeFromReflectionType($reflected); + + $parsedType = Type::parseString($specified); + + try { + $this->assertTrue(UnionTypeComparator::isContainedBy(self::$codebase, $parsedType, $expectedType), $message); + } catch (InvalidArgumentException $e) { + if (preg_match('/^Could not get class storage for (.*)$/', $e->getMessage(), $matches) + && !class_exists($matches[1]) + ) { + $this->fail("Class used in CallMap does not exist: {$matches[1]}"); + } + } + } } diff --git a/tests/InternalAnnotationTest.php b/tests/InternalAnnotationTest.php index feb1ec8f80e..38f6dd5df8a 100644 --- a/tests/InternalAnnotationTest.php +++ b/tests/InternalAnnotationTest.php @@ -556,6 +556,59 @@ public function batBat() : void { } }', ], + 'psalmInternalMultipleNamespaces' => [ + 'code' => ' [ + 'code' => 'bar(); + } + } + } + ' + ], ]; } @@ -606,7 +659,7 @@ public function batBat(): void { } } }', - 'error_message' => 'The method A\Foo::__clone is internal to A but called from B\Bat', + 'error_message' => 'The method A\Foo::__clone is internal to A but called from B', ], 'internalMethodWithCallFromRootNamespace' => [ 'code' => ' 'MixedArgumentTypeCoercion' + 'error_message' => 'TooManyTemplateParams' ], 'concreteDefinesSignatureTypesDifferent' => [ 'code' => ' [ + 'code' => ' $_fooClass */ + function bar(string $_fooClass): void {} + + bar(Foo::class); + ', + ], + 'classStringWithGenericChildSatisfiesGenericParentWithDifferentConstraint' => [ + 'code' => ' + */ + class Bar extends Foo {} + + /** @param class-string $_fooClass */ + function bar(string $_fooClass): void {} + + bar(Bar::class); + ', + ], ]; } diff --git a/tests/TypeReconciliation/EmptyTest.php b/tests/TypeReconciliation/EmptyTest.php index af9eddefaaa..d9d8161b232 100644 --- a/tests/TypeReconciliation/EmptyTest.php +++ b/tests/TypeReconciliation/EmptyTest.php @@ -418,6 +418,35 @@ function foo(array $arr): void { if (empty($arr["a"])) {} }' ], + 'SKIPPED-countWithLiteralIntVariable' => [ // #8163 + 'code' => ' */ + $arr = [1]; + assert(count($arr) === $c); + ', + 'assertions' => ['$arr===' => 'non-empty-list'], + ], + 'SKIPPED-countWithIntRange' => [ // #8163 + 'code' => ' */ + $c = 1; + /** @var list */ + $arr = [1]; + assert(count($arr) === $c); + ', + 'assertions' => ['$arr===' => 'non-empty-list'], + ], + 'SKIPPED-countEmptyWithIntRange' => [ // #8163 + 'code' => ' */ + $c = 1; + /** @var list */ + $arr = [1]; + assert(count($arr) === $c); + ', + 'assertions' => ['$arr===' => 'list'], + ], ]; } diff --git a/tests/TypeReconciliation/TypeAlgebraTest.php b/tests/TypeReconciliation/TypeAlgebraTest.php index 63a4b41c77a..c3cae983726 100644 --- a/tests/TypeReconciliation/TypeAlgebraTest.php +++ b/tests/TypeReconciliation/TypeAlgebraTest.php @@ -1470,6 +1470,17 @@ function foo(?X $x): void { 'ignored_issues' => [], 'php_version' => '8.1', ], + 'arrayShapeListCanBeEmpty' => [ + 'code' => ' $_list */ + function foobar(array $_list): void {} + + $list = random_int(0, 1) ? [] : ["foobar"]; + + foobar($list); + ', + 'error_message' => 'InvalidArgument', + ], ]; } }