From 867290cd06043c05ff5d2d2386efd4780e88e4f8 Mon Sep 17 00:00:00 2001 From: Yannick Gottschalk Date: Mon, 10 Jul 2023 14:09:11 +0200 Subject: [PATCH 01/56] Always report UnusedBaseline issues --- src/Psalm/IssueBuffer.php | 113 +++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 57 deletions(-) diff --git a/src/Psalm/IssueBuffer.php b/src/Psalm/IssueBuffer.php index 56d7b4fb719..8adb31ead82 100644 --- a/src/Psalm/IssueBuffer.php +++ b/src/Psalm/IssueBuffer.php @@ -570,9 +570,8 @@ public static function finish( foreach (self::$issues_data as $file_path => $file_issues) { usort( $file_issues, - static fn(IssueData $d1, IssueData $d2): int => - [$d1->file_path, $d1->line_from, $d1->column_from] - <=> + static fn(IssueData $d1, IssueData $d2): int => [$d1->file_path, $d1->line_from, $d1->column_from] + <=> [$d2->file_path, $d2->line_from, $d2->column_from] ); self::$issues_data[$file_path] = $file_issues; @@ -580,68 +579,68 @@ public static function finish( // make a copy so what gets saved in cache is unaffected by baseline $issues_data = self::$issues_data; + } - if (!empty($issue_baseline)) { - // Set severity for issues in baseline to INFO - foreach ($issues_data as $file_path => $file_issues) { - foreach ($file_issues as $key => $issue_data) { - $file = $issue_data->file_name; - $file = str_replace('\\', '/', $file); - $type = $issue_data->type; - - if (isset($issue_baseline[$file][$type]) && $issue_baseline[$file][$type]['o'] > 0) { - if ($issue_baseline[$file][$type]['o'] === count($issue_baseline[$file][$type]['s'])) { - $position = array_search( - str_replace("\r\n", "\n", trim($issue_data->selected_text)), - $issue_baseline[$file][$type]['s'], - true, - ); - - if ($position !== false) { - $issue_data->severity = IssueData::SEVERITY_INFO; - array_splice($issue_baseline[$file][$type]['s'], $position, 1); - $issue_baseline[$file][$type]['o']--; - } - } else { - $issue_baseline[$file][$type]['s'] = []; + if (!empty($issue_baseline)) { + // Set severity for issues in baseline to INFO + foreach ($issues_data as $file_path => $file_issues) { + foreach ($file_issues as $key => $issue_data) { + $file = $issue_data->file_name; + $file = str_replace('\\', '/', $file); + $type = $issue_data->type; + + if (isset($issue_baseline[$file][$type]) && $issue_baseline[$file][$type]['o'] > 0) { + if ($issue_baseline[$file][$type]['o'] === count($issue_baseline[$file][$type]['s'])) { + $position = array_search( + str_replace("\r\n", "\n", trim($issue_data->selected_text)), + $issue_baseline[$file][$type]['s'], + true, + ); + + if ($position !== false) { $issue_data->severity = IssueData::SEVERITY_INFO; + array_splice($issue_baseline[$file][$type]['s'], $position, 1); $issue_baseline[$file][$type]['o']--; } + } else { + $issue_baseline[$file][$type]['s'] = []; + $issue_data->severity = IssueData::SEVERITY_INFO; + $issue_baseline[$file][$type]['o']--; } - - $issues_data[$file_path][$key] = $issue_data; } + + $issues_data[$file_path][$key] = $issue_data; } + } - if ($codebase->config->find_unused_baseline_entry) { - foreach ($issue_baseline as $file_path => $issues) { - foreach ($issues as $issue_name => $issue) { - if ($issue['o'] !== 0) { - $issues_data[$file_path][] = new IssueData( - IssueData::SEVERITY_ERROR, - 0, - 0, - UnusedBaselineEntry::getIssueType(), - sprintf( - 'Baseline for issue "%s" has %d extra %s.', - $issue_name, - $issue['o'], - $issue['o'] === 1 ? 'entry' : 'entries', - ), - $file_path, - '', - '', - '', - 0, - 0, - 0, - 0, - 0, - 0, - UnusedBaselineEntry::SHORTCODE, - UnusedBaselineEntry::ERROR_LEVEL, - ); - } + if ($codebase->config->find_unused_baseline_entry) { + foreach ($issue_baseline as $file_path => $issues) { + foreach ($issues as $issue_name => $issue) { + if ($issue['o'] !== 0) { + $issues_data[$file_path][] = new IssueData( + IssueData::SEVERITY_ERROR, + 0, + 0, + UnusedBaselineEntry::getIssueType(), + sprintf( + 'Baseline for issue "%s" has %d extra %s.', + $issue_name, + $issue['o'], + $issue['o'] === 1 ? 'entry' : 'entries', + ), + $file_path, + '', + '', + '', + 0, + 0, + 0, + 0, + 0, + 0, + UnusedBaselineEntry::SHORTCODE, + UnusedBaselineEntry::ERROR_LEVEL, + ); } } } From d39013324d5558925c6194d4df7afaf8e40109ac Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Wed, 19 Jul 2023 20:30:57 +0200 Subject: [PATCH 02/56] #10026 added GitHub CI for unit tests with PHP 8.2 and 8.3 --- .github/workflows/ci.yml | 132 +++++++++++++++++++++++++++++++++++++-- composer.json | 2 +- 2 files changed, 129 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0693fb9c4a0..1cfb5800818 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,8 +69,8 @@ jobs: echo "count=$(php -r 'echo json_encode([ ${{ env.CHUNK_COUNT }} ]);')" >> $GITHUB_OUTPUT echo "chunks=$(php -r 'echo json_encode(range(1, ${{ env.CHUNK_COUNT }} ));')" >> $GITHUB_OUTPUT - tests: - name: "Unit Tests - ${{ matrix.chunk }}" + tests_81: + name: "Unit Tests PHP 8.1 - ${{ matrix.chunk }}" runs-on: ubuntu-latest needs: @@ -88,7 +88,7 @@ jobs: PARALLEL_PROCESSES: 5 steps: - - name: Set up PHP + - name: Set up PHP 8.1 uses: shivammathur/setup-php@v2 with: php-version: '8.1' @@ -128,5 +128,129 @@ jobs: env: COMPOSER_ROOT_VERSION: dev-master - - name: Run unit tests + - name: Run unit tests php 8.1 + run: bin/tests-github-actions.sh + + tests_82: + name: "Unit Tests PHP 8.2 - ${{ matrix.chunk }}" + + runs-on: ubuntu-latest + needs: + - chunk-matrix + + strategy: + fail-fast: false + matrix: + count: ${{ fromJson(needs.chunk-matrix.outputs.count) }} + chunk: ${{ fromJson(needs.chunk-matrix.outputs.chunks) }} + + env: + CHUNK_COUNT: "${{ matrix.count }}" + CHUNK_NUMBER: "${{ matrix.chunk }}" + PARALLEL_PROCESSES: 5 + + steps: + - name: Set up PHP 8.2 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + ini-values: zend.assertions=1, assert.exception=1, opcache.enable_cli=1, opcache.jit=function, opcache.jit_buffer_size=512M + tools: composer:v2 + coverage: none + extensions: none, curl, dom, filter, intl, json, libxml, mbstring, opcache, openssl, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter + env: + fail-fast: true + + - uses: actions/checkout@v3 + + - name: Get Composer Cache Directories + id: composer-cache + run: | + echo "files_cache=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + echo "vcs_cache=$(composer config cache-vcs-dir)" >> $GITHUB_OUTPUT + + - name: Generate composer.lock + run: | + composer update --no-install + env: + COMPOSER_ROOT_VERSION: dev-master + + - name: Cache composer cache + uses: actions/cache@v3 + with: + path: | + ${{ steps.composer-cache.outputs.files_cache }} + ${{ steps.composer-cache.outputs.vcs_cache }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Run composer install + run: composer install -o + env: + COMPOSER_ROOT_VERSION: dev-master + + - name: Run unit tests php 8.2 + run: bin/tests-github-actions.sh + + tests_83: + name: "Unit Tests PHP 8.3 - ${{ matrix.chunk }}" + + runs-on: ubuntu-latest + needs: + - chunk-matrix + + strategy: + fail-fast: false + matrix: + count: ${{ fromJson(needs.chunk-matrix.outputs.count) }} + chunk: ${{ fromJson(needs.chunk-matrix.outputs.chunks) }} + + env: + CHUNK_COUNT: "${{ matrix.count }}" + CHUNK_NUMBER: "${{ matrix.chunk }}" + PARALLEL_PROCESSES: 5 + + steps: + - name: Set up PHP 8.3 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + ini-values: zend.assertions=1, assert.exception=1, opcache.enable_cli=1, opcache.jit=function, opcache.jit_buffer_size=512M + tools: composer:v2 + coverage: none + extensions: none, curl, dom, filter, intl, json, libxml, mbstring, opcache, openssl, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter + env: + fail-fast: true + + - uses: actions/checkout@v3 + + - name: Get Composer Cache Directories + id: composer-cache + run: | + echo "files_cache=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + echo "vcs_cache=$(composer config cache-vcs-dir)" >> $GITHUB_OUTPUT + + - name: Generate composer.lock + run: | + composer update --no-install + env: + COMPOSER_ROOT_VERSION: dev-master + + - name: Cache composer cache + uses: actions/cache@v3 + with: + path: | + ${{ steps.composer-cache.outputs.files_cache }} + ${{ steps.composer-cache.outputs.vcs_cache }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Run composer install + run: composer install -o + env: + COMPOSER_ROOT_VERSION: dev-master + + - name: Run unit tests php 8.3 run: bin/tests-github-actions.sh diff --git a/composer.json b/composer.json index 7f4a9f9c8a9..7df7c97af38 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0", + "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", "ext-SimpleXML": "*", "ext-ctype": "*", "ext-dom": "*", From 9eb3c7eb7d4347eb56fca64907bb618520812c3e Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Wed, 19 Jul 2023 20:41:59 +0200 Subject: [PATCH 03/56] #10026 test matrix for different php versions and operating systems --- .github/workflows/ci.yml | 141 ++++----------------------------------- 1 file changed, 13 insertions(+), 128 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1cfb5800818..757b8eef42f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,140 +69,25 @@ jobs: echo "count=$(php -r 'echo json_encode([ ${{ env.CHUNK_COUNT }} ]);')" >> $GITHUB_OUTPUT echo "chunks=$(php -r 'echo json_encode(range(1, ${{ env.CHUNK_COUNT }} ));')" >> $GITHUB_OUTPUT - tests_81: - name: "Unit Tests PHP 8.1 - ${{ matrix.chunk }}" + tests: + name: "Unit Tests - ${{ matrix.chunk }}" runs-on: ubuntu-latest needs: - chunk-matrix - strategy: - fail-fast: false - matrix: - count: ${{ fromJson(needs.chunk-matrix.outputs.count) }} - chunk: ${{ fromJson(needs.chunk-matrix.outputs.chunks) }} - - env: - CHUNK_COUNT: "${{ matrix.count }}" - CHUNK_NUMBER: "${{ matrix.chunk }}" - PARALLEL_PROCESSES: 5 - - steps: - - name: Set up PHP 8.1 - uses: shivammathur/setup-php@v2 - with: - php-version: '8.1' - ini-values: zend.assertions=1, assert.exception=1, opcache.enable_cli=1, opcache.jit=function, opcache.jit_buffer_size=512M - tools: composer:v2 - coverage: none - extensions: none, curl, dom, filter, intl, json, libxml, mbstring, opcache, openssl, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter - env: - fail-fast: true - - - uses: actions/checkout@v3 - - - name: Get Composer Cache Directories - id: composer-cache - run: | - echo "files_cache=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - echo "vcs_cache=$(composer config cache-vcs-dir)" >> $GITHUB_OUTPUT - - - name: Generate composer.lock - run: | - composer update --no-install - env: - COMPOSER_ROOT_VERSION: dev-master - - - name: Cache composer cache - uses: actions/cache@v3 - with: - path: | - ${{ steps.composer-cache.outputs.files_cache }} - ${{ steps.composer-cache.outputs.vcs_cache }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-composer- - - - name: Run composer install - run: composer install -o - env: - COMPOSER_ROOT_VERSION: dev-master - - - name: Run unit tests php 8.1 - run: bin/tests-github-actions.sh - - tests_82: - name: "Unit Tests PHP 8.2 - ${{ matrix.chunk }}" - - runs-on: ubuntu-latest - needs: - - chunk-matrix - - strategy: - fail-fast: false - matrix: - count: ${{ fromJson(needs.chunk-matrix.outputs.count) }} - chunk: ${{ fromJson(needs.chunk-matrix.outputs.chunks) }} - - env: - CHUNK_COUNT: "${{ matrix.count }}" - CHUNK_NUMBER: "${{ matrix.chunk }}" - PARALLEL_PROCESSES: 5 - - steps: - - name: Set up PHP 8.2 - uses: shivammathur/setup-php@v2 - with: - php-version: '8.2' - ini-values: zend.assertions=1, assert.exception=1, opcache.enable_cli=1, opcache.jit=function, opcache.jit_buffer_size=512M - tools: composer:v2 - coverage: none - extensions: none, curl, dom, filter, intl, json, libxml, mbstring, opcache, openssl, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter - env: - fail-fast: true - - - uses: actions/checkout@v3 - - - name: Get Composer Cache Directories - id: composer-cache - run: | - echo "files_cache=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - echo "vcs_cache=$(composer config cache-vcs-dir)" >> $GITHUB_OUTPUT - - - name: Generate composer.lock - run: | - composer update --no-install - env: - COMPOSER_ROOT_VERSION: dev-master - - - name: Cache composer cache - uses: actions/cache@v3 - with: - path: | - ${{ steps.composer-cache.outputs.files_cache }} - ${{ steps.composer-cache.outputs.vcs_cache }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-composer- - - - name: Run composer install - run: composer install -o - env: - COMPOSER_ROOT_VERSION: dev-master - - - name: Run unit tests php 8.2 - run: bin/tests-github-actions.sh - - tests_83: - name: "Unit Tests PHP 8.3 - ${{ matrix.chunk }}" - - runs-on: ubuntu-latest - needs: - - chunk-matrix + runs-on: ${{ matrix.operating-system }} strategy: fail-fast: false matrix: + php-version: + - "7.4" + - "8.0" + - "8.1" + - "8.2" + - "8.3" + operating-system: [ubuntu-latest, windows-latest] count: ${{ fromJson(needs.chunk-matrix.outputs.count) }} chunk: ${{ fromJson(needs.chunk-matrix.outputs.chunks) }} @@ -212,10 +97,10 @@ jobs: PARALLEL_PROCESSES: 5 steps: - - name: Set up PHP 8.3 + - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: "${{ matrix.php-version }}" ini-values: zend.assertions=1, assert.exception=1, opcache.enable_cli=1, opcache.jit=function, opcache.jit_buffer_size=512M tools: composer:v2 coverage: none @@ -252,5 +137,5 @@ jobs: env: COMPOSER_ROOT_VERSION: dev-master - - name: Run unit tests php 8.3 + - name: Run unit tests run: bin/tests-github-actions.sh From e8586a7a35b2db6fa650dfc03b5c0e1b96569b74 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Wed, 19 Jul 2023 20:42:39 +0200 Subject: [PATCH 04/56] #10026 test matrix for different php versions and operating systems --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 757b8eef42f..b47e6daa7a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,11 +72,10 @@ jobs: tests: name: "Unit Tests - ${{ matrix.chunk }}" - runs-on: ubuntu-latest + runs-on: ${{ matrix.operating-system }} needs: - chunk-matrix - runs-on: ${{ matrix.operating-system }} strategy: fail-fast: false From d0605ee765d894079d8f318958a9459e00036308 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Wed, 19 Jul 2023 20:46:50 +0200 Subject: [PATCH 05/56] cleanup --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b47e6daa7a3..a0d1424fd1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,7 +70,7 @@ jobs: echo "chunks=$(php -r 'echo json_encode(range(1, ${{ env.CHUNK_COUNT }} ));')" >> $GITHUB_OUTPUT tests: - name: "Unit Tests - ${{ matrix.chunk }}" + name: "Unit Tests - ${{ matrix.operating-system }} ${{ matrix.php-version }} ${{ matrix.chunk }}" runs-on: ${{ matrix.operating-system }} needs: From ba4e40a9f67ec701d7e11d622460228423543cd2 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Wed, 19 Jul 2023 20:47:58 +0200 Subject: [PATCH 06/56] cleanup --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0d1424fd1c..9c2608df802 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,12 +81,11 @@ jobs: fail-fast: false matrix: php-version: - - "7.4" - "8.0" - "8.1" - "8.2" - "8.3" - operating-system: [ubuntu-latest, windows-latest] + operating-system: [ubuntu-latest] count: ${{ fromJson(needs.chunk-matrix.outputs.count) }} chunk: ${{ fromJson(needs.chunk-matrix.outputs.chunks) }} From 1c55437d78e98d8e9bd41c6fd2ea8eb8faf30957 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Wed, 19 Jul 2023 20:51:13 +0200 Subject: [PATCH 07/56] cleanup --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c2608df802..7902007a745 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: runs-on: ubuntu-latest env: - CHUNK_COUNT: 8 + CHUNK_COUNT: 1 outputs: count: ${{ steps.chunk-matrix.outputs.count }} From fffbf75cf8586b0b505e3e807d4287ab61aeae22 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Wed, 19 Jul 2023 21:06:35 +0200 Subject: [PATCH 08/56] cleanup --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7902007a745..91648f3c09a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: runs-on: ubuntu-latest env: - CHUNK_COUNT: 1 + CHUNK_COUNT: 8 outputs: count: ${{ steps.chunk-matrix.outputs.count }} @@ -70,7 +70,7 @@ jobs: echo "chunks=$(php -r 'echo json_encode(range(1, ${{ env.CHUNK_COUNT }} ));')" >> $GITHUB_OUTPUT tests: - name: "Unit Tests - ${{ matrix.operating-system }} ${{ matrix.php-version }} ${{ matrix.chunk }}" + name: "Tests - ${{ matrix.php-version }} ${{ matrix.chunk }}/${{ matrix.count }} ${{ matrix.operating-system }}" runs-on: ${{ matrix.operating-system }} needs: From 0f99799fa9c3f226efafca913c99a7e2bd43234e Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Wed, 19 Jul 2023 22:52:15 +0200 Subject: [PATCH 09/56] #10026 fixed AnnotationTest --- tests/AnnotationTest.php | 63 +++++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/tests/AnnotationTest.php b/tests/AnnotationTest.php index c6f0f0bc47c..b06bd217e7f 100644 --- a/tests/AnnotationTest.php +++ b/tests/AnnotationTest.php @@ -25,17 +25,28 @@ public function setUp(): void public function testLessSpecificImplementedReturnTypeWithDocblockOnMultipleLines(): void { $this->expectException(CodeException::class); - $this->expectExceptionMessage('LessSpecificImplementedReturnType - somefile.php:5:'); + $this->expectExceptionMessage('LessSpecificImplementedReturnType - somefile.php:16:'); $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); @@ -50,23 +61,23 @@ public function testLessSpecificImplementedReturnTypeWithDocblockOnMultipleLines 'somefile.php', 'analyzeFile('somefile.php', new Context()); @@ -75,19 +86,31 @@ class BreakingThings extends ParentClass public function testLessSpecificImplementedReturnTypeWithDescription(): void { $this->expectException(CodeException::class); - $this->expectExceptionMessage('LessSpecificImplementedReturnType - somefile.php:7:'); + $this->expectExceptionMessage('LessSpecificImplementedReturnType - somefile.php:19:'); $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); From 804087b5d59f70c8186aa234e56ccde5aca0e1cc Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Wed, 19 Jul 2023 23:14:04 +0200 Subject: [PATCH 10/56] #10026 added workaround for DateTimeInterface::__unserialize() signature --- tests/Traits/ValidCodeAnalysisTestTrait.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Traits/ValidCodeAnalysisTestTrait.php b/tests/Traits/ValidCodeAnalysisTestTrait.php index 7a76481d275..c6b78b6c4ec 100644 --- a/tests/Traits/ValidCodeAnalysisTestTrait.php +++ b/tests/Traits/ValidCodeAnalysisTestTrait.php @@ -76,6 +76,19 @@ public function testValidCode( $codebase->enterServerMode(); $codebase->config->visitPreloadedStubFiles($codebase); + if (version_compare(PHP_VERSION, '8.2.0', '>=')) { + $this->addStubFile( + 'stubOne.phpstub', + 'addFile($file_path, $code); From cbee1e094e390fb22a3c1555b96c8b31912d73e2 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Wed, 19 Jul 2023 23:52:25 +0200 Subject: [PATCH 11/56] #10026 adapted DateTime tests for DateMalformedStringException in PHP 8.3 --- .github/workflows/ci.yml | 6 +- tests/DateTimeTest.php | 107 ++++++++++++-------- tests/Traits/ValidCodeAnalysisTestTrait.php | 3 +- 3 files changed, 72 insertions(+), 44 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91648f3c09a..f97b31396b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,9 +70,9 @@ jobs: echo "chunks=$(php -r 'echo json_encode(range(1, ${{ env.CHUNK_COUNT }} ));')" >> $GITHUB_OUTPUT tests: - name: "Tests - ${{ matrix.php-version }} ${{ matrix.chunk }}/${{ matrix.count }} ${{ matrix.operating-system }}" + name: "Tests - PHP ${{ matrix.php-version }} ${{ matrix.chunk }}/${{ matrix.count }}" - runs-on: ${{ matrix.operating-system }} + runs-on: ubuntu-latest needs: - chunk-matrix @@ -81,11 +81,11 @@ jobs: fail-fast: false matrix: php-version: + - "7.4" - "8.0" - "8.1" - "8.2" - "8.3" - operating-system: [ubuntu-latest] count: ${{ fromJson(needs.chunk-matrix.outputs.count) }} chunk: ${{ fromJson(needs.chunk-matrix.outputs.chunks) }} diff --git a/tests/DateTimeTest.php b/tests/DateTimeTest.php index ce9e926ff3a..424bf596d24 100644 --- a/tests/DateTimeTest.php +++ b/tests/DateTimeTest.php @@ -2,12 +2,79 @@ namespace Psalm\Tests; +use Psalm\Context; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; class DateTimeTest extends TestCase { use ValidCodeAnalysisTestTrait; + public function testModifyWithInvalidConstant(): void + { + $context = new Context(); + + if (version_compare(PHP_VERSION, '8.3', '>')) { + $this->expectException(\DateMalformedStringException::class); + $this->expectExceptionMessage('DateTime::modify(): Failed to parse time string (foo) at position 0 (f)'); + } + + $this->addFile( + 'somefile.php', + 'modify(getString()); + $b = $dateTimeImmutable->modify(getString());', + ); + + $this->analyzeFile('somefile.php', $context); + + $this->assertSame('false', $context->vars_in_scope['$a']->getId(true)); + $this->assertSame('false', $context->vars_in_scope['$b']->getId(true)); + } + + public function testModifyWithBothConstant(): void + { + $context = new Context(); + + if (version_compare(PHP_VERSION, '8.3', '>')) { + $this->expectException(\DateMalformedStringException::class); + $this->expectExceptionMessage('DateTime::modify(): Failed to parse time string (bar) at position 0 (b)'); + } + + $this->addFile( + 'somefile.php', + 'modify(getString()); + $b = $dateTimeImmutable->modify(getString());', + ); + + $this->analyzeFile('somefile.php', $context); + + $this->assertSame('DateTime|false', $context->vars_in_scope['$a']->getId(false)); + $this->assertSame('DateTimeImmutable|false', $context->vars_in_scope['$b']->getId(false)); + } + public function providerValidCodeParse(): iterable { return [ @@ -48,46 +115,6 @@ function getString(): string '$b' => 'DateTimeImmutable', ], ], - 'modifyWithInvalidConstant' => [ - 'code' => 'modify(getString()); - $b = $dateTimeImmutable->modify(getString()); - ', - 'assertions' => [ - '$a' => 'false', - '$b' => 'false', - ], - ], - 'modifyWithBothConstant' => [ - 'code' => 'modify(getString()); - $b = $dateTimeImmutable->modify(getString()); - ', - 'assertions' => [ - '$a' => 'DateTime|false', - '$b' => 'DateTimeImmutable|false', - ], - ], 'otherMethodAfterModify' => [ 'code' => 'enterServerMode(); $codebase->config->visitPreloadedStubFiles($codebase); - if (version_compare(PHP_VERSION, '8.2.0', '>=')) { + // avoid MethodSignatureMismatch for __unserialize/() when extending DateTime + if (version_compare(PHP_VERSION, '8.2', '>')) { $this->addStubFile( 'stubOne.phpstub', ' Date: Thu, 20 Jul 2023 00:58:18 +0200 Subject: [PATCH 12/56] #10026 extended callmap delta for php 8.3 --- dictionaries/CallMap.php | 7 ------- dictionaries/CallMap_83_delta.php | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 4145bf41e22..1ec37f85b60 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -491,7 +491,6 @@ 'atan' => ['float', 'num'=>'float'], 'atan2' => ['float', 'y'=>'float', 'x'=>'float'], 'atanh' => ['float', 'num'=>'float'], -'BadFunctionCallException::__clone' => ['void'], 'BadFunctionCallException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'BadFunctionCallException::__toString' => ['string'], 'BadFunctionCallException::getCode' => ['int'], @@ -7652,7 +7651,6 @@ 'mt_getrandmax' => ['int'], 'mt_rand' => ['int', 'min'=>'int', 'max'=>'int'], 'mt_rand\'1' => ['int'], -'mt_srand' => ['void', 'seed='=>'int', 'mode='=>'int'], 'MultipleIterator::__construct' => ['void', 'flags='=>'int'], 'MultipleIterator::attachIterator' => ['void', 'iterator'=>'Iterator', 'info='=>'string|int|null'], 'MultipleIterator::containsIterator' => ['bool', 'iterator'=>'Iterator'], @@ -8129,8 +8127,6 @@ 'MysqlndUhPreparedStatement::__construct' => ['void'], 'MysqlndUhPreparedStatement::execute' => ['bool', 'statement'=>'mysqlnd_prepared_statement'], 'MysqlndUhPreparedStatement::prepare' => ['bool', 'statement'=>'mysqlnd_prepared_statement', 'query'=>'string'], -'natcasesort' => ['bool', '&rw_array'=>'array'], -'natsort' => ['bool', '&rw_array'=>'array'], 'net_get_interfaces' => ['array>|false'], 'newrelic_add_custom_parameter' => ['bool', 'key'=>'string', 'value'=>'bool|float|int|string'], 'newrelic_add_custom_tracer' => ['bool', 'function_name'=>'string'], @@ -9400,7 +9396,6 @@ 'posix_getppid' => ['int'], 'posix_getpwnam' => ['array{name: string, passwd: string, uid: int, gid: int, gecos: string, dir: string, shell: string}|false', 'username'=>'string'], 'posix_getpwuid' => ['array{name: string, passwd: string, uid: int, gid: int, gecos: string, dir: string, shell: string}|false', 'user_id'=>'int'], -'posix_getrlimit' => ['array{"soft core": string, "hard core": string, "soft data": string, "hard data": string, "soft stack": integer, "hard stack": string, "soft totalmem": string, "hard totalmem": string, "soft rss": string, "hard rss": string, "soft maxproc": integer, "hard maxproc": integer, "soft memlock": integer, "hard memlock": integer, "soft cpu": string, "hard cpu": string, "soft filesize": string, "hard filesize": string, "soft openfiles": integer, "hard openfiles": integer}|false'], 'posix_getsid' => ['int|false', 'process_id'=>'int'], 'posix_getuid' => ['int'], 'posix_initgroups' => ['bool', 'username'=>'string', 'group_id'=>'int'], @@ -10851,7 +10846,6 @@ 'RRDGraph::setOptions' => ['void', 'options'=>'array'], 'RRDUpdater::__construct' => ['void', 'path'=>'string'], 'RRDUpdater::update' => ['bool', 'values'=>'array', 'time='=>'string'], -'rsort' => ['bool', '&rw_array'=>'array', 'flags='=>'int'], 'rtrim' => ['string', 'string'=>'string', 'characters='=>'string'], 'runkit7_constant_add' => ['bool', 'constant_name'=>'string', 'value'=>'mixed', 'new_visibility='=>'int'], 'runkit7_constant_redefine' => ['bool', 'constant_name'=>'string', 'value'=>'mixed', 'new_visibility='=>'?int'], @@ -12758,7 +12752,6 @@ 'sqlsrv_send_stream_data' => ['bool', 'stmt'=>'resource'], 'sqlsrv_server_info' => ['array', 'conn'=>'resource'], 'sqrt' => ['float', 'num'=>'float'], -'srand' => ['void', 'seed='=>'int', 'mode='=>'int'], 'sscanf' => ['list|int|null', 'string'=>'string', 'format'=>'string', '&...w_vars='=>'string|int|float|null'], 'ssdeep_fuzzy_compare' => ['int', 'signature1'=>'string', 'signature2'=>'string'], 'ssdeep_fuzzy_hash' => ['string', 'to_hash'=>'string'], diff --git a/dictionaries/CallMap_83_delta.php b/dictionaries/CallMap_83_delta.php index 2d27020ddc5..1c813f3131a 100644 --- a/dictionaries/CallMap_83_delta.php +++ b/dictionaries/CallMap_83_delta.php @@ -25,6 +25,30 @@ 'old' => ['array{runs:int,collected:int,threshold:int,roots:int}'], 'new' => ['array{runs:int,collected:int,threshold:int,roots:int,running:bool,protected:bool,full:bool,buffer_size:int}'], ], + 'srand' => [ + 'old' => ['void', 'seed='=>'int', 'mode='=>'int'], + 'new' => ['void', 'seed='=>'?int', 'mode='=>'int'], + ], + 'mt_srand' => [ + 'old' => ['void', 'seed='=>'int', 'mode='=>'int'], + 'new' =>['void', 'seed='=>'?int', 'mode='=>'int'], + ], + 'posix_getrlimit' => [ + 'old' => ['array{"soft core": string, "hard core": string, "soft data": string, "hard data": string, "soft stack": integer, "hard stack": string, "soft totalmem": string, "hard totalmem": string, "soft rss": string, "hard rss": string, "soft maxproc": integer, "hard maxproc": integer, "soft memlock": integer, "hard memlock": integer, "soft cpu": string, "hard cpu": string, "soft filesize": string, "hard filesize": string, "soft openfiles": integer, "hard openfiles": integer}|false'], + 'new' => ['array{"resource": string, "soft core": string, "hard core": string, "soft data": string, "hard data": string, "soft stack": integer, "hard stack": string, "soft totalmem": string, "hard totalmem": string, "soft rss": string, "hard rss": string, "soft maxproc": integer, "hard maxproc": integer, "soft memlock": integer, "hard memlock": integer, "soft cpu": string, "hard cpu": string, "soft filesize": string, "hard filesize": string, "soft openfiles": integer, "hard openfiles": integer}|false'], + ], + 'natcasesort' => [ + 'old' => ['bool', '&rw_array'=>'array'], + 'new' => ['true', '&rw_array'=>'array'], + ], + 'natsort' => [ + 'old' => ['bool', '&rw_array'=>'array'], + 'new' => ['true', '&rw_array'=>'array'], + ], + 'rsort' => [ + 'old' => ['bool', '&rw_array'=>'array', 'flags='=>'int'], + 'new' => ['true', '&rw_array'=>'array', 'flags='=>'int'], + ], ], 'removed' => [ From f34327e079eba52106751c227e6291d2e04b5752 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Thu, 20 Jul 2023 01:01:10 +0200 Subject: [PATCH 13/56] 10026 extended InternalCallMapHandlerTest ignores for php 8.3 --- .../Codebase/InternalCallMapHandlerTest.php | 129 ++++++++++++------ 1 file changed, 85 insertions(+), 44 deletions(-) diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index b21fc991a6b..03c60132459 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -73,15 +73,28 @@ class InternalCallMapHandlerTest extends TestCase * @var array> */ private static array $ignoredFunctions = [ + 'argumentcounterror::__clone' => ['8.3'], + 'arithmeticerror::__clone' => ['8.3'], 'array_multisort', + 'badfunctioncallexception::__clone' => ['8.3'], + 'badmethodcallexception::__clone' => ['8.3'], + 'closedgeneratorexception::__clone' => ['8.3'], 'datefmt_create' => ['8.0'], + 'domainexception::__clone' => ['8.3'], + 'errorexception::__clone' => ['8.3'], 'fiber::start', 'imagefilledpolygon', 'imagegd', 'imagegd2', 'imageopenpolygon', 'imagepolygon', + 'intlcodepointbreakiterator::__construct' => ['8.3'], + 'intlexception::__clone' => ['8.3'], 'intlgregoriancalendar::__construct', + 'invalidargumentexception::__clone' => ['8.3'], + 'jsonexception::__clone' => ['8.3'], + 'lengthexception::__clone' => ['8.3'], + 'logicexception::__clone' => ['8.3'], 'lzf_compress', 'lzf_decompress', 'mailparse_msg_extract_part', @@ -140,7 +153,15 @@ class InternalCallMapHandlerTest extends TestCase 'oci_result', 'ocigetbufferinglob', 'ocisetbufferinglob', + 'outofboundsexception::__clone' => ['8.3'], + 'outofrangeexception::__clone' => ['8.3'], + 'overflowexception::__clone' => ['8.3'], + 'parseerror::__clone' => ['8.3'], + 'rangeexception::__clone' => ['8.3'], 'recursiveiteratoriterator::__construct', // Class used in CallMap does not exist: recursiveiterator + 'reflectionnamedtype::__clone' => ['8.3'], + 'reflectionobject::__clone' => ['8.3'], + 'runtimeexception::__clone' => ['8.3'], 'sqlsrv_fetch_array', 'sqlsrv_fetch_object', 'sqlsrv_get_field', @@ -148,6 +169,9 @@ class InternalCallMapHandlerTest extends TestCase 'sqlsrv_query', 'sqlsrv_server_info', 'ssh2_forward_accept', + 'typeerror::__clone' => ['8.3'], + 'underflowexception::__clone' => ['8.3'], + 'unexpectedvalueexception::__clone' => ['8.3'], 'xdiff_file_bdiff', 'xdiff_file_bdiff_size', 'xdiff_file_diff', @@ -171,62 +195,79 @@ class InternalCallMapHandlerTest extends TestCase * @var array> */ private static array $ignoredReturnTypeOnlyFunctions = [ - 'appenditerator::getinneriterator' => ['8.1', '8.2'], - 'appenditerator::getiteratorindex' => ['8.1', '8.2'], - 'arrayobject::getiterator' => ['8.1', '8.2'], - 'cachingiterator::getinneriterator' => ['8.1', '8.2'], - 'callbackfilteriterator::getinneriterator' => ['8.1', '8.2'], + 'appenditerator::getinneriterator' => ['8.1', '8.2', '8.3'], + 'appenditerator::getiteratorindex' => ['8.1', '8.2', '8.3'], + 'arrayobject::getiterator' => ['8.1', '8.2', '8.3'], + 'cachingiterator::getinneriterator' => ['8.1', '8.2', '8.3'], + 'callbackfilteriterator::getinneriterator' => ['8.1', '8.2', '8.3'], 'curl_multi_getcontent', - 'datetime::add' => ['8.1', '8.2'], // DateTime does not contain static - 'datetime::modify' => ['8.1', '8.2'], // DateTime does not contain static - 'datetime::createfromformat' => ['8.1', '8.2'], // DateTime does not contain static + 'datetime::add' => ['8.1', '8.2', '8.3'], // DateTime does not contain static + 'datetime::modify' => ['8.1', '8.2', '8.3'], // DateTime does not contain static + 'datetime::createfromformat' => ['8.1', '8.2', '8.3'], // DateTime does not contain static 'datetime::createfromimmutable' => ['8.1'], 'datetime::createfrominterface', - 'datetime::setdate' => ['8.1', '8.2'], // DateTime does not contain static - 'datetime::setisodate' => ['8.1', '8.2'], // DateTime does not contain static - 'datetime::settime' => ['8.1', '8.2'], // DateTime does not contain static - 'datetime::settimestamp' => ['8.1', '8.2'], // DateTime does not contain static - 'datetime::settimezone' => ['8.1', '8.2'], // DateTime does not contain static - 'datetime::sub' => ['8.1', '8.2'], // DateTime does not contain static + 'datetime::setdate' => ['8.1', '8.2', '8.3'], // DateTime does not contain static + 'datetime::setisodate' => ['8.1', '8.2', '8.3'], // DateTime does not contain static + 'datetime::settime' => ['8.1', '8.2', '8.3'], // DateTime does not contain static + 'datetime::settimestamp' => ['8.1', '8.2', '8.3'], // DateTime does not contain static + 'datetime::settimezone' => ['8.1', '8.2', '8.3'], // DateTime does not contain static + 'datetime::sub' => ['8.1', '8.2', '8.3'], // DateTime does not contain static 'datetimeimmutable::createfrominterface', 'fiber::getcurrent', - 'filteriterator::getinneriterator' => ['8.1', '8.2'], + 'filteriterator::getinneriterator' => ['8.1', '8.2', '8.3'], 'get_cfg_var', // Ignore array return type - 'infiniteiterator::getinneriterator' => ['8.1', '8.2'], - 'iteratoriterator::getinneriterator' => ['8.1', '8.2'], - 'limititerator::getinneriterator' => ['8.1', '8.2'], - 'locale::canonicalize' => ['8.1', '8.2'], - 'locale::getallvariants' => ['8.1', '8.2'], - 'locale::getkeywords' => ['8.1', '8.2'], - 'locale::getprimarylanguage' => ['8.1', '8.2'], - 'locale::getregion' => ['8.1', '8.2'], - 'locale::getscript' => ['8.1', '8.2'], - 'locale::parselocale' => ['8.1', '8.2'], - 'messageformatter::create' => ['8.1', '8.2'], - 'multipleiterator::current' => ['8.1', '8.2'], - 'mysqli::get_charset' => ['8.1', '8.2'], - 'mysqli_stmt::get_warnings' => ['8.1', '8.2'], + 'infiniteiterator::getinneriterator' => ['8.1', '8.2', '8.3'], + 'iteratoriterator::getinneriterator' => ['8.1', '8.2', '8.3'], + 'limititerator::getinneriterator' => ['8.1', '8.2', '8.3'], + 'locale::canonicalize' => ['8.1', '8.2', '8.3'], + 'locale::getallvariants' => ['8.1', '8.2', '8.3'], + 'locale::getkeywords' => ['8.1', '8.2', '8.3'], + 'locale::getprimarylanguage' => ['8.1', '8.2', '8.3'], + 'locale::getregion' => ['8.1', '8.2', '8.3'], + 'locale::getscript' => ['8.1', '8.2', '8.3'], + 'locale::parselocale' => ['8.1', '8.2', '8.3'], + 'messageformatter::create' => ['8.1', '8.2', '8.3'], + 'multipleiterator::current' => ['8.1', '8.2', '8.3'], + 'mysqli::get_charset' => ['8.1', '8.2', '8.3'], + 'mysqli_stmt::get_warnings' => ['8.1', '8.2', '8.3'], 'mysqli_stmt_get_warnings', 'mysqli_stmt_insert_id', - 'norewinditerator::getinneriterator' => ['8.1', '8.2'], + 'norewinditerator::getinneriterator' => ['8.1', '8.2', '8.3'], 'passthru', - 'recursivecachingiterator::getinneriterator' => ['8.1', '8.2'], - 'recursivecallbackfilteriterator::getinneriterator' => ['8.1', '8.2'], - 'recursivefilteriterator::getinneriterator' => ['8.1', '8.2'], - 'recursiveregexiterator::getinneriterator' => ['8.1', '8.2'], + 'recursivecachingiterator::getinneriterator' => ['8.1', '8.2', '8.3'], + 'recursivecallbackfilteriterator::getinneriterator' => ['8.1', '8.2', '8.3'], + 'recursivefilteriterator::getinneriterator' => ['8.1', '8.2', '8.3'], + 'recursiveregexiterator::getinneriterator' => ['8.1', '8.2', '8.3'], 'reflectionclass::getstaticproperties' => ['8.1', '8.2'], - 'reflectionclass::newinstanceargs' => ['8.1', '8.2'], - 'reflectionfunction::getclosurescopeclass' => ['8.1', '8.2'], - 'reflectionfunction::getclosurethis' => ['8.1', '8.2'], - 'reflectionmethod::getclosurescopeclass' => ['8.1', '8.2'], - 'reflectionmethod::getclosurethis' => ['8.1', '8.2'], + 'reflectionclass::newinstanceargs' => ['8.1', '8.2', '8.3'], + 'reflectionfunction::getclosurescopeclass' => ['8.1', '8.2', '8.3'], + 'reflectionfunction::getclosurethis' => ['8.1', '8.2', '8.3'], + 'reflectionmethod::getclosurescopeclass' => ['8.1', '8.2', '8.3'], + 'reflectionmethod::getclosurethis' => ['8.1', '8.2', '8.3'], 'reflectionobject::getstaticproperties' => ['8.1', '8.2'], - 'reflectionobject::newinstanceargs' => ['8.1', '8.2'], - 'regexiterator::getinneriterator' => ['8.1', '8.2'], + 'reflectionobject::newinstanceargs' => ['8.1', '8.2', '8.3'], + 'regexiterator::getinneriterator' => ['8.1', '8.2', '8.3'], 'register_shutdown_function' => ['8.0', '8.1'], - 'splfileobject::fscanf' => ['8.1', '8.2'], - 'spltempfileobject::fscanf' => ['8.1', '8.2'], - 'xsltprocessor::transformtoxml' => ['8.1', '8.2'], + 'splfileobject::fscanf' => ['8.1', '8.2', '8.3'], + 'spltempfileobject::fscanf' => ['8.1', '8.2', '8.3'], + 'xsltprocessor::transformtoxml' => ['8.1', '8.2', '8.3'], + 'intlcal_clear' => ['8.3'], + 'intlrulebasedbreakiterator::settext' => ['8.3'], + 'intlcodepointbreakiterator::__construct' => ['8.3'], + 'intlcodepointbreakiterator::settext' => ['8.3'], + 'intldateformatter::settimezone' => ['8.3'], + 'intlchar::enumcharnames' => ['8.3'], + 'intlbreakiterator::settext' => ['8.3'], + 'intlcal_set_lenient' => ['8.3'], + 'intlcal_set_first_day_of_week' => ['8.3'], + 'datefmt_set_timezone' => ['8.3'], + 'imap_setflag_full' => ['8.3'], + 'imap_expunge' => ['8.3'], + 'imap_gc' => ['8.3'], + 'imap_undelete' => ['8.3'], + 'imap_delete' => ['8.3'], + 'imap_clearflag_full' => ['8.3'], + 'imap_close' => ['8.3'], ]; /** From ee185b9ad84fe39a5bbe4a79ddad9334accd7dff Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Thu, 20 Jul 2023 01:28:46 +0200 Subject: [PATCH 14/56] #10026 cleanup method signatures --- .github/workflows/ci.yml | 1 - dictionaries/CallMap.php | 7 +++++++ dictionaries/CallMap_83_delta.php | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f97b31396b3..fb54eec7b4b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,6 @@ jobs: fail-fast: false matrix: php-version: - - "7.4" - "8.0" - "8.1" - "8.2" diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 1ec37f85b60..79d13f4dfae 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -491,6 +491,7 @@ 'atan' => ['float', 'num'=>'float'], 'atan2' => ['float', 'y'=>'float', 'x'=>'float'], 'atanh' => ['float', 'num'=>'float'], +'BadFunctionCallException::__clone' => ['void'], 'BadFunctionCallException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'BadFunctionCallException::__toString' => ['string'], 'BadFunctionCallException::getCode' => ['int'], @@ -7651,6 +7652,7 @@ 'mt_getrandmax' => ['int'], 'mt_rand' => ['int', 'min'=>'int', 'max'=>'int'], 'mt_rand\'1' => ['int'], +'mt_srand' => ['void', 'seed='=>'?int', 'mode='=>'int'], 'MultipleIterator::__construct' => ['void', 'flags='=>'int'], 'MultipleIterator::attachIterator' => ['void', 'iterator'=>'Iterator', 'info='=>'string|int|null'], 'MultipleIterator::containsIterator' => ['bool', 'iterator'=>'Iterator'], @@ -8127,6 +8129,8 @@ 'MysqlndUhPreparedStatement::__construct' => ['void'], 'MysqlndUhPreparedStatement::execute' => ['bool', 'statement'=>'mysqlnd_prepared_statement'], 'MysqlndUhPreparedStatement::prepare' => ['bool', 'statement'=>'mysqlnd_prepared_statement', 'query'=>'string'], +'natcasesort' => ['true', '&rw_array'=>'array'], +'natsort' => ['true', '&rw_array'=>'array'], 'net_get_interfaces' => ['array>|false'], 'newrelic_add_custom_parameter' => ['bool', 'key'=>'string', 'value'=>'bool|float|int|string'], 'newrelic_add_custom_tracer' => ['bool', 'function_name'=>'string'], @@ -9396,6 +9400,7 @@ 'posix_getppid' => ['int'], 'posix_getpwnam' => ['array{name: string, passwd: string, uid: int, gid: int, gecos: string, dir: string, shell: string}|false', 'username'=>'string'], 'posix_getpwuid' => ['array{name: string, passwd: string, uid: int, gid: int, gecos: string, dir: string, shell: string}|false', 'user_id'=>'int'], +'posix_getrlimit' => ['array{"soft core": string, "hard core": string, "soft data": string, "hard data": string, "soft stack": integer, "hard stack": string, "soft totalmem": string, "hard totalmem": string, "soft rss": string, "hard rss": string, "soft maxproc": integer, "hard maxproc": integer, "soft memlock": integer, "hard memlock": integer, "soft cpu": string, "hard cpu": string, "soft filesize": string, "hard filesize": string, "soft openfiles": integer, "hard openfiles": integer}|false', 'resource=' => '?int'], 'posix_getsid' => ['int|false', 'process_id'=>'int'], 'posix_getuid' => ['int'], 'posix_initgroups' => ['bool', 'username'=>'string', 'group_id'=>'int'], @@ -10846,6 +10851,7 @@ 'RRDGraph::setOptions' => ['void', 'options'=>'array'], 'RRDUpdater::__construct' => ['void', 'path'=>'string'], 'RRDUpdater::update' => ['bool', 'values'=>'array', 'time='=>'string'], +'rsort' => ['true', '&rw_array'=>'array', 'flags='=>'int'], 'rtrim' => ['string', 'string'=>'string', 'characters='=>'string'], 'runkit7_constant_add' => ['bool', 'constant_name'=>'string', 'value'=>'mixed', 'new_visibility='=>'int'], 'runkit7_constant_redefine' => ['bool', 'constant_name'=>'string', 'value'=>'mixed', 'new_visibility='=>'?int'], @@ -12752,6 +12758,7 @@ 'sqlsrv_send_stream_data' => ['bool', 'stmt'=>'resource'], 'sqlsrv_server_info' => ['array', 'conn'=>'resource'], 'sqrt' => ['float', 'num'=>'float'], +'srand' => ['void', 'seed='=>'?int', 'mode='=>'int'], 'sscanf' => ['list|int|null', 'string'=>'string', 'format'=>'string', '&...w_vars='=>'string|int|float|null'], 'ssdeep_fuzzy_compare' => ['int', 'signature1'=>'string', 'signature2'=>'string'], 'ssdeep_fuzzy_hash' => ['string', 'to_hash'=>'string'], diff --git a/dictionaries/CallMap_83_delta.php b/dictionaries/CallMap_83_delta.php index 1c813f3131a..9e242943323 100644 --- a/dictionaries/CallMap_83_delta.php +++ b/dictionaries/CallMap_83_delta.php @@ -35,7 +35,7 @@ ], 'posix_getrlimit' => [ 'old' => ['array{"soft core": string, "hard core": string, "soft data": string, "hard data": string, "soft stack": integer, "hard stack": string, "soft totalmem": string, "hard totalmem": string, "soft rss": string, "hard rss": string, "soft maxproc": integer, "hard maxproc": integer, "soft memlock": integer, "hard memlock": integer, "soft cpu": string, "hard cpu": string, "soft filesize": string, "hard filesize": string, "soft openfiles": integer, "hard openfiles": integer}|false'], - 'new' => ['array{"resource": string, "soft core": string, "hard core": string, "soft data": string, "hard data": string, "soft stack": integer, "hard stack": string, "soft totalmem": string, "hard totalmem": string, "soft rss": string, "hard rss": string, "soft maxproc": integer, "hard maxproc": integer, "soft memlock": integer, "hard memlock": integer, "soft cpu": string, "hard cpu": string, "soft filesize": string, "hard filesize": string, "soft openfiles": integer, "hard openfiles": integer}|false'], + 'new' => ['array{"soft core": string, "hard core": string, "soft data": string, "hard data": string, "soft stack": integer, "hard stack": string, "soft totalmem": string, "hard totalmem": string, "soft rss": string, "hard rss": string, "soft maxproc": integer, "hard maxproc": integer, "soft memlock": integer, "hard memlock": integer, "soft cpu": string, "hard cpu": string, "soft filesize": string, "hard filesize": string, "soft openfiles": integer, "hard openfiles": integer}|false', 'resource=' => '?int'], ], 'natcasesort' => [ 'old' => ['bool', '&rw_array'=>'array'], From 741d19682a988e380c7f2fca241c57aab95e7e25 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Thu, 20 Jul 2023 01:43:31 +0200 Subject: [PATCH 15/56] #10026 cleanup signature for hash_pbkdf2() --- .github/workflows/ci.yml | 2 +- dictionaries/CallMap_83_delta.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb54eec7b4b..21436d3f397 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,7 +70,7 @@ jobs: echo "chunks=$(php -r 'echo json_encode(range(1, ${{ env.CHUNK_COUNT }} ));')" >> $GITHUB_OUTPUT tests: - name: "Tests - PHP ${{ matrix.php-version }} ${{ matrix.chunk }}/${{ matrix.count }}" + name: "Unit Tests - PHP ${{ matrix.php-version }} ${{ matrix.chunk }}/${{ matrix.count }}" runs-on: ubuntu-latest needs: diff --git a/dictionaries/CallMap_83_delta.php b/dictionaries/CallMap_83_delta.php index 9e242943323..eee4d2d2d3b 100644 --- a/dictionaries/CallMap_83_delta.php +++ b/dictionaries/CallMap_83_delta.php @@ -49,6 +49,10 @@ 'old' => ['bool', '&rw_array'=>'array', 'flags='=>'int'], 'new' => ['true', '&rw_array'=>'array', 'flags='=>'int'], ], + 'hash_pbkdf2' => [ + 'old' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool'], + 'new' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool', 'options=' => 'array'], + ], ], 'removed' => [ From 51058eb45e5f17229ab561e9d1f70ed10d14853c Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Thu, 20 Jul 2023 02:02:28 +0200 Subject: [PATCH 16/56] #10026 cleanup --- .github/workflows/ci.yml | 1 - dictionaries/CallMap.php | 2 +- tests/DateTimeTest.php | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21436d3f397..8649a7189d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,7 +76,6 @@ jobs: needs: - chunk-matrix - strategy: fail-fast: false matrix: diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 79d13f4dfae..b4bf5fed75b 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -3925,7 +3925,7 @@ 'hash_hmac_algos' => ['list'], 'hash_hmac_file' => ['non-empty-string', 'algo'=>'string', 'filename'=>'string', 'key'=>'string', 'binary='=>'bool'], 'hash_init' => ['HashContext', 'algo'=>'string', 'flags='=>'int', 'key='=>'string', 'options='=>'array{seed:scalar}'], -'hash_pbkdf2' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool'], +'hash_pbkdf2' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool', 'options=' => 'array'], 'hash_update' => ['bool', 'context'=>'HashContext', 'data'=>'string'], 'hash_update_file' => ['bool', 'context'=>'HashContext', 'filename'=>'string', 'stream_context='=>'?resource'], 'hash_update_stream' => ['int', 'context'=>'HashContext', 'stream'=>'resource', 'length='=>'int'], diff --git a/tests/DateTimeTest.php b/tests/DateTimeTest.php index 424bf596d24..741530de70e 100644 --- a/tests/DateTimeTest.php +++ b/tests/DateTimeTest.php @@ -14,7 +14,7 @@ public function testModifyWithInvalidConstant(): void $context = new Context(); if (version_compare(PHP_VERSION, '8.3', '>')) { - $this->expectException(\DateMalformedStringException::class); + $this->expectException(\Exception::class); $this->expectExceptionMessage('DateTime::modify(): Failed to parse time string (foo) at position 0 (f)'); } @@ -47,7 +47,7 @@ public function testModifyWithBothConstant(): void $context = new Context(); if (version_compare(PHP_VERSION, '8.3', '>')) { - $this->expectException(\DateMalformedStringException::class); + $this->expectException(\Exception::class); $this->expectExceptionMessage('DateTime::modify(): Failed to parse time string (bar) at position 0 (b)'); } From 719496b46e03b3b23d2d75633fc201103791e23f Mon Sep 17 00:00:00 2001 From: cgocast Date: Fri, 21 Jul 2023 15:27:47 +0200 Subject: [PATCH 17/56] #10030 Add PDOStatement::bindValuebindValue() and PDOStatement::bindParam() as sources for TaintedSql --- stubs/extensions/pdo.phpstub | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/stubs/extensions/pdo.phpstub b/stubs/extensions/pdo.phpstub index 039b565e2d7..aec4965477c 100644 --- a/stubs/extensions/pdo.phpstub +++ b/stubs/extensions/pdo.phpstub @@ -150,6 +150,16 @@ class PDOStatement implements Traversable * @return false|T */ public function fetchObject($class = \stdclass::class, array $ctorArgs = array()) {} + + /** + * @psalm-taint-sink sql $value + */ + public function bindValue(string|int $param, mixed $value, int $type = PDO::PARAM_STR): bool {} + + /** + * @psalm-taint-sink sql $var + */ + public function bindParam(string|int $param, mixed &$var, int $type = PDO::PARAM_STR, int $maxLength = 0, mixed $driverOptions = null): bool {} } class PDOException extends RuntimeException { From 5affc125e8c36dcb6e336f1c106196fc25c8bf67 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Fri, 21 Jul 2023 21:48:28 +0200 Subject: [PATCH 18/56] #10026 run phpcs and phar-build with php 8.0 in circle-ci (php 7.4 is out of official support) --- .circleci/config.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fb7edfe00e2..9ac95561065 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,5 @@ # Use the latest 2.1 version of CircleCI pipeline processing engine, see https://circleci.com/docs/2.0/configuration-reference/ +# See https://app.circleci.com/pipelines/github/vimeo/psalm version: 2.1 executors: php-74: @@ -12,7 +13,7 @@ executors: - image: thecodingmachine/php:8.1-v4-cli jobs: "Code Style Analysis": - executor: php-74 + executor: php-80 steps: - checkout @@ -40,7 +41,7 @@ jobs: command: vendor/bin/phpcs -d memory_limit=512M phar-build: - executor: php-74 + executor: php-80 steps: - attach_workspace: at: /home/docker/project/ From 4307bc49e3602bff0d3bb33b3c02e793b8eea151 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Fri, 21 Jul 2023 21:49:07 +0200 Subject: [PATCH 19/56] #10026 run buuld-phar with php 8.0 in github actions (php 7.4 is out of official support) --- .github/workflows/build-phar.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-phar.yml b/.github/workflows/build-phar.yml index 8d75dfe63c3..b042b090497 100644 --- a/.github/workflows/build-phar.yml +++ b/.github/workflows/build-phar.yml @@ -38,7 +38,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.0' tools: composer:v2 coverage: none env: From fc1d3e09abb761a1df54c2a8d59255a86dd47d47 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Fri, 21 Jul 2023 21:49:36 +0200 Subject: [PATCH 20/56] #10026 added code style analysis with phpcs to github actions --- .github/workflows/ci.yml | 47 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8649a7189d9..4ad2fd3b7ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.0' tools: composer:v2 coverage: none env: @@ -49,6 +49,51 @@ jobs: - run: | git ls-files | grep \\\.php$ | grep -v ^dictionaries/scripts/* | ./vendor/bin/parallel-lint --stdin + + code-style: + name: Code Style Analysis + runs-on: ubuntu-latest + steps: + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + tools: composer:v2 + coverage: none + env: + fail-fast: true + + - uses: actions/checkout@v3 + + - name: Get Composer Cache Directories + id: composer-cache + run: | + echo "files_cache=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + echo "vcs_cache=$(composer config cache-vcs-dir)" >> $GITHUB_OUTPUT + + - name: Generate composer.lock + run: composer update --no-install + env: + COMPOSER_ROOT_VERSION: dev-master + + - name: Cache composer cache + uses: actions/cache@v3 + with: + path: | + ${{ steps.composer-cache.outputs.files_cache }} + ${{ steps.composer-cache.outputs.vcs_cache }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Run composer install + run: composer install -o + env: + COMPOSER_ROOT_VERSION: dev-master + + - name: Code Style Analysis with PHPCS + run: vendor/bin/phpcs -d memory_limit=512M + chunk-matrix: permissions: contents: none From 9590279c6213b1457e2d156f96de8adc65ba00ed Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Fri, 21 Jul 2023 21:50:30 +0200 Subject: [PATCH 21/56] #10026 added style changes to pass phpcs check --- tests/DateTimeTest.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/DateTimeTest.php b/tests/DateTimeTest.php index 741530de70e..b6fbe2ef2e8 100644 --- a/tests/DateTimeTest.php +++ b/tests/DateTimeTest.php @@ -2,9 +2,14 @@ namespace Psalm\Tests; +use Exception; use Psalm\Context; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; +use function version_compare; + +use const PHP_VERSION; + class DateTimeTest extends TestCase { use ValidCodeAnalysisTestTrait; @@ -14,7 +19,7 @@ public function testModifyWithInvalidConstant(): void $context = new Context(); if (version_compare(PHP_VERSION, '8.3', '>')) { - $this->expectException(\Exception::class); + $this->expectException(Exception::class); $this->expectExceptionMessage('DateTime::modify(): Failed to parse time string (foo) at position 0 (f)'); } @@ -47,7 +52,7 @@ public function testModifyWithBothConstant(): void $context = new Context(); if (version_compare(PHP_VERSION, '8.3', '>')) { - $this->expectException(\Exception::class); + $this->expectException(Exception::class); $this->expectExceptionMessage('DateTime::modify(): Failed to parse time string (bar) at position 0 (b)'); } From 7f39dab07bc0d44f569138f00b0b2600b34f506d Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Fri, 21 Jul 2023 22:26:05 +0200 Subject: [PATCH 22/56] #10026 moved some more signature changes to CallMap_83_delta --- dictionaries/CallMap.php | 56 ++++-------- dictionaries/CallMap_83_delta.php | 88 +++++++++++++++++++ .../Codebase/InternalCallMapHandlerTest.php | 41 --------- 3 files changed, 104 insertions(+), 81 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index b4bf5fed75b..7d8c477b1a1 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -337,7 +337,6 @@ 'AppendIterator::next' => ['void'], 'AppendIterator::rewind' => ['void'], 'AppendIterator::valid' => ['bool'], -'ArgumentCountError::__clone' => ['void'], 'ArgumentCountError::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'ArgumentCountError::__toString' => ['string'], 'ArgumentCountError::__wakeup' => ['void'], @@ -348,7 +347,6 @@ 'ArgumentCountError::getPrevious' => ['?Throwable'], 'ArgumentCountError::getTrace' => ['list\',args?:array}>'], 'ArgumentCountError::getTraceAsString' => ['string'], -'ArithmeticError::__clone' => ['void'], 'ArithmeticError::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'ArithmeticError::__toString' => ['string'], 'ArithmeticError::__wakeup' => ['void'], @@ -491,7 +489,6 @@ 'atan' => ['float', 'num'=>'float'], 'atan2' => ['float', 'y'=>'float', 'x'=>'float'], 'atanh' => ['float', 'num'=>'float'], -'BadFunctionCallException::__clone' => ['void'], 'BadFunctionCallException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'BadFunctionCallException::__toString' => ['string'], 'BadFunctionCallException::getCode' => ['int'], @@ -501,7 +498,6 @@ 'BadFunctionCallException::getPrevious' => ['?Throwable'], 'BadFunctionCallException::getTrace' => ['list\',args?:array}>'], 'BadFunctionCallException::getTraceAsString' => ['string'], -'BadMethodCallException::__clone' => ['void'], 'BadMethodCallException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'BadMethodCallException::__toString' => ['string'], 'BadMethodCallException::getCode' => ['int'], @@ -661,7 +657,6 @@ 'clearstatcache' => ['void', 'clear_realpath_cache='=>'bool', 'filename='=>'string'], 'cli_get_process_title' => ['?string'], 'cli_set_process_title' => ['bool', 'title'=>'string'], -'ClosedGeneratorException::__clone' => ['void'], 'ClosedGeneratorException::__toString' => ['string'], 'ClosedGeneratorException::getCode' => ['int'], 'ClosedGeneratorException::getFile' => ['string'], @@ -1352,7 +1347,7 @@ 'datefmt_set_calendar' => ['bool', 'formatter'=>'IntlDateFormatter', 'calendar'=>'IntlCalendar|int|null'], 'datefmt_set_lenient' => ['void', 'formatter'=>'IntlDateFormatter', 'lenient'=>'bool'], 'datefmt_set_pattern' => ['bool', 'formatter'=>'IntlDateFormatter', 'pattern'=>'string'], -'datefmt_set_timezone' => ['false|null', 'formatter'=>'IntlDateFormatter', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], +'datefmt_set_timezone' => ['bool', 'formatter'=>'IntlDateFormatter', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], 'DateInterval::__construct' => ['void', 'duration'=>'string'], 'DateInterval::__set_state' => ['DateInterval', 'array'=>'array'], 'DateInterval::__wakeup' => ['void'], @@ -1633,7 +1628,6 @@ 'dom_xpath_query' => ['DOMNodeList', 'expr'=>'string', 'context'=>'DOMNode', 'registernodens'=>'bool'], 'dom_xpath_register_ns' => ['bool', 'prefix'=>'string', 'uri'=>'string'], 'dom_xpath_register_php_functions' => [''], -'DomainException::__clone' => ['void'], 'DomainException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'DomainException::__toString' => ['string'], 'DomainException::__wakeup' => ['void'], @@ -2093,7 +2087,6 @@ 'error_get_last' => ['?array{type:int,message:string,file:string,line:int}'], 'error_log' => ['bool', 'message'=>'string', 'message_type='=>'int', 'destination='=>'?string', 'additional_headers='=>'?string'], 'error_reporting' => ['int', 'error_level='=>'?int'], -'ErrorException::__clone' => ['void'], 'ErrorException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'severity='=>'int', 'filename='=>'?string', 'line='=>'?int', 'previous='=>'?Throwable'], 'ErrorException::__toString' => ['string'], 'ErrorException::getCode' => ['int'], @@ -5581,21 +5574,21 @@ 'imap_body' => ['string|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'flags='=>'int'], 'imap_bodystruct' => ['stdClass|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'section'=>'string'], 'imap_check' => ['stdClass|false', 'imap'=>'IMAP\Connection'], -'imap_clearflag_full' => ['bool', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flag'=>'string', 'options='=>'int'], -'imap_close' => ['bool', 'imap'=>'IMAP\Connection', 'flags='=>'int'], +'imap_clearflag_full' => ['true', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flag'=>'string', 'options='=>'int'], +'imap_close' => ['true', 'imap'=>'IMAP\Connection', 'flags='=>'int'], 'imap_create' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], 'imap_createmailbox' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], -'imap_delete' => ['bool', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], +'imap_delete' => ['true', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], 'imap_deletemailbox' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], 'imap_errors' => ['array|false'], -'imap_expunge' => ['bool', 'imap'=>'IMAP\Connection'], +'imap_expunge' => ['true', 'imap'=>'IMAP\Connection'], 'imap_fetch_overview' => ['array|false', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flags='=>'int'], 'imap_fetchbody' => ['string|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'section'=>'string', 'flags='=>'int'], 'imap_fetchheader' => ['string|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'flags='=>'int'], 'imap_fetchmime' => ['string|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'section'=>'string', 'flags='=>'int'], 'imap_fetchstructure' => ['stdClass|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'flags='=>'int'], 'imap_fetchtext' => ['string|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'flags='=>'int'], -'imap_gc' => ['bool', 'imap'=>'IMAP\Connection', 'flags'=>'int'], +'imap_gc' => ['true', 'imap'=>'IMAP\Connection', 'flags'=>'int'], 'imap_get_quota' => ['array|false', 'imap'=>'IMAP\Connection', 'quota_root'=>'string'], 'imap_get_quotaroot' => ['array|false', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], 'imap_getacl' => ['array|false', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], @@ -5636,14 +5629,14 @@ 'imap_search' => ['array|false', 'imap'=>'IMAP\Connection', 'criteria'=>'string', 'flags='=>'int', 'charset='=>'string'], 'imap_set_quota' => ['bool', 'imap'=>'IMAP\Connection', 'quota_root'=>'string', 'mailbox_size'=>'int'], 'imap_setacl' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string', 'user_id'=>'string', 'rights'=>'string'], -'imap_setflag_full' => ['bool', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flag'=>'string', 'options='=>'int'], +'imap_setflag_full' => ['true', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flag'=>'string', 'options='=>'int'], 'imap_sort' => ['array|false', 'imap'=>'IMAP\Connection', 'criteria'=>'int', 'reverse'=>'bool', 'flags='=>'int', 'search_criteria='=>'?string', 'charset='=>'?string'], 'imap_status' => ['stdClass|false', 'imap'=>'IMAP\Connection', 'mailbox'=>'string', 'flags'=>'int'], 'imap_subscribe' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], 'imap_thread' => ['array|false', 'imap'=>'IMAP\Connection', 'flags='=>'int'], 'imap_timeout' => ['int|bool', 'timeout_type'=>'int', 'timeout='=>'int'], 'imap_uid' => ['int|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int'], -'imap_undelete' => ['bool', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], +'imap_undelete' => ['true', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], 'imap_unsubscribe' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], 'imap_utf7_decode' => ['string|false', 'string'=>'string'], 'imap_utf7_encode' => ['string', 'string'=>'string'], @@ -5738,11 +5731,11 @@ 'IntlBreakIterator::next' => ['int', 'offset='=>'?int'], 'IntlBreakIterator::preceding' => ['int', 'offset'=>'int'], 'IntlBreakIterator::previous' => ['int'], -'IntlBreakIterator::setText' => ['?bool', 'text'=>'string'], +'IntlBreakIterator::setText' => ['bool', 'text'=>'string'], 'intlcal_add' => ['bool', 'calendar'=>'IntlCalendar', 'field'=>'int', 'value'=>'int'], 'intlcal_after' => ['bool', 'calendar'=>'IntlCalendar', 'other'=>'IntlCalendar'], 'intlcal_before' => ['bool', 'calendar'=>'IntlCalendar', 'other'=>'IntlCalendar'], -'intlcal_clear' => ['bool', 'calendar'=>'IntlCalendar', 'field='=>'?int'], +'intlcal_clear' => ['true', 'calendar'=>'IntlCalendar', 'field='=>'?int'], 'intlcal_create_instance' => ['?IntlCalendar', 'timezone='=>'mixed', 'locale='=>'?string'], 'intlcal_equals' => ['bool', 'calendar'=>'IntlCalendar', 'other'=>'IntlCalendar'], 'intlcal_field_difference' => ['int|false', 'calendar'=>'IntlCalendar', 'timestamp'=>'float', 'field'=>'int'], @@ -5775,8 +5768,8 @@ 'intlcal_roll' => ['bool', 'calendar'=>'IntlCalendar', 'field'=>'int', 'value'=>'mixed'], 'intlcal_set' => ['bool', 'calendar'=>'IntlCalendar', 'year'=>'int', 'month'=>'int'], 'intlcal_set\'1' => ['bool', 'calendar'=>'IntlCalendar', 'year'=>'int', 'month'=>'int', 'dayOfMonth='=>'int', 'hour='=>'int', 'minute='=>'int', 'second='=>'int'], -'intlcal_set_first_day_of_week' => ['bool', 'calendar'=>'IntlCalendar', 'dayOfWeek'=>'int'], -'intlcal_set_lenient' => ['bool', 'calendar'=>'IntlCalendar', 'lenient'=>'bool'], +'intlcal_set_first_day_of_week' => ['true', 'calendar'=>'IntlCalendar', 'dayOfWeek'=>'int'], +'intlcal_set_lenient' => ['true', 'calendar'=>'IntlCalendar', 'lenient'=>'bool'], 'intlcal_set_repeated_wall_time_option' => ['true', 'calendar'=>'IntlCalendar', 'option'=>'int'], 'intlcal_set_skipped_wall_time_option' => ['true', 'calendar'=>'IntlCalendar', 'option'=>'int'], 'intlcal_set_time' => ['bool', 'calendar'=>'IntlCalendar', 'timestamp'=>'float'], @@ -5838,7 +5831,7 @@ 'IntlChar::charType' => ['?int', 'codepoint'=>'int|string'], 'IntlChar::chr' => ['?string', 'codepoint'=>'int|string'], 'IntlChar::digit' => ['int|false|null', 'codepoint'=>'int|string', 'base='=>'int'], -'IntlChar::enumCharNames' => ['?bool', 'start'=>'string|int', 'end'=>'string|int', 'callback'=>'callable(int,int,int):void', 'type='=>'int'], +'IntlChar::enumCharNames' => ['bool', 'start'=>'string|int', 'end'=>'string|int', 'callback'=>'callable(int,int,int):void', 'type='=>'int'], 'IntlChar::enumCharTypes' => ['void', 'callback'=>'callable(int,int,int):void'], 'IntlChar::foldCase' => ['int|string|null', 'codepoint'=>'int|string', 'options='=>'int'], 'IntlChar::forDigit' => ['int', 'digit'=>'int', 'base='=>'int'], @@ -5888,7 +5881,6 @@ 'IntlChar::tolower' => ['int|string|null', 'codepoint'=>'int|string'], 'IntlChar::totitle' => ['int|string|null', 'codepoint'=>'int|string'], 'IntlChar::toupper' => ['int|string|null', 'codepoint'=>'int|string'], -'IntlCodePointBreakIterator::__construct' => ['void'], 'IntlCodePointBreakIterator::createCharacterInstance' => ['?IntlRuleBasedBreakIterator', 'locale='=>'?string'], 'IntlCodePointBreakIterator::createCodePointInstance' => ['IntlCodePointBreakIterator'], 'IntlCodePointBreakIterator::createLineInstance' => ['?IntlRuleBasedBreakIterator', 'locale='=>'?string'], @@ -5909,7 +5901,7 @@ 'IntlCodePointBreakIterator::next' => ['int', 'offset='=>'?int'], 'IntlCodePointBreakIterator::preceding' => ['int', 'offset'=>'int'], 'IntlCodePointBreakIterator::previous' => ['int'], -'IntlCodePointBreakIterator::setText' => ['?bool', 'text'=>'string'], +'IntlCodePointBreakIterator::setText' => ['bool', 'text'=>'string'], 'IntlDateFormatter::__construct' => ['void', 'locale'=>'?string', 'dateType='=>'int', 'timeType='=>'int', 'timezone='=>'IntlTimeZone|DateTimeZone|string|null', 'calendar='=>'IntlCalendar|int|null', 'pattern='=>'?string'], 'IntlDateFormatter::create' => ['?IntlDateFormatter', 'locale'=>'?string', 'dateType='=>'int', 'timeType='=>'int', 'timezone='=>'IntlTimeZone|DateTimeZone|string|null', 'calendar='=>'IntlCalendar|int|null', 'pattern='=>'?string'], 'IntlDateFormatter::format' => ['string|false', 'datetime'=>'IntlCalendar|DateTimeInterface|array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int}|array{tm_sec: int, tm_min: int, tm_hour: int, tm_mday: int, tm_mon: int, tm_year: int, tm_wday: int, tm_yday: int, tm_isdst: int}|string|int|float'], @@ -5930,8 +5922,7 @@ 'IntlDateFormatter::setCalendar' => ['bool', 'calendar'=>'IntlCalendar|int|null'], 'IntlDateFormatter::setLenient' => ['void', 'lenient'=>'bool'], 'IntlDateFormatter::setPattern' => ['bool', 'pattern'=>'string'], -'IntlDateFormatter::setTimeZone' => ['null|false', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], -'IntlException::__clone' => ['void'], +'IntlDateFormatter::setTimeZone' => ['bool', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], 'IntlException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'IntlException::__toString' => ['string'], 'IntlException::__wakeup' => ['void'], @@ -6027,7 +6018,7 @@ 'IntlRuleBasedBreakIterator::next' => ['int', 'offset='=>'?int'], 'IntlRuleBasedBreakIterator::preceding' => ['int', 'offset'=>'int'], 'IntlRuleBasedBreakIterator::previous' => ['int'], -'IntlRuleBasedBreakIterator::setText' => ['?bool', 'text'=>'string'], +'IntlRuleBasedBreakIterator::setText' => ['bool', 'text'=>'string'], 'IntlTimeZone::countEquivalentIDs' => ['int|false', 'timezoneId'=>'string'], 'IntlTimeZone::createDefault' => ['IntlTimeZone'], 'IntlTimeZone::createEnumeration' => ['IntlIterator|false', 'countryOrRawOffset='=>'IntlTimeZone|string|int|float|null'], @@ -6072,7 +6063,6 @@ 'intltz_use_daylight_time' => ['bool', 'timezone'=>'IntlTimeZone'], 'intlz_create_default' => ['IntlTimeZone'], 'intval' => ['int', 'value'=>'mixed', 'base='=>'int'], -'InvalidArgumentException::__clone' => ['void'], 'InvalidArgumentException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'InvalidArgumentException::__toString' => ['string'], 'InvalidArgumentException::getCode' => ['int'], @@ -6158,7 +6148,6 @@ 'json_last_error' => ['int'], 'json_last_error_msg' => ['string'], 'json_validate' => ['bool', 'json'=>'string', 'depth='=>'positive-int', 'flags='=>'int'], -'JsonException::__clone' => ['void'], 'JsonException::__construct' => ['void', "message="=>"string", 'code='=>'int', 'previous='=>'?Throwable'], 'JsonException::__toString' => ['string'], 'JsonException::__wakeup' => ['void'], @@ -6385,7 +6374,6 @@ 'legendObj::free' => ['void'], 'legendObj::set' => ['int', 'property_name'=>'string', 'new_value'=>''], 'legendObj::updateFromString' => ['int', 'snippet'=>'string'], -'LengthException::__clone' => ['void'], 'LengthException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'LengthException::__toString' => ['string'], 'LengthException::getCode' => ['int'], @@ -6498,7 +6486,6 @@ 'log' => ['float', 'num'=>'float', 'base='=>'float'], 'log10' => ['float', 'num'=>'float'], 'log1p' => ['float', 'num'=>'float'], -'LogicException::__clone' => ['void'], 'LogicException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'LogicException::__toString' => ['string'], 'LogicException::getCode' => ['int'], @@ -8539,7 +8526,6 @@ 'OuterIterator::next' => ['void'], 'OuterIterator::rewind' => ['void'], 'OuterIterator::valid' => ['bool'], -'OutOfBoundsException::__clone' => ['void'], 'OutOfBoundsException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'OutOfBoundsException::__toString' => ['string'], 'OutOfBoundsException::getCode' => ['int'], @@ -8549,7 +8535,6 @@ 'OutOfBoundsException::getPrevious' => ['?Throwable'], 'OutOfBoundsException::getTrace' => ['list\',args?:array}>'], 'OutOfBoundsException::getTraceAsString' => ['string'], -'OutOfRangeException::__clone' => ['void'], 'OutOfRangeException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'OutOfRangeException::__toString' => ['string'], 'OutOfRangeException::getCode' => ['int'], @@ -8576,7 +8561,6 @@ 'outputformatObj::set' => ['int', 'property_name'=>'string', 'new_value'=>''], 'outputformatObj::setOption' => ['void', 'property_name'=>'string', 'new_value'=>'string'], 'outputformatObj::validate' => ['int'], -'OverflowException::__clone' => ['void'], 'OverflowException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'OverflowException::__toString' => ['string'], 'OverflowException::getCode' => ['int'], @@ -8668,7 +8652,6 @@ 'parse_ini_string' => ['array|false', 'ini_string'=>'string', 'process_sections='=>'bool', 'scanner_mode='=>'int'], 'parse_str' => ['void', 'string'=>'string', '&w_result'=>'array'], 'parse_url' => ['int|string|array|null|false', 'url'=>'string', 'component='=>'int'], -'ParseError::__clone' => ['void'], 'ParseError::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'ParseError::__toString' => ['string'], 'ParseError::getCode' => ['int'], @@ -9661,7 +9644,6 @@ 'random_bytes' => ['non-empty-string', 'length'=>'positive-int'], 'random_int' => ['int', 'min'=>'int', 'max'=>'int'], 'range' => ['non-empty-array', 'start'=>'string|int|float', 'end'=>'string|int|float', 'step='=>'int<1, max>|float'], -'RangeException::__clone' => ['void'], 'RangeException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'RangeException::__toString' => ['string'], 'RangeException::getCode' => ['int'], @@ -10661,12 +10643,10 @@ 'ReflectionMethod::isVariadic' => ['bool'], 'ReflectionMethod::returnsReference' => ['bool'], 'ReflectionMethod::setAccessible' => ['void', 'accessible'=>'bool'], -'ReflectionNamedType::__clone' => ['void'], 'ReflectionNamedType::__toString' => ['string'], 'ReflectionNamedType::allowsNull' => ['bool'], 'ReflectionNamedType::getName' => ['string'], 'ReflectionNamedType::isBuiltin' => ['bool'], -'ReflectionObject::__clone' => ['void'], 'ReflectionObject::__construct' => ['void', 'object'=>'object'], 'ReflectionObject::__toString' => ['string'], 'ReflectionObject::getConstant' => ['mixed', 'name'=>'string'], @@ -10898,7 +10878,6 @@ 'Runkit_Sandbox_Parent::__construct' => ['void'], 'runkit_superglobals' => ['array'], 'runkit_zval_inspect' => ['array', 'value'=>'mixed'], -'RuntimeException::__clone' => ['void'], 'RuntimeException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'RuntimeException::__toString' => ['string'], 'RuntimeException::getCode' => ['int'], @@ -14010,7 +13989,6 @@ 'transliterator_transliterate' => ['string|false', 'transliterator'=>'Transliterator|string', 'string'=>'string', 'start='=>'int', 'end='=>'int'], 'trigger_error' => ['bool', 'message'=>'string', 'error_level='=>'256|512|1024|16384'], 'trim' => ['string', 'string'=>'string', 'characters='=>'string'], -'TypeError::__clone' => ['void'], 'TypeError::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'TypeError::__toString' => ['string'], 'TypeError::getCode' => ['int'], @@ -14233,7 +14211,6 @@ 'ui\window::setTitle' => ['', 'title'=>'string'], 'uksort' => ['true', '&rw_array'=>'array', 'callback'=>'callable(mixed,mixed):int'], 'umask' => ['int', 'mask='=>'?int'], -'UnderflowException::__clone' => ['void'], 'UnderflowException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'UnderflowException::__toString' => ['string'], 'UnderflowException::getCode' => ['int'], @@ -14243,7 +14220,6 @@ 'UnderflowException::getPrevious' => ['?Throwable'], 'UnderflowException::getTrace' => ['list\',args?:array}>'], 'UnderflowException::getTraceAsString' => ['string'], -'UnexpectedValueException::__clone' => ['void'], 'UnexpectedValueException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable'], 'UnexpectedValueException::__toString' => ['string'], 'UnexpectedValueException::getCode' => ['int'], diff --git a/dictionaries/CallMap_83_delta.php b/dictionaries/CallMap_83_delta.php index eee4d2d2d3b..c5706151853 100644 --- a/dictionaries/CallMap_83_delta.php +++ b/dictionaries/CallMap_83_delta.php @@ -53,8 +53,96 @@ 'old' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool'], 'new' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool', 'options=' => 'array'], ], + 'imap_setflag_full' => [ + 'old' => ['bool', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flag'=>'string', 'options='=>'int'], + 'new' => ['true', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flag'=>'string', 'options='=>'int'], + ], + 'imap_expunge' => [ + 'old' => ['bool', 'imap'=>'IMAP\Connection'], + 'new' => ['true', 'imap'=>'IMAP\Connection'], + ], + 'imap_gc' => [ + 'old' => ['bool', 'imap'=>'IMAP\Connection', 'flags'=>'int'], + 'new' => ['true', 'imap'=>'IMAP\Connection', 'flags'=>'int'], + ], + 'imap_undelete' => [ + 'old' => ['bool', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], + 'new' => ['true', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], + ], + 'imap_delete' => [ + 'old' => ['bool', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], + 'new' => ['true', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], + ], + 'imap_clearflag_full' => [ + 'old' => ['bool', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flag'=>'string', 'options='=>'int'], + 'new' => ['true', 'imap'=>'IMAP\Connection', 'sequence'=>'string', 'flag'=>'string', 'options='=>'int'], + ], + 'imap_close' => [ + 'old' => ['bool', 'imap'=>'IMAP\Connection', 'flags='=>'int'], + 'new' => ['true', 'imap'=>'IMAP\Connection', 'flags='=>'int'], + ], + 'intlcal_clear' => [ + 'old' => ['bool', 'calendar'=>'IntlCalendar', 'field='=>'?int'], + 'new' => ['true', 'calendar'=>'IntlCalendar', 'field='=>'?int'], + ], + 'intlcal_set_lenient' => [ + 'old' => ['bool', 'calendar'=>'IntlCalendar', 'lenient'=>'bool'], + 'new' => ['true', 'calendar'=>'IntlCalendar', 'lenient'=>'bool'], + ], + 'intlcal_set_first_day_of_week' => [ + 'old' => ['bool', 'calendar'=>'IntlCalendar', 'dayOfWeek'=>'int'], + 'new' => ['true', 'calendar'=>'IntlCalendar', 'dayOfWeek'=>'int'], + ], + 'datefmt_set_timezone' => [ + 'old' => ['false|null', 'formatter'=>'IntlDateFormatter', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], + 'new' => ['bool', 'formatter'=>'IntlDateFormatter', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], + ], + 'intlrulebasedbreakiterator::settext' => [ + 'old' => ['?bool', 'text'=>'string'], + 'new' => ['bool', 'text'=>'string'], + ], + 'intlcodepointbreakiterator::settext' => [ + 'old' => ['?bool', 'text'=>'string'], + 'new' => ['bool', 'text'=>'string'], + ], + 'intldateformatter::settimezone' => [ + 'old' => ['null|false', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], + 'new' => ['bool', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], + ], + 'intlchar::enumcharnames' => [ + 'old' => ['?bool', 'start'=>'string|int', 'end'=>'string|int', 'callback'=>'callable(int,int,int):void', 'type='=>'int'], + 'new' => ['bool', 'start'=>'string|int', 'end'=>'string|int', 'callback'=>'callable(int,int,int):void', 'type='=>'int'], + ], + 'intlbreakiterator::settext' => [ + 'old' => ['?bool', 'text'=>'string'], + 'new' => ['bool', 'text'=>'string'], + ], ], 'removed' => [ + 'OutOfBoundsException::__clone' => ['void'], + 'ArgumentCountError::__clone' => ['void'], + 'ArithmeticError::__clone' => ['void'], + 'BadFunctionCallException::__clone' => ['void'], + 'BadMethodCallException::__clone' => ['void'], + 'ClosedGeneratorException::__clone' => ['void'], + 'DomainException::__clone' => ['void'], + 'ErrorException::__clone' => ['void'], + 'IntlException::__clone' => ['void'], + 'InvalidArgumentException::__clone' => ['void'], + 'JsonException::__clone' => ['void'], + 'LengthException::__clone' => ['void'], + 'LogicException::__clone' => ['void'], + 'OutOfRangeException::__clone' => ['void'], + 'OverflowException::__clone' => ['void'], + 'ParseError::__clone' => ['void'], + 'RangeException::__clone' => ['void'], + 'ReflectionNamedType::__clone' => ['void'], + 'ReflectionObject::__clone' => ['void'], + 'RuntimeException::__clone' => ['void'], + 'TypeError::__clone' => ['void'], + 'UnderflowException::__clone' => ['void'], + 'UnexpectedValueException::__clone' => ['void'], + 'IntlCodePointBreakIterator::__construct' => ['void'], ], ]; diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index 03c60132459..e3e99991c13 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -73,28 +73,15 @@ class InternalCallMapHandlerTest extends TestCase * @var array> */ private static array $ignoredFunctions = [ - 'argumentcounterror::__clone' => ['8.3'], - 'arithmeticerror::__clone' => ['8.3'], 'array_multisort', - 'badfunctioncallexception::__clone' => ['8.3'], - 'badmethodcallexception::__clone' => ['8.3'], - 'closedgeneratorexception::__clone' => ['8.3'], 'datefmt_create' => ['8.0'], - 'domainexception::__clone' => ['8.3'], - 'errorexception::__clone' => ['8.3'], 'fiber::start', 'imagefilledpolygon', 'imagegd', 'imagegd2', 'imageopenpolygon', 'imagepolygon', - 'intlcodepointbreakiterator::__construct' => ['8.3'], - 'intlexception::__clone' => ['8.3'], 'intlgregoriancalendar::__construct', - 'invalidargumentexception::__clone' => ['8.3'], - 'jsonexception::__clone' => ['8.3'], - 'lengthexception::__clone' => ['8.3'], - 'logicexception::__clone' => ['8.3'], 'lzf_compress', 'lzf_decompress', 'mailparse_msg_extract_part', @@ -153,15 +140,7 @@ class InternalCallMapHandlerTest extends TestCase 'oci_result', 'ocigetbufferinglob', 'ocisetbufferinglob', - 'outofboundsexception::__clone' => ['8.3'], - 'outofrangeexception::__clone' => ['8.3'], - 'overflowexception::__clone' => ['8.3'], - 'parseerror::__clone' => ['8.3'], - 'rangeexception::__clone' => ['8.3'], 'recursiveiteratoriterator::__construct', // Class used in CallMap does not exist: recursiveiterator - 'reflectionnamedtype::__clone' => ['8.3'], - 'reflectionobject::__clone' => ['8.3'], - 'runtimeexception::__clone' => ['8.3'], 'sqlsrv_fetch_array', 'sqlsrv_fetch_object', 'sqlsrv_get_field', @@ -169,9 +148,6 @@ class InternalCallMapHandlerTest extends TestCase 'sqlsrv_query', 'sqlsrv_server_info', 'ssh2_forward_accept', - 'typeerror::__clone' => ['8.3'], - 'underflowexception::__clone' => ['8.3'], - 'unexpectedvalueexception::__clone' => ['8.3'], 'xdiff_file_bdiff', 'xdiff_file_bdiff_size', 'xdiff_file_diff', @@ -251,23 +227,6 @@ class InternalCallMapHandlerTest extends TestCase 'splfileobject::fscanf' => ['8.1', '8.2', '8.3'], 'spltempfileobject::fscanf' => ['8.1', '8.2', '8.3'], 'xsltprocessor::transformtoxml' => ['8.1', '8.2', '8.3'], - 'intlcal_clear' => ['8.3'], - 'intlrulebasedbreakiterator::settext' => ['8.3'], - 'intlcodepointbreakiterator::__construct' => ['8.3'], - 'intlcodepointbreakiterator::settext' => ['8.3'], - 'intldateformatter::settimezone' => ['8.3'], - 'intlchar::enumcharnames' => ['8.3'], - 'intlbreakiterator::settext' => ['8.3'], - 'intlcal_set_lenient' => ['8.3'], - 'intlcal_set_first_day_of_week' => ['8.3'], - 'datefmt_set_timezone' => ['8.3'], - 'imap_setflag_full' => ['8.3'], - 'imap_expunge' => ['8.3'], - 'imap_gc' => ['8.3'], - 'imap_undelete' => ['8.3'], - 'imap_delete' => ['8.3'], - 'imap_clearflag_full' => ['8.3'], - 'imap_close' => ['8.3'], ]; /** From fda483a98b2621755bd23d6d3d09b6f329918ddb Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Fri, 21 Jul 2023 22:42:27 +0200 Subject: [PATCH 23/56] #10026 converted version_compare() to direct comparison with PHP_VERSION_ID --- tests/DateTimeTest.php | 2 +- tests/Traits/ValidCodeAnalysisTestTrait.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/DateTimeTest.php b/tests/DateTimeTest.php index b6fbe2ef2e8..97ad20e6f9c 100644 --- a/tests/DateTimeTest.php +++ b/tests/DateTimeTest.php @@ -18,7 +18,7 @@ public function testModifyWithInvalidConstant(): void { $context = new Context(); - if (version_compare(PHP_VERSION, '8.3', '>')) { + if (PHP_VERSION_ID >= 8_03_00) { $this->expectException(Exception::class); $this->expectExceptionMessage('DateTime::modify(): Failed to parse time string (foo) at position 0 (f)'); } diff --git a/tests/Traits/ValidCodeAnalysisTestTrait.php b/tests/Traits/ValidCodeAnalysisTestTrait.php index 6f8b2f18aea..23a44a3d0c3 100644 --- a/tests/Traits/ValidCodeAnalysisTestTrait.php +++ b/tests/Traits/ValidCodeAnalysisTestTrait.php @@ -77,7 +77,7 @@ public function testValidCode( $codebase->config->visitPreloadedStubFiles($codebase); // avoid MethodSignatureMismatch for __unserialize/() when extending DateTime - if (version_compare(PHP_VERSION, '8.2', '>')) { + if (PHP_VERSION_ID >= 8_02_00) { $this->addStubFile( 'stubOne.phpstub', ' Date: Fri, 21 Jul 2023 22:46:46 +0200 Subject: [PATCH 24/56] #10026 keep running phpcs and php lint with 7.4 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ad2fd3b7ba..cf9fbf7b147 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.0' + php-version: '7.4' tools: composer:v2 coverage: none env: @@ -57,7 +57,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.0' + php-version: '7.4' tools: composer:v2 coverage: none env: From 1e1ffe164fabd1ac75f731845ae07d5f4e4f3e36 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Fri, 21 Jul 2023 22:50:24 +0200 Subject: [PATCH 25/56] #10026 keep running phpcs and php lint with 7.4 --- .circleci/config.yml | 4 ++-- .github/workflows/build-phar.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9ac95561065..0c4d5026289 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,7 +13,7 @@ executors: - image: thecodingmachine/php:8.1-v4-cli jobs: "Code Style Analysis": - executor: php-80 + executor: php-74 steps: - checkout @@ -41,7 +41,7 @@ jobs: command: vendor/bin/phpcs -d memory_limit=512M phar-build: - executor: php-80 + executor: php-74 steps: - attach_workspace: at: /home/docker/project/ diff --git a/.github/workflows/build-phar.yml b/.github/workflows/build-phar.yml index b042b090497..8d75dfe63c3 100644 --- a/.github/workflows/build-phar.yml +++ b/.github/workflows/build-phar.yml @@ -38,7 +38,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.0' + php-version: '7.4' tools: composer:v2 coverage: none env: From 603dfa2e5a819ef1adf98afdccf785f6a133829e Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Fri, 21 Jul 2023 22:57:49 +0200 Subject: [PATCH 26/56] #10026 cleanup phpcs --- tests/DateTimeTest.php | 6 ++---- tests/Traits/ValidCodeAnalysisTestTrait.php | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/DateTimeTest.php b/tests/DateTimeTest.php index 97ad20e6f9c..d8c60882079 100644 --- a/tests/DateTimeTest.php +++ b/tests/DateTimeTest.php @@ -6,9 +6,7 @@ use Psalm\Context; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; -use function version_compare; - -use const PHP_VERSION; +use const PHP_VERSION_ID; class DateTimeTest extends TestCase { @@ -51,7 +49,7 @@ public function testModifyWithBothConstant(): void { $context = new Context(); - if (version_compare(PHP_VERSION, '8.3', '>')) { + if (PHP_VERSION_ID >= 8_03_00) { $this->expectException(Exception::class); $this->expectExceptionMessage('DateTime::modify(): Failed to parse time string (bar) at position 0 (b)'); } diff --git a/tests/Traits/ValidCodeAnalysisTestTrait.php b/tests/Traits/ValidCodeAnalysisTestTrait.php index 23a44a3d0c3..b37acced357 100644 --- a/tests/Traits/ValidCodeAnalysisTestTrait.php +++ b/tests/Traits/ValidCodeAnalysisTestTrait.php @@ -14,6 +14,7 @@ use const PHP_OS; use const PHP_VERSION; +use const PHP_VERSION_ID; trait ValidCodeAnalysisTestTrait { From 637dcc4425c809793854dd28fc9b45a0bb872d37 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Fri, 21 Jul 2023 23:11:20 +0200 Subject: [PATCH 27/56] #10026 fixed case mismatches in CallMap_83_delta --- dictionaries/CallMap_83_delta.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dictionaries/CallMap_83_delta.php b/dictionaries/CallMap_83_delta.php index c5706151853..9bcf76deece 100644 --- a/dictionaries/CallMap_83_delta.php +++ b/dictionaries/CallMap_83_delta.php @@ -97,23 +97,23 @@ 'old' => ['false|null', 'formatter'=>'IntlDateFormatter', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], 'new' => ['bool', 'formatter'=>'IntlDateFormatter', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], ], - 'intlrulebasedbreakiterator::settext' => [ + 'IntlRuleBasedBreakIterator::setText' => [ 'old' => ['?bool', 'text'=>'string'], 'new' => ['bool', 'text'=>'string'], ], - 'intlcodepointbreakiterator::settext' => [ + 'IntlCodePointBreakIterator::setText' => [ 'old' => ['?bool', 'text'=>'string'], 'new' => ['bool', 'text'=>'string'], ], - 'intldateformatter::settimezone' => [ + 'IntlDateFormatter::setTimeZone' => [ 'old' => ['null|false', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], 'new' => ['bool', 'timezone'=>'IntlTimeZone|DateTimeZone|string|null'], ], - 'intlchar::enumcharnames' => [ + 'IntlChar::enumCharNames' => [ 'old' => ['?bool', 'start'=>'string|int', 'end'=>'string|int', 'callback'=>'callable(int,int,int):void', 'type='=>'int'], 'new' => ['bool', 'start'=>'string|int', 'end'=>'string|int', 'callback'=>'callable(int,int,int):void', 'type='=>'int'], ], - 'intlbreakiterator::settext' => [ + 'IntlBreakIterator::setText' => [ 'old' => ['?bool', 'text'=>'string'], 'new' => ['bool', 'text'=>'string'], ], From eaf41a1874f6d2fc2e7ce58c1be02f8bfedfb441 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 23 Jul 2023 07:09:24 +0200 Subject: [PATCH 28/56] Fix incorrect positions set in code action edits Neovim requires positions to supply column as well as row. --- src/Psalm/Internal/LanguageServer/Server/TextDocument.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index b9c4bcca8f4..076b7cb26f5 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -435,8 +435,8 @@ public function codeAction(TextDocumentIdentifier $textDocument, Range $range, C //$contents = $this->codebase->file_provider->getContents($file_path); $snippetRange = new Range( - new Position($data['line_from']-1), - new Position($data['line_to']), + new Position($data['line_from'] - 1, 0), + new Position($data['line_to'], 0), ); $indentation = ''; From f64da372f3eed1094aacc5dbfca572a917633cf7 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 23 Jul 2023 23:09:46 +0200 Subject: [PATCH 29/56] Simplify conditions with deep object access --- src/Psalm/Internal/LanguageServer/LanguageClient.php | 2 +- src/Psalm/Internal/LanguageServer/LanguageServer.php | 12 ++---------- .../Internal/LanguageServer/Server/TextDocument.php | 8 ++------ 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageClient.php b/src/Psalm/Internal/LanguageServer/LanguageClient.php index 096177d9d5c..0aed33950a6 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageClient.php +++ b/src/Psalm/Internal/LanguageServer/LanguageClient.php @@ -64,7 +64,7 @@ public function __construct( public function refreshConfiguration(): void { $capabilities = $this->server->clientCapabilities; - if ($capabilities && $capabilities->workspace && $capabilities->workspace->configuration) { + if ($capabilities->workspace->configuration ?? false) { $this->workspace->requestConfiguration('psalm')->onResolve(function ($error, $value): void { if ($error) { $this->server->logError('There was an error getting configuration'); diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index c165b0cddac..54009b9cc5a 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -538,11 +538,7 @@ function () { * * @since LSP 3.16.0 */ - if ($this->clientCapabilities && - $this->clientCapabilities->textDocument && - $this->clientCapabilities->textDocument->publishDiagnostics && - $this->clientCapabilities->textDocument->publishDiagnostics->dataSupport - ) { + if ($this->clientCapabilities->textDocument->publishDiagnostics->dataSupport ?? false) { $serverCapabilities->codeActionProvider = true; } @@ -757,11 +753,7 @@ function (IssueData $issue_data): Diagnostic { * * @since LSP 3.16.0 */ - if ($this->clientCapabilities !== null && - $this->clientCapabilities->textDocument && - $this->clientCapabilities->textDocument->publishDiagnostics && - $this->clientCapabilities->textDocument->publishDiagnostics->codeDescriptionSupport - ) { + if ($this->clientCapabilities->textDocument->publishDiagnostics->codeDescriptionSupport ?? false) { $diagnostic->codeDescription = new CodeDescription($issue_data->link); } diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index 076b7cb26f5..1ea170a86f0 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -301,12 +301,8 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit [$recent_type, $gap, $offset] = $completion_data; if ($gap === '->' || $gap === '::') { - $snippetSupport = ($this->server->clientCapabilities && - $this->server->clientCapabilities->textDocument && - $this->server->clientCapabilities->textDocument->completion && - $this->server->clientCapabilities->textDocument->completion->completionItem && - $this->server->clientCapabilities->textDocument->completion->completionItem->snippetSupport) - ? true : false; + $snippetSupport = $this->server->clientCapabilities + ->textDocument->completion->completionItem->snippetSupport ?? false; $completion_items = $this->codebase->getCompletionItemsForClassishThing($recent_type, $gap, $snippetSupport); } elseif ($gap === '[') { From acdb11e6792fea8a892a5ad7dcb24484df7f8296 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Fri, 21 Jul 2023 02:06:08 +0200 Subject: [PATCH 30/56] Use instance methods instead of static --- .../LanguageServer/LanguageServer.php | 4 ++-- .../LanguageServer/Server/TextDocument.php | 22 +++++++++---------- .../LanguageServer/Server/Workspace.php | 6 ++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 54009b9cc5a..07990378bd2 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -951,7 +951,7 @@ private function clientStatus(string $status, ?string $additional_info = null): * * @psalm-pure */ - public static function pathToUri(string $filepath): string + public function pathToUri(string $filepath): string { $filepath = trim(str_replace('\\', '/', $filepath), '/'); $parts = explode('/', $filepath); @@ -970,7 +970,7 @@ public static function pathToUri(string $filepath): string /** * Transforms URI into file path */ - public static function uriToPath(string $uri): string + public function uriToPath(string $uri): string { $fragments = parse_url($uri); if ($fragments === false diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index 1ea170a86f0..b24862e7460 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -74,7 +74,7 @@ public function didOpen(TextDocumentItem $textDocument): void ['version' => $textDocument->version, 'uri' => $textDocument->uri], ); - $file_path = LanguageServer::uriToPath($textDocument->uri); + $file_path = $this->server->uriToPath($textDocument->uri); $this->codebase->removeTemporaryFileChanges($file_path); $this->codebase->file_provider->openFile($file_path); @@ -97,7 +97,7 @@ public function didSave(TextDocumentIdentifier $textDocument, ?string $text = nu ['uri' => (array) $textDocument], ); - $file_path = LanguageServer::uriToPath($textDocument->uri); + $file_path = $this->server->uriToPath($textDocument->uri); // reopen file $this->codebase->removeTemporaryFileChanges($file_path); @@ -119,7 +119,7 @@ public function didChange(VersionedTextDocumentIdentifier $textDocument, array $ ['version' => $textDocument->version, 'uri' => $textDocument->uri], ); - $file_path = LanguageServer::uriToPath($textDocument->uri); + $file_path = $this->server->uriToPath($textDocument->uri); if (count($contentChanges) === 1 && isset($contentChanges[0]) && $contentChanges[0]->range === null) { $new_content = $contentChanges[0]->text; @@ -154,7 +154,7 @@ public function didClose(TextDocumentIdentifier $textDocument): void ['uri' => $textDocument->uri], ); - $file_path = LanguageServer::uriToPath($textDocument->uri); + $file_path = $this->server->uriToPath($textDocument->uri); $this->codebase->file_provider->closeFile($file_path); $this->server->client->textDocument->publishDiagnostics($textDocument->uri, []); @@ -178,7 +178,7 @@ public function definition(TextDocumentIdentifier $textDocument, Position $posit 'textDocument/definition', ); - $file_path = LanguageServer::uriToPath($textDocument->uri); + $file_path = $this->server->uriToPath($textDocument->uri); //This currently doesnt work right with out of project files if (!$this->codebase->config->isInProjectDirs($file_path)) { @@ -205,7 +205,7 @@ public function definition(TextDocumentIdentifier $textDocument, Position $posit return new Success( new Location( - LanguageServer::pathToUri($code_location->file_path), + $this->server->pathToUri($code_location->file_path), new Range( new Position($code_location->getLineNumber() - 1, $code_location->getColumn() - 1), new Position($code_location->getEndLineNumber() - 1, $code_location->getEndColumn() - 1), @@ -232,7 +232,7 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): 'textDocument/hover', ); - $file_path = LanguageServer::uriToPath($textDocument->uri); + $file_path = $this->server->uriToPath($textDocument->uri); //This currently doesnt work right with out of project files if (!$this->codebase->config->isInProjectDirs($file_path)) { @@ -288,7 +288,7 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit 'textDocument/completion', ); - $file_path = LanguageServer::uriToPath($textDocument->uri); + $file_path = $this->server->uriToPath($textDocument->uri); //This currently doesnt work right with out of project files if (!$this->codebase->config->isInProjectDirs($file_path)) { @@ -356,7 +356,7 @@ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $po 'textDocument/signatureHelp', ); - $file_path = LanguageServer::uriToPath($textDocument->uri); + $file_path = $this->server->uriToPath($textDocument->uri); //This currently doesnt work right with out of project files if (!$this->codebase->config->isInProjectDirs($file_path)) { @@ -411,7 +411,7 @@ public function codeAction(TextDocumentIdentifier $textDocument, Range $range, C 'textDocument/codeAction', ); - $file_path = LanguageServer::uriToPath($textDocument->uri); + $file_path = $this->server->uriToPath($textDocument->uri); //Don't report code actions for files we arent watching if (!$this->codebase->config->isInProjectDirs($file_path)) { @@ -427,7 +427,7 @@ public function codeAction(TextDocumentIdentifier $textDocument, Range $range, C /** @var array{type: string, snippet: string, line_from: int, line_to: int} */ $data = (array)$diagnostic->data; - //$file_path = LanguageServer::uriToPath($textDocument->uri); + //$file_path = $this->server->uriToPath($textDocument->uri); //$contents = $this->codebase->file_provider->getContents($file_path); $snippetRange = new Range( diff --git a/src/Psalm/Internal/LanguageServer/Server/Workspace.php b/src/Psalm/Internal/LanguageServer/Server/Workspace.php index af49619c356..113a8f17974 100644 --- a/src/Psalm/Internal/LanguageServer/Server/Workspace.php +++ b/src/Psalm/Internal/LanguageServer/Server/Workspace.php @@ -63,7 +63,7 @@ public function didChangeWatchedFiles(array $changes): void $realFiles = array_filter( array_map(function (FileEvent $change) { try { - return LanguageServer::uriToPath($change->uri); + return $this->server->uriToPath($change->uri); } catch (InvalidArgumentException $e) { return null; } @@ -79,7 +79,7 @@ public function didChangeWatchedFiles(array $changes): void } foreach ($changes as $change) { - $file_path = LanguageServer::uriToPath($change->uri); + $file_path = $this->server->uriToPath($change->uri); if ($composerLockFile === $file_path) { continue; @@ -140,7 +140,7 @@ public function executeCommand(string $command, $arguments): Promise case 'psalm.analyze.uri': /** @var array{uri: string} */ $arguments = (array) $arguments; - $file = LanguageServer::uriToPath($arguments['uri']); + $file = $this->server->uriToPath($arguments['uri']); $this->codebase->reloadFiles( $this->project_analyzer, [$file], From c44b9f5c5e5739f1a59a3b7f4334645f9c1fd66c Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Fri, 21 Jul 2023 18:22:29 +0200 Subject: [PATCH 31/56] Map LSP paths --- .../LanguageServer/LanguageServer.php | 57 ++++++++++++++----- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 07990378bd2..c23e0ef4d6b 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -142,6 +142,8 @@ class LanguageServer extends Dispatcher */ protected JsonMapper $mapper; + protected ?string $clientRootPath = null; + public function __construct( ProtocolReader $reader, ProtocolWriter $writer, @@ -394,6 +396,11 @@ public function initialize( $this->clientInfo = $clientInfo; $this->clientCapabilities = $capabilities; $this->trace = $trace; + + if ($rootUri !== null) { + $this->clientRootPath = $this->getPathPart($rootUri); + } + return call( /** @return Generator */ function () { @@ -948,12 +955,22 @@ private function clientStatus(string $status, ?string $additional_info = null): /** * Transforms an absolute file path into a URI as used by the language server protocol. - * - * @psalm-pure */ public function pathToUri(string $filepath): string { - $filepath = trim(str_replace('\\', '/', $filepath), '/'); + $filepath = str_replace('\\', '/', $filepath); + + if ($this->clientRootPath !== null) { + $oldpath = $filepath; + $filepath = str_replace( + rtrim($this->codebase->config->base_dir, '/') . '/', + rtrim($this->clientRootPath, '/') . '/', + $filepath + ); + $this->logDebug('Translated path to URI', ['from' => $oldpath, 'to' => $filepath]); + } + + $filepath = trim($filepath, '/'); $parts = explode('/', $filepath); // Don't %-encode the colon after a Windows drive letter $first = array_shift($parts); @@ -972,16 +989,7 @@ public function pathToUri(string $filepath): string */ public function uriToPath(string $uri): string { - $fragments = parse_url($uri); - if ($fragments === false - || !isset($fragments['scheme']) - || $fragments['scheme'] !== 'file' - || !isset($fragments['path']) - ) { - throw new InvalidArgumentException("Not a valid file URI: $uri"); - } - - $filepath = urldecode($fragments['path']); + $filepath = urldecode($this->getPathPart($uri)); if (strpos($filepath, ':') !== false) { if ($filepath[0] === '/') { @@ -990,6 +998,16 @@ public function uriToPath(string $uri): string $filepath = str_replace('/', '\\', $filepath); } + if ($this->clientRootPath !== null) { + $oldpath = $filepath; + $filepath = str_replace( + rtrim($this->clientRootPath, '/') . '/', + rtrim($this->codebase->config->base_dir, '/') . '/', + $filepath + ); + $this->logDebug('Translated URI to path', ['from' => $oldpath, 'to' => $filepath]); + } + $realpath = realpath($filepath); if ($realpath !== false) { return $realpath; @@ -997,4 +1015,17 @@ public function uriToPath(string $uri): string return $filepath; } + + private function getPathPart(string $uri): string + { + $fragments = parse_url($uri); + if ($fragments === false + || !isset($fragments['scheme']) + || $fragments['scheme'] !== 'file' + || !isset($fragments['path']) + ) { + throw new InvalidArgumentException("Not a valid file URI: $uri"); + } + return $fragments['path']; + } } From f634a0047a631ffe59656457fad04f94db833f21 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Fri, 21 Jul 2023 18:31:47 +0200 Subject: [PATCH 32/56] CS fix --- src/Psalm/Internal/LanguageServer/LanguageServer.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index c23e0ef4d6b..5ea1ea8ef6b 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -80,6 +80,7 @@ use function parse_url; use function rawurlencode; use function realpath; +use function rtrim; use function str_replace; use function stream_set_blocking; use function stream_socket_accept; @@ -965,7 +966,7 @@ public function pathToUri(string $filepath): string $filepath = str_replace( rtrim($this->codebase->config->base_dir, '/') . '/', rtrim($this->clientRootPath, '/') . '/', - $filepath + $filepath, ); $this->logDebug('Translated path to URI', ['from' => $oldpath, 'to' => $filepath]); } @@ -1003,7 +1004,7 @@ public function uriToPath(string $uri): string $filepath = str_replace( rtrim($this->clientRootPath, '/') . '/', rtrim($this->codebase->config->base_dir, '/') . '/', - $filepath + $filepath, ); $this->logDebug('Translated URI to path', ['from' => $oldpath, 'to' => $filepath]); } From a68c4804f4b8ef14fcaa426712f8a95a59afb414 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 23 Jul 2023 00:04:25 +0200 Subject: [PATCH 33/56] Add path mapper --- .../Internal/LanguageServer/PathMapper.php | 57 ++++++++++++++ tests/LanguageServer/PathMapperTest.php | 75 +++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 src/Psalm/Internal/LanguageServer/PathMapper.php create mode 100644 tests/LanguageServer/PathMapperTest.php diff --git a/src/Psalm/Internal/LanguageServer/PathMapper.php b/src/Psalm/Internal/LanguageServer/PathMapper.php new file mode 100644 index 00000000000..8d9a09bdb89 --- /dev/null +++ b/src/Psalm/Internal/LanguageServer/PathMapper.php @@ -0,0 +1,57 @@ +serverRoot = $this->sanitizeFolderPath($serverRoot); + $this->clientRoot = $this->sanitizeFolderPath($clientRoot); + } + + public function configureClientRoot(string $clientRoot): void + { + // ignore if preconfigured + if ($this->clientRoot === null) { + $this->clientRoot = $this->sanitizeFolderPath($clientRoot); + } + } + + public function mapClientToServer(string $clientPath): string + { + if ($this->clientRoot === null) { + return $clientPath; + } + + if (substr($clientPath, 0, strlen($this->clientRoot)) === $this->clientRoot) { + return $this->serverRoot . substr($clientPath, strlen($this->clientRoot)); + } + + return $clientPath; + } + + public function mapServerToClient(string $serverPath): string + { + if ($this->clientRoot === null) { + return $serverPath; + } + if (substr($serverPath, 0, strlen($this->serverRoot)) === $this->serverRoot) { + return $this->clientRoot . substr($serverPath, strlen($this->serverRoot)); + } + return $serverPath; + } + + /** @return ($path is null ? null : string) */ + private function sanitizeFolderPath(?string $path): ?string + { + if ($path === null) { + return $path; + } + return rtrim($path, '/'); + } +} diff --git a/tests/LanguageServer/PathMapperTest.php b/tests/LanguageServer/PathMapperTest.php new file mode 100644 index 00000000000..c6a45a1b759 --- /dev/null +++ b/tests/LanguageServer/PathMapperTest.php @@ -0,0 +1,75 @@ +configureClientRoot('/home/user/src/project'); + $this->assertSame( + '/home/user/src/project/filename.php', + $mapper->mapServerToClient('/var/www/filename.php') + ); + } + + public function testIgnoresClientRootIfItWasPreconfigures(): void + { + $mapper = new PathMapper('/var/www', '/home/user/src/project'); + // this will be ignored + $mapper->configureClientRoot('/home/anotheruser/Projects/project'); + + $this->assertSame( + '/home/user/src/project/filename.php', + $mapper->mapServerToClient('/var/www/filename.php') + ); + } + + /** + * @dataProvider mappingProvider + */ + public function testMapsClientToServer( + string $serverRoot, + ?string $clientRootPreconfigured, + string $clientRootProvidedLater, + string $clientPath, + string $serverPath + ): void { + $mapper = new PathMapper($serverRoot, $clientRootPreconfigured); + $mapper->configureClientRoot($clientRootProvidedLater); + $this->assertSame( + $serverPath, + $mapper->mapClientToServer($clientPath) + ); + } + + /** @dataProvider mappingProvider */ + public function testMapsServerToClient( + string $serverRoot, + ?string $clientRootPreconfigured, + string $clientRootProvidedLater, + string $clientPath, + string $serverPath + ): void { + $mapper = new PathMapper($serverRoot, $clientRootPreconfigured); + $mapper->configureClientRoot($clientRootProvidedLater); + $this->assertSame( + $clientPath, + $mapper->mapServerToClient($serverPath) + ); + } + + /** @return iterable */ + public static function mappingProvider(): iterable + { + yield ["/var/a", null, "/user/project", "/user/project/filename.php", "/var/a/filename.php"]; + yield ["/var/a", "/user/project", "/whatever", "/user/project/filename.php", "/var/a/filename.php"]; + yield ["/var/a/", "/user/project", "/whatever", "/user/project/filename.php", "/var/a/filename.php"]; + yield ["/var/a", "/user/project/", "/whatever", "/user/project/filename.php", "/var/a/filename.php"]; + yield ["/var/a/", "/user/project/", "/whatever", "/user/project/filename.php", "/var/a/filename.php"]; + } +} From 389aa7965f12d8709f65f00dffe2c7b9cbd153a3 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 23 Jul 2023 00:56:06 +0200 Subject: [PATCH 34/56] Use PathMapper to map paths --- .../LanguageServer/LanguageServer.php | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 5ea1ea8ef6b..3ea4b50465f 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -145,6 +145,8 @@ class LanguageServer extends Dispatcher protected ?string $clientRootPath = null; + protected PathMapper $path_mapper; + public function __construct( ProtocolReader $reader, ProtocolWriter $writer, @@ -243,6 +245,8 @@ function (): void { $this->client = new LanguageClient($reader, $writer, $this, $clientConfiguration); + $this->path_mapper = new PathMapper($codebase->config->base_dir, null); + $this->logInfo("Psalm Language Server ".PSALM_VERSION." has started."); } @@ -398,8 +402,9 @@ public function initialize( $this->clientCapabilities = $capabilities; $this->trace = $trace; + if ($rootUri !== null) { - $this->clientRootPath = $this->getPathPart($rootUri); + $this->path_mapper->configureClientRoot($this->getPathPart($rootUri)); } return call( @@ -961,15 +966,8 @@ public function pathToUri(string $filepath): string { $filepath = str_replace('\\', '/', $filepath); - if ($this->clientRootPath !== null) { - $oldpath = $filepath; - $filepath = str_replace( - rtrim($this->codebase->config->base_dir, '/') . '/', - rtrim($this->clientRootPath, '/') . '/', - $filepath, - ); - $this->logDebug('Translated path to URI', ['from' => $oldpath, 'to' => $filepath]); - } + $filepath = $this->path_mapper->mapServerToClient($oldpath = $filepath); + $this->logDebug('Translated path to URI', ['from' => $oldpath, 'to' => $filepath]); $filepath = trim($filepath, '/'); $parts = explode('/', $filepath); @@ -999,15 +997,8 @@ public function uriToPath(string $uri): string $filepath = str_replace('/', '\\', $filepath); } - if ($this->clientRootPath !== null) { - $oldpath = $filepath; - $filepath = str_replace( - rtrim($this->clientRootPath, '/') . '/', - rtrim($this->codebase->config->base_dir, '/') . '/', - $filepath, - ); - $this->logDebug('Translated URI to path', ['from' => $oldpath, 'to' => $filepath]); - } + $filepath = $this->path_mapper->mapClientToServer($oldpath = $filepath); + $this->logDebug('Translated URI to path', ['from' => $oldpath, 'to' => $filepath]); $realpath = realpath($filepath); if ($realpath !== false) { From 6b9d9805b12f11bf02f0a0c32324c667bed9d1e2 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 23 Jul 2023 01:13:37 +0200 Subject: [PATCH 35/56] Account for `-r` option --- src/Psalm/Internal/Cli/LanguageServer.php | 4 +++- src/Psalm/Internal/LanguageServer/LanguageServer.php | 10 +++++++--- tests/LanguageServer/DiagnosticTest.php | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 429144b6808..cc52be7a7d4 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -254,6 +254,8 @@ static function (string $arg) use ($valid_long_options): void { $current_dir = $root_path . DIRECTORY_SEPARATOR; } + $server_start_dir = $current_dir; + $vendor_dir = CliUtils::getVendorDir($current_dir); $include_collector = new IncludeCollector(); @@ -394,6 +396,6 @@ static function (string $arg) use ($valid_long_options): void { $clientConfiguration->TCPServerAddress = $options['tcp'] ?? null; $clientConfiguration->TCPServerMode = isset($options['tcp-server']); - LanguageServerLanguageServer::run($config, $clientConfiguration, $current_dir, $inMemory); + LanguageServerLanguageServer::run($config, $clientConfiguration, $current_dir, $server_start_dir, $inMemory); } } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 3ea4b50465f..4ffe03af380 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -80,7 +80,6 @@ use function parse_url; use function rawurlencode; use function realpath; -use function rtrim; use function str_replace; use function stream_set_blocking; use function stream_socket_accept; @@ -153,7 +152,8 @@ public function __construct( ProjectAnalyzer $project_analyzer, Codebase $codebase, ClientConfiguration $clientConfiguration, - Progress $progress + Progress $progress, + string $server_start_dir ) { parent::__construct($this, '/'); @@ -245,7 +245,7 @@ function (): void { $this->client = new LanguageClient($reader, $writer, $this, $clientConfiguration); - $this->path_mapper = new PathMapper($codebase->config->base_dir, null); + $this->path_mapper = new PathMapper($server_start_dir, null); $this->logInfo("Psalm Language Server ".PSALM_VERSION." has started."); } @@ -257,6 +257,7 @@ public static function run( Config $config, ClientConfiguration $clientConfiguration, string $base_dir, + string $server_start_dir, bool $inMemory = false ): void { $progress = new Progress(); @@ -329,6 +330,7 @@ public static function run( $codebase, $clientConfiguration, $progress, + $server_start_dir, ); Loop::run(); } elseif ($clientConfiguration->TCPServerMode && $clientConfiguration->TCPServerAddress) { @@ -352,6 +354,7 @@ public static function run( $codebase, $clientConfiguration, $progress, + $server_start_dir, ); Loop::run(); } @@ -365,6 +368,7 @@ public static function run( $codebase, $clientConfiguration, $progress, + $server_start_dir, ); Loop::run(); } diff --git a/tests/LanguageServer/DiagnosticTest.php b/tests/LanguageServer/DiagnosticTest.php index 690c008ea95..ea480138c20 100644 --- a/tests/LanguageServer/DiagnosticTest.php +++ b/tests/LanguageServer/DiagnosticTest.php @@ -85,6 +85,7 @@ public function testSnippetSupportDisabled(): void $this->codebase, $clientConfiguration, new Progress, + getcwd(), ); $write->on('message', function (Message $message) use ($deferred, $server): void { From 8a51aaedd489953026f4cda4e3e4a3d42393e1eb Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 23 Jul 2023 01:19:39 +0200 Subject: [PATCH 36/56] CS fix --- .../Internal/LanguageServer/PathMapper.php | 40 +++++++++---------- tests/LanguageServer/PathMapperTest.php | 36 ++++++++--------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/PathMapper.php b/src/Psalm/Internal/LanguageServer/PathMapper.php index 8d9a09bdb89..88ff1661d2c 100644 --- a/src/Psalm/Internal/LanguageServer/PathMapper.php +++ b/src/Psalm/Internal/LanguageServer/PathMapper.php @@ -5,45 +5,45 @@ /** @internal */ final class PathMapper { - private string $serverRoot; - private ?string $clientRoot; + private string $server_root; + private ?string $client_root; - public function __construct(string $serverRoot, ?string $clientRoot = null) + public function __construct(string $server_root, ?string $client_root = null) { - $this->serverRoot = $this->sanitizeFolderPath($serverRoot); - $this->clientRoot = $this->sanitizeFolderPath($clientRoot); + $this->server_root = $this->sanitizeFolderPath($server_root); + $this->client_root = $this->sanitizeFolderPath($client_root); } - public function configureClientRoot(string $clientRoot): void + public function configureClientRoot(string $client_root): void { // ignore if preconfigured - if ($this->clientRoot === null) { - $this->clientRoot = $this->sanitizeFolderPath($clientRoot); + if ($this->client_root === null) { + $this->client_root = $this->sanitizeFolderPath($client_root); } } - public function mapClientToServer(string $clientPath): string + public function mapClientToServer(string $client_path): string { - if ($this->clientRoot === null) { - return $clientPath; + if ($this->client_root === null) { + return $client_path; } - if (substr($clientPath, 0, strlen($this->clientRoot)) === $this->clientRoot) { - return $this->serverRoot . substr($clientPath, strlen($this->clientRoot)); + if (substr($client_path, 0, strlen($this->client_root)) === $this->client_root) { + return $this->server_root . substr($client_path, strlen($this->client_root)); } - return $clientPath; + return $client_path; } - public function mapServerToClient(string $serverPath): string + public function mapServerToClient(string $server_path): string { - if ($this->clientRoot === null) { - return $serverPath; + if ($this->client_root === null) { + return $server_path; } - if (substr($serverPath, 0, strlen($this->serverRoot)) === $this->serverRoot) { - return $this->clientRoot . substr($serverPath, strlen($this->serverRoot)); + if (substr($server_path, 0, strlen($this->server_root)) === $this->server_root) { + return $this->client_root . substr($server_path, strlen($this->server_root)); } - return $serverPath; + return $server_path; } /** @return ($path is null ? null : string) */ diff --git a/tests/LanguageServer/PathMapperTest.php b/tests/LanguageServer/PathMapperTest.php index c6a45a1b759..ac6e1fd5c32 100644 --- a/tests/LanguageServer/PathMapperTest.php +++ b/tests/LanguageServer/PathMapperTest.php @@ -33,33 +33,33 @@ public function testIgnoresClientRootIfItWasPreconfigures(): void * @dataProvider mappingProvider */ public function testMapsClientToServer( - string $serverRoot, - ?string $clientRootPreconfigured, - string $clientRootProvidedLater, - string $clientPath, - string $serverPath + string $server_root, + ?string $client_root_reconfigured, + string $client_root_provided_later, + string $client_path, + string $server_ath ): void { - $mapper = new PathMapper($serverRoot, $clientRootPreconfigured); - $mapper->configureClientRoot($clientRootProvidedLater); + $mapper = new PathMapper($server_root, $client_root_reconfigured); + $mapper->configureClientRoot($client_root_provided_later); $this->assertSame( - $serverPath, - $mapper->mapClientToServer($clientPath) + $server_ath, + $mapper->mapClientToServer($client_path) ); } /** @dataProvider mappingProvider */ public function testMapsServerToClient( - string $serverRoot, - ?string $clientRootPreconfigured, - string $clientRootProvidedLater, - string $clientPath, - string $serverPath + string $server_root, + ?string $client_root_preconfigured, + string $client_root_provided_later, + string $client_path, + string $server_path ): void { - $mapper = new PathMapper($serverRoot, $clientRootPreconfigured); - $mapper->configureClientRoot($clientRootProvidedLater); + $mapper = new PathMapper($server_root, $client_root_preconfigured); + $mapper->configureClientRoot($client_root_provided_later); $this->assertSame( - $clientPath, - $mapper->mapServerToClient($serverPath) + $client_path, + $mapper->mapServerToClient($server_path) ); } From 6eb7a688d1e682753cf5ea5ba778ebd817612564 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 23 Jul 2023 02:10:49 +0200 Subject: [PATCH 37/56] Introduce `--map-folder` switch And create PathMapper based on its value --- src/Psalm/Internal/Cli/LanguageServer.php | 67 ++++++++++++++++++- .../LanguageServer/LanguageServer.php | 13 ++-- tests/LanguageServer/DiagnosticTest.php | 3 +- 3 files changed, 73 insertions(+), 10 deletions(-) diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index cc52be7a7d4..026d67efda5 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -10,6 +10,7 @@ use Psalm\Internal\IncludeCollector; use Psalm\Internal\LanguageServer\ClientConfiguration; use Psalm\Internal\LanguageServer\LanguageServer as LanguageServerLanguageServer; +use Psalm\Internal\LanguageServer\PathMapper; use Psalm\Report; use function array_key_exists; @@ -75,6 +76,7 @@ public static function run(array $argv): void 'find-dead-code', 'help', 'root:', + 'map-folder::', 'use-ini-defaults', 'version', 'tcp:', @@ -127,6 +129,14 @@ static function (string $arg) use ($valid_long_options): void { // get options from command line $options = getopt(implode('', $valid_short_options), $valid_long_options); + if ($options === false) { + // shouldn't really happen, but just in case + fwrite( + STDERR, + 'Failed to get CLI args' . PHP_EOL, + ); + exit(1); + } if (!array_key_exists('use-ini-defaults', $options)) { ini_set('display_errors', '1'); @@ -169,6 +179,14 @@ static function (string $arg) use ($valid_long_options): void { -r, --root If running Psalm globally you'll need to specify a project root. Defaults to cwd + --map-folder[=SERVER_FOLDER:CLIENT_FOLDER] + Specify folder to map between the client and the server. Use this when the client + and server have different views of the filesystem (e.g. in a docker container). + Defaults to mapping the rootUri provided by the client to the server's cwd, + or `-r` if provided. + + No mapping is done when this option is not specified. + --find-dead-code Look for dead code @@ -254,8 +272,6 @@ static function (string $arg) use ($valid_long_options): void { $current_dir = $root_path . DIRECTORY_SEPARATOR; } - $server_start_dir = $current_dir; - $vendor_dir = CliUtils::getVendorDir($current_dir); $include_collector = new IncludeCollector(); @@ -293,6 +309,8 @@ static function (string $arg) use ($valid_long_options): void { setlocale(LC_CTYPE, 'C'); + $path_mapper = self::createPathMapper($options, $current_dir); + $path_to_config = CliUtils::getPathToConfig($options); if (isset($options['tcp'])) { @@ -396,6 +414,49 @@ static function (string $arg) use ($valid_long_options): void { $clientConfiguration->TCPServerAddress = $options['tcp'] ?? null; $clientConfiguration->TCPServerMode = isset($options['tcp-server']); - LanguageServerLanguageServer::run($config, $clientConfiguration, $current_dir, $server_start_dir, $inMemory); + LanguageServerLanguageServer::run($config, $clientConfiguration, $current_dir, $path_mapper, $inMemory); + } + + /** @param array> $options */ + private static function createPathMapper(array $options, string $server_start_dir): PathMapper + { + if (!isset($options['map-folder'])) { + // dummy no-op mapper + return new PathMapper('/', '/'); + } + + $map_folder = $options['map-folder']; + + if ($map_folder === false) { + // autoconfigured mapper + return new PathMapper($server_start_dir, null); + } + + if (is_string($map_folder)) { + if (strpos($map_folder, ':') === false) { + fwrite( + STDERR, + 'invalid format for --map-folder option' . PHP_EOL, + ); + exit(1); + } + /** @psalm-suppress PossiblyUndefinedArrayOffset we just checked that we have the separator*/ + [$server_dir, $client_dir] = explode(':', $map_folder, 2); + if (!strlen($server_dir) || !strlen($client_dir)) { + fwrite( + STDERR, + 'invalid format for --map-folder option, ' + . 'neither SERVER_FOLDER nor CLIENT_FOLDER can be empty' . PHP_EOL, + ); + exit(1); + } + return new PathMapper($server_dir, $client_dir); + } + + fwrite( + STDERR, + '--map-folder option can only be specified once' . PHP_EOL, + ); + exit(1); } } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 4ffe03af380..13dc3c993d7 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -153,7 +153,7 @@ public function __construct( Codebase $codebase, ClientConfiguration $clientConfiguration, Progress $progress, - string $server_start_dir + PathMapper $path_mapper ) { parent::__construct($this, '/'); @@ -163,6 +163,8 @@ public function __construct( $this->codebase = $codebase; + $this->path_mapper = $path_mapper; + $this->protocolWriter = $writer; $this->protocolReader = $reader; @@ -245,7 +247,6 @@ function (): void { $this->client = new LanguageClient($reader, $writer, $this, $clientConfiguration); - $this->path_mapper = new PathMapper($server_start_dir, null); $this->logInfo("Psalm Language Server ".PSALM_VERSION." has started."); } @@ -257,7 +258,7 @@ public static function run( Config $config, ClientConfiguration $clientConfiguration, string $base_dir, - string $server_start_dir, + PathMapper $path_mapper, bool $inMemory = false ): void { $progress = new Progress(); @@ -330,7 +331,7 @@ public static function run( $codebase, $clientConfiguration, $progress, - $server_start_dir, + $path_mapper, ); Loop::run(); } elseif ($clientConfiguration->TCPServerMode && $clientConfiguration->TCPServerAddress) { @@ -354,7 +355,7 @@ public static function run( $codebase, $clientConfiguration, $progress, - $server_start_dir, + $path_mapper, ); Loop::run(); } @@ -368,7 +369,7 @@ public static function run( $codebase, $clientConfiguration, $progress, - $server_start_dir, + $path_mapper, ); Loop::run(); } diff --git a/tests/LanguageServer/DiagnosticTest.php b/tests/LanguageServer/DiagnosticTest.php index ea480138c20..470bc68199e 100644 --- a/tests/LanguageServer/DiagnosticTest.php +++ b/tests/LanguageServer/DiagnosticTest.php @@ -9,6 +9,7 @@ use Psalm\Internal\LanguageServer\ClientConfiguration; use Psalm\Internal\LanguageServer\LanguageServer; use Psalm\Internal\LanguageServer\Message; +use Psalm\Internal\LanguageServer\PathMapper; use Psalm\Internal\LanguageServer\Progress; use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; @@ -85,7 +86,7 @@ public function testSnippetSupportDisabled(): void $this->codebase, $clientConfiguration, new Progress, - getcwd(), + new PathMapper(getcwd(), getcwd()), ); $write->on('message', function (Message $message) use ($deferred, $server): void { From 0a2a0feaf233df92253e8b3725825a5b3309091e Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 23 Jul 2023 02:16:44 +0200 Subject: [PATCH 38/56] CS fix --- src/Psalm/Internal/Cli/LanguageServer.php | 2 ++ src/Psalm/Internal/LanguageServer/PathMapper.php | 4 ++++ tests/LanguageServer/DiagnosticTest.php | 1 + tests/LanguageServer/PathMapperTest.php | 8 ++++---- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 026d67efda5..07d5e6f93e8 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -19,6 +19,7 @@ use function array_slice; use function chdir; use function error_log; +use function explode; use function fwrite; use function gc_disable; use function getcwd; @@ -32,6 +33,7 @@ use function preg_replace; use function realpath; use function setlocale; +use function strlen; use function strpos; use function strtolower; use function substr; diff --git a/src/Psalm/Internal/LanguageServer/PathMapper.php b/src/Psalm/Internal/LanguageServer/PathMapper.php index 88ff1661d2c..bd4b815bc94 100644 --- a/src/Psalm/Internal/LanguageServer/PathMapper.php +++ b/src/Psalm/Internal/LanguageServer/PathMapper.php @@ -2,6 +2,10 @@ namespace Psalm\Internal\LanguageServer; +use function rtrim; +use function strlen; +use function substr; + /** @internal */ final class PathMapper { diff --git a/tests/LanguageServer/DiagnosticTest.php b/tests/LanguageServer/DiagnosticTest.php index 470bc68199e..b40ef38ace3 100644 --- a/tests/LanguageServer/DiagnosticTest.php +++ b/tests/LanguageServer/DiagnosticTest.php @@ -23,6 +23,7 @@ use Psalm\Tests\TestConfig; use function Amp\Promise\wait; +use function getcwd; use function rand; class DiagnosticTest extends AsyncTestCase diff --git a/tests/LanguageServer/PathMapperTest.php b/tests/LanguageServer/PathMapperTest.php index ac6e1fd5c32..2e64b356399 100644 --- a/tests/LanguageServer/PathMapperTest.php +++ b/tests/LanguageServer/PathMapperTest.php @@ -13,7 +13,7 @@ public function testUsesUpdatedClientRoot(): void $mapper->configureClientRoot('/home/user/src/project'); $this->assertSame( '/home/user/src/project/filename.php', - $mapper->mapServerToClient('/var/www/filename.php') + $mapper->mapServerToClient('/var/www/filename.php'), ); } @@ -25,7 +25,7 @@ public function testIgnoresClientRootIfItWasPreconfigures(): void $this->assertSame( '/home/user/src/project/filename.php', - $mapper->mapServerToClient('/var/www/filename.php') + $mapper->mapServerToClient('/var/www/filename.php'), ); } @@ -43,7 +43,7 @@ public function testMapsClientToServer( $mapper->configureClientRoot($client_root_provided_later); $this->assertSame( $server_ath, - $mapper->mapClientToServer($client_path) + $mapper->mapClientToServer($client_path), ); } @@ -59,7 +59,7 @@ public function testMapsServerToClient( $mapper->configureClientRoot($client_root_provided_later); $this->assertSame( $client_path, - $mapper->mapServerToClient($server_path) + $mapper->mapServerToClient($server_path), ); } From bb102760ea2bb901af3927d1a211171a0715c7a7 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 23 Jul 2023 02:18:56 +0200 Subject: [PATCH 39/56] Drop unused property --- src/Psalm/Internal/LanguageServer/LanguageServer.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 13dc3c993d7..628fb9cc253 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -142,8 +142,6 @@ class LanguageServer extends Dispatcher */ protected JsonMapper $mapper; - protected ?string $clientRootPath = null; - protected PathMapper $path_mapper; public function __construct( From 5c0154c422a5207115cef95c912d289eab721057 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 23 Jul 2023 02:38:07 +0200 Subject: [PATCH 40/56] Added docs on running LS in a container --- docs/running_psalm/language_server.md | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/docs/running_psalm/language_server.md b/docs/running_psalm/language_server.md index 47504068898..3d80a91c9d6 100644 --- a/docs/running_psalm/language_server.md +++ b/docs/running_psalm/language_server.md @@ -6,7 +6,9 @@ It currently supports diagnostics (i.e. finding errors and warnings), go-to-defi It works well in a variety of editors (listed alphabetically): -## Emacs +## Client configuration + +### Emacs I got it working with [eglot](https://github.com/joaotavora/eglot) @@ -27,13 +29,13 @@ This is the config I used: ) ``` -## PhpStorm +### PhpStorm -### Native Support +#### Native Support As of PhpStorm 2020.3 support for psalm is supported and on by default, you can read more about that [here](https://www.jetbrains.com/help/phpstorm/using-psalm.html) -### With LSP +#### With LSP Alternatively, psalm works with `gtache/intellij-lsp` plugin ([Jetbrains-approved version](https://plugins.jetbrains.com/plugin/10209-lsp-support), [latest version](https://github.com/gtache/intellij-lsp/releases/tag/v1.6.0)). @@ -51,7 +53,7 @@ In the "Server definitions" tab you should add a definition for Psalm: In the "Timeouts" tab you can adjust the initialization timeout. This is important if you have a large project. You should set the "Init" value to the number of milliseconds you allow Psalm to scan your entire project and your project's dependencies. For opening a couple of projects that use large PHP frameworks, on a high-end business laptop, try `240000` milliseconds for Init. -## Sublime Text +### Sublime Text I use the excellent Sublime [LSP plugin](https://github.com/tomv564/LSP) with the following config(Package Settings > LSP > Settings): ```json @@ -64,7 +66,7 @@ I use the excellent Sublime [LSP plugin](https://github.com/tomv564/LSP) with th } ``` -## Vim & Neovim +### Vim & Neovim **ALE** @@ -105,6 +107,15 @@ Add settings to `coc-settings.json`: } ``` -## VS Code +### VS Code [Get the Psalm plugin here](https://marketplace.visualstudio.com/items?itemName=getpsalm.psalm-vscode-plugin) (Requires VS Code 1.26+): + +## Running the server in a docker container + +Make sure you use `--map-folder` option. Using it without argument will map the server's CWD to the host's project root folder. You can also specify a custom mapping. For example: +```bash +docker-compose exec php /usr/share/php/psalm/psalm-language-server \ + -r=/var/www/html \ + --map-folder=/var/www/html:$PWD +``` From e1a30a20a1e00ee87671d6d3077a12e4c3e66530 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Tue, 25 Jul 2023 00:11:35 +0200 Subject: [PATCH 41/56] Map special RPC paths Now `$/cancelRequest` will be resolved to `$server->cancelRequest()` and `$/textDocument/whatever` to `$server->textDocument->whatever()` --- .../Internal/LanguageServer/LanguageServer.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 54009b9cc5a..aa709503831 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -997,4 +997,19 @@ public static function uriToPath(string $uri): string return $filepath; } + + // the methods below forward special paths + // like `$/cancelRequest` to `$this->cancelRequest()` + // and `$/a/b/c` to `$this->a->b->c()` + + public function __isset(string $prop_name): bool + { + return $prop_name === '$'; + } + + /** @return static */ + public function __get(string $prop_name): self + { + return $this; + } } From 4a0a12d9e23e28ccda9b05a8461893f82c5ccd04 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Tue, 25 Jul 2023 00:17:12 +0200 Subject: [PATCH 42/56] Fix unused var --- src/Psalm/Internal/LanguageServer/LanguageServer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index aa709503831..4edcc7449aa 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -1008,7 +1008,7 @@ public function __isset(string $prop_name): bool } /** @return static */ - public function __get(string $prop_name): self + public function __get(string $_prop_name): self { return $this; } From 902f90b19b5a55a0c703a701b6b339d28eb43f99 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Tue, 25 Jul 2023 21:56:52 +0200 Subject: [PATCH 43/56] Modernize LSP progress reporting This will use `$/progress` when available and fall back to old telemetry-based reporting otherwise --- .../Client/Progress/LegacyProgress.php | 60 +++++++++++ .../Client/Progress/Progress.php | 102 ++++++++++++++++++ .../Client/Progress/ProgressInterface.php | 16 +++ .../LanguageServer/LanguageClient.php | 12 +++ .../LanguageServer/LanguageServer.php | 18 ++-- 5 files changed, 201 insertions(+), 7 deletions(-) create mode 100644 src/Psalm/Internal/LanguageServer/Client/Progress/LegacyProgress.php create mode 100644 src/Psalm/Internal/LanguageServer/Client/Progress/Progress.php create mode 100644 src/Psalm/Internal/LanguageServer/Client/Progress/ProgressInterface.php diff --git a/src/Psalm/Internal/LanguageServer/Client/Progress/LegacyProgress.php b/src/Psalm/Internal/LanguageServer/Client/Progress/LegacyProgress.php new file mode 100644 index 00000000000..8bbc1541002 --- /dev/null +++ b/src/Psalm/Internal/LanguageServer/Client/Progress/LegacyProgress.php @@ -0,0 +1,60 @@ +handler = $handler; + } + + public function begin(string $title, ?string $message = null, ?int $percentage = null): void + { + if ($this->title !== null) { + throw new LogicException('Progress has already been started'); + } + + $this->title = $title; + + $this->notify($message); + } + + public function update(?string $message = null, ?int $percentage = null): void + { + if ($this->title === null) { + throw new LogicException('The progress has not been started yet'); + } + + $this->notify($message); + } + + public function end(?string $message = null): void + { + if ($this->title === null) { + throw new LogicException('The progress has not been started yet'); + } + + $this->notify($message); + } + + private function notify(?string $message): void + { + $this->handler->notify( + 'telemetry/event', + new LogMessage( + MessageType::INFO, + $this->title . (empty($message) ? '' : (': ' . $message)), + ), + ); + } +} diff --git a/src/Psalm/Internal/LanguageServer/Client/Progress/Progress.php b/src/Psalm/Internal/LanguageServer/Client/Progress/Progress.php new file mode 100644 index 00000000000..edb6428e4ee --- /dev/null +++ b/src/Psalm/Internal/LanguageServer/Client/Progress/Progress.php @@ -0,0 +1,102 @@ +handler = $handler; + $this->token = $token; + } + + public function begin( + string $title, + ?string $message = null, + ?int $percentage = null + ): void { + if ($this->finished) { + throw new LogicException('Progress has already been finished'); + } + + $notification = [ + 'token' => $this->token, + 'value' => [ + 'kind' => 'begin', + 'title' => $title, + ], + ]; + + if ($message !== null) { + $notification['value']['message'] = $message; + } + + if ($percentage !== null) { + $notification['value']['percentage'] = $percentage; + $this->withPercentage = true; + } + + $this->handler->notify('$/progress', $notification); + } + + public function end(?string $message = null): void + { + if ($this->finished) { + throw new LogicException('Progress has already been finished'); + } + + $notification = [ + 'token' => $this->token, + 'value' => [ + 'kind' => 'end', + ], + ]; + + if ($message !== null) { + $notification['value']['message'] = $message; + } + + $this->handler->notify('$/progress', $notification); + + $this->finished = true; + } + + public function update(?string $message = null, ?int $percentage = null): void + { + if ($this->finished) { + throw new LogicException('Progress has already been finished'); + } + + $notification = [ + 'token' => $this->token, + 'value' => [ + 'kind' => 'report', + ], + ]; + + if ($message !== null) { + $notification['value']['message'] = $message; + } + + if ($percentage !== null) { + if (!$this->withPercentage) { + throw new LogicException( + 'Cannot update percentage for progress ' + . 'that was started without percentage', + ); + } + $notification['value']['percentage'] = $percentage; + } + + $this->handler->notify('$/progress', $notification); + } +} diff --git a/src/Psalm/Internal/LanguageServer/Client/Progress/ProgressInterface.php b/src/Psalm/Internal/LanguageServer/Client/Progress/ProgressInterface.php new file mode 100644 index 00000000000..78f045c2c53 --- /dev/null +++ b/src/Psalm/Internal/LanguageServer/Client/Progress/ProgressInterface.php @@ -0,0 +1,16 @@ +server->clientCapabilities->window->workDoneProgress ?? false) { + return new Progress($this->handler, $token); + } else { + return new LegacyProgress($this->handler); + } + } + /** * Configuration Refreshed from Client * diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 3fa4261c8d3..995e9b21065 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -88,6 +88,7 @@ use function strpos; use function substr; use function trim; +use function uniqid; use function urldecode; use const JSON_PRETTY_PRINT; @@ -398,7 +399,8 @@ public function initialize( ?string $rootPath = null, ?string $rootUri = null, $initializationOptions = null, - ?string $trace = null + ?string $trace = null, + ?string $workdDoneToken = null //?array $workspaceFolders = null //error in json-dispatcher ): Promise { $this->clientInfo = $clientInfo; @@ -412,9 +414,11 @@ public function initialize( return call( /** @return Generator */ - function () { + function () use ($workdDoneToken) { + $progress = $this->client->makeProgress($workdDoneToken ?? uniqid('tkn', true)); + $this->logInfo("Initializing..."); - $this->clientStatus('initializing'); + $progress->begin('Initialization', 'Starting'); // Eventually, this might block on something. Leave it as a generator. /** @psalm-suppress TypeDoesNotContainType */ @@ -425,14 +429,14 @@ function () { $this->project_analyzer->serverMode($this); $this->logInfo("Initializing: Getting code base..."); - $this->clientStatus('initializing', 'getting code base'); + $progress->update('Getting code base'); $this->logInfo("Initializing: Scanning files ({$this->project_analyzer->threads} Threads)..."); - $this->clientStatus('initializing', 'scanning files'); + $progress->update('Scanning files'); $this->codebase->scanFiles($this->project_analyzer->threads); $this->logInfo("Initializing: Registering stub files..."); - $this->clientStatus('initializing', 'registering stub files'); + $progress->update('Registering stub files'); $this->codebase->config->visitStubFiles($this->codebase, $this->project_analyzer->progress); if ($this->textDocument === null) { @@ -572,7 +576,7 @@ function () { } $this->logInfo("Initializing: Complete."); - $this->clientStatus('initialized'); + $progress->end('Initialized'); /** * Information about the server. From 5f828c75d1a3eae6307fbf4e524485bb0fb66c57 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Tue, 25 Jul 2023 23:10:16 +0200 Subject: [PATCH 44/56] Make sure we get the token Name does matter, as is the presence of docblock --- src/Psalm/Internal/LanguageServer/LanguageServer.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 995e9b21065..3c09b091adc 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -388,6 +388,7 @@ public static function run( * @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open. * @param mixed $initializationOptions * @param string|null $trace The initial trace setting. If omitted trace is disabled ('off'). + * @param string|null $workDoneToken The token to be used to report progress during init. * @psalm-return Promise * @psalm-suppress PossiblyUnusedParam */ @@ -400,7 +401,7 @@ public function initialize( ?string $rootUri = null, $initializationOptions = null, ?string $trace = null, - ?string $workdDoneToken = null + ?string $workDoneToken = null //?array $workspaceFolders = null //error in json-dispatcher ): Promise { $this->clientInfo = $clientInfo; @@ -414,8 +415,8 @@ public function initialize( return call( /** @return Generator */ - function () use ($workdDoneToken) { - $progress = $this->client->makeProgress($workdDoneToken ?? uniqid('tkn', true)); + function () use ($workDoneToken) { + $progress = $this->client->makeProgress($workDoneToken ?? uniqid('tkn', true)); $this->logInfo("Initializing..."); $progress->begin('Initialization', 'Starting'); From 85b2af83daffbe1b18ecb51ef77c52b6f3c39658 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Tue, 25 Jul 2023 23:13:31 +0200 Subject: [PATCH 45/56] Cosmetic changes for VSCode --- src/Psalm/Internal/LanguageServer/LanguageServer.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 3c09b091adc..af1a2a7b116 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -419,7 +419,7 @@ function () use ($workDoneToken) { $progress = $this->client->makeProgress($workDoneToken ?? uniqid('tkn', true)); $this->logInfo("Initializing..."); - $progress->begin('Initialization', 'Starting'); + $progress->begin('Psalm', 'initializing'); // Eventually, this might block on something. Leave it as a generator. /** @psalm-suppress TypeDoesNotContainType */ @@ -430,14 +430,14 @@ function () use ($workDoneToken) { $this->project_analyzer->serverMode($this); $this->logInfo("Initializing: Getting code base..."); - $progress->update('Getting code base'); + $progress->update('getting code base'); $this->logInfo("Initializing: Scanning files ({$this->project_analyzer->threads} Threads)..."); - $progress->update('Scanning files'); + $progress->update('scanning files'); $this->codebase->scanFiles($this->project_analyzer->threads); $this->logInfo("Initializing: Registering stub files..."); - $progress->update('Registering stub files'); + $progress->update('registering stub files'); $this->codebase->config->visitStubFiles($this->codebase, $this->project_analyzer->progress); if ($this->textDocument === null) { @@ -577,7 +577,7 @@ function () use ($workDoneToken) { } $this->logInfo("Initializing: Complete."); - $progress->end('Initialized'); + $progress->end('initialized'); /** * Information about the server. From 4f6fc3585b650956255f91633bec121de3604c70 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Tue, 25 Jul 2023 23:50:07 +0200 Subject: [PATCH 46/56] Update tests to account for new progress protocol --- tests/LanguageServer/DiagnosticTest.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/LanguageServer/DiagnosticTest.php b/tests/LanguageServer/DiagnosticTest.php index b40ef38ace3..d8814ec3633 100644 --- a/tests/LanguageServer/DiagnosticTest.php +++ b/tests/LanguageServer/DiagnosticTest.php @@ -91,10 +91,21 @@ public function testSnippetSupportDisabled(): void ); $write->on('message', function (Message $message) use ($deferred, $server): void { - /** @psalm-suppress PossiblyNullPropertyFetch,UndefinedPropertyFetch,MixedPropertyFetch */ - if ($message->body->method === 'telemetry/event' && $message->body->params->message === 'initialized') { + /** @psalm-suppress NullPropertyFetch,PossiblyNullPropertyFetch,UndefinedPropertyFetch */ + if ($message->body->method === 'telemetry/event' && ($message->body->params->message ?? null) === 'initialized') { $this->assertFalse($server->clientCapabilities->textDocument->completion->completionItem->snippetSupport); $deferred->resolve(null); + return; + } + + /** @psalm-suppress NullPropertyFetch,PossiblyNullPropertyFetch */ + if ($message->body->method === '$/progress' + && ($message->body->params->value->kind ?? null) === 'end' + && ($message->body->params->value->message ?? null) === 'initialized' + ) { + $this->assertFalse($server->clientCapabilities->textDocument->completion->completionItem->snippetSupport); + $deferred->resolve(null); + return; } }); From c2a05c2e9002b6e4854194b713171f14941cc268 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Wed, 26 Jul 2023 03:59:59 +0200 Subject: [PATCH 47/56] Ensure correct method call order --- .../Client/Progress/LegacyProgress.php | 33 ++++++++++++++++--- .../Client/Progress/Progress.php | 29 +++++++++++++--- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/Client/Progress/LegacyProgress.php b/src/Psalm/Internal/LanguageServer/Client/Progress/LegacyProgress.php index 8bbc1541002..13eba280c7e 100644 --- a/src/Psalm/Internal/LanguageServer/Client/Progress/LegacyProgress.php +++ b/src/Psalm/Internal/LanguageServer/Client/Progress/LegacyProgress.php @@ -10,6 +10,12 @@ /** @internal */ final class LegacyProgress implements ProgressInterface { + private const STATUS_INACTIVE = 'inactive'; + private const STATUS_ACTIVE = 'active'; + private const STATUS_FINISHED = 'finished'; + + private string $status = self::STATUS_INACTIVE; + private ClientHandler $handler; private ?string $title = null; @@ -20,19 +26,30 @@ public function __construct(ClientHandler $handler) public function begin(string $title, ?string $message = null, ?int $percentage = null): void { - if ($this->title !== null) { + + if ($this->status === self::STATUS_ACTIVE) { throw new LogicException('Progress has already been started'); } + if ($this->status === self::STATUS_FINISHED) { + throw new LogicException('Progress has already been finished'); + } + $this->title = $title; $this->notify($message); + + $this->status = self::STATUS_ACTIVE; } public function update(?string $message = null, ?int $percentage = null): void { - if ($this->title === null) { - throw new LogicException('The progress has not been started yet'); + if ($this->status === self::STATUS_FINISHED) { + throw new LogicException('Progress has already been finished'); + } + + if ($this->status === self::STATUS_INACTIVE) { + throw new LogicException('Progress has not been started yet'); } $this->notify($message); @@ -40,11 +57,17 @@ public function update(?string $message = null, ?int $percentage = null): void public function end(?string $message = null): void { - if ($this->title === null) { - throw new LogicException('The progress has not been started yet'); + if ($this->status === self::STATUS_FINISHED) { + throw new LogicException('Progress has already been finished'); + } + + if ($this->status === self::STATUS_INACTIVE) { + throw new LogicException('Progress has not been started yet'); } $this->notify($message); + + $this->status = self::STATUS_FINISHED; } private function notify(?string $message): void diff --git a/src/Psalm/Internal/LanguageServer/Client/Progress/Progress.php b/src/Psalm/Internal/LanguageServer/Client/Progress/Progress.php index edb6428e4ee..8a1ca5da3a9 100644 --- a/src/Psalm/Internal/LanguageServer/Client/Progress/Progress.php +++ b/src/Psalm/Internal/LanguageServer/Client/Progress/Progress.php @@ -8,10 +8,15 @@ /** @internal */ final class Progress implements ProgressInterface { + private const STATUS_INACTIVE = 'inactive'; + private const STATUS_ACTIVE = 'active'; + private const STATUS_FINISHED = 'finished'; + + private string $status = self::STATUS_INACTIVE; + private ClientHandler $handler; private string $token; private bool $withPercentage = false; - private bool $finished = false; public function __construct(ClientHandler $handler, string $token) { @@ -24,7 +29,11 @@ public function begin( ?string $message = null, ?int $percentage = null ): void { - if ($this->finished) { + if ($this->status === self::STATUS_ACTIVE) { + throw new LogicException('Progress has already been started'); + } + + if ($this->status === self::STATUS_FINISHED) { throw new LogicException('Progress has already been finished'); } @@ -46,14 +55,20 @@ public function begin( } $this->handler->notify('$/progress', $notification); + + $this->status = self::STATUS_ACTIVE; } public function end(?string $message = null): void { - if ($this->finished) { + if ($this->status === self::STATUS_FINISHED) { throw new LogicException('Progress has already been finished'); } + if ($this->status === self::STATUS_INACTIVE) { + throw new LogicException('Progress has not been started yet'); + } + $notification = [ 'token' => $this->token, 'value' => [ @@ -67,15 +82,19 @@ public function end(?string $message = null): void $this->handler->notify('$/progress', $notification); - $this->finished = true; + $this->status = self::STATUS_FINISHED; } public function update(?string $message = null, ?int $percentage = null): void { - if ($this->finished) { + if ($this->status === self::STATUS_FINISHED) { throw new LogicException('Progress has already been finished'); } + if ($this->status === self::STATUS_INACTIVE) { + throw new LogicException('Progress has not been started yet'); + } + $notification = [ 'token' => $this->token, 'value' => [ From a34222aa76b4bbeeccd8d58e501297808b0702ba Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Wed, 26 Jul 2023 04:19:46 +0200 Subject: [PATCH 48/56] Drop unused parameters for RPC methods Parameters are marshalled using their names and docblock tags, so it's safe to do. --- src/Psalm/Internal/LanguageServer/LanguageServer.php | 12 ------------ .../Internal/LanguageServer/Server/TextDocument.php | 4 +--- .../Internal/LanguageServer/Server/Workspace.php | 5 ++--- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index af1a2a7b116..cc358753644 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -378,31 +378,19 @@ public static function run( * The initialize request is sent as the first request from the client to the server. * * @param ClientCapabilities $capabilities The capabilities provided by the client (editor) - * @param int|null $processId The process Id of the parent process that started the server. * Is null if the process has not been started by another process. If the parent process is * not alive then the server should exit (see exit notification) its process. * @param ClientInfo|null $clientInfo Information about the client - * @param string|null $locale The locale the client is currently showing the user interface - * in. This must not necessarily be the locale of the operating - * system. - * @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open. - * @param mixed $initializationOptions * @param string|null $trace The initial trace setting. If omitted trace is disabled ('off'). * @param string|null $workDoneToken The token to be used to report progress during init. * @psalm-return Promise - * @psalm-suppress PossiblyUnusedParam */ public function initialize( ClientCapabilities $capabilities, - ?int $processId = null, ?ClientInfo $clientInfo = null, - ?string $locale = null, - ?string $rootPath = null, ?string $rootUri = null, - $initializationOptions = null, ?string $trace = null, ?string $workDoneToken = null - //?array $workspaceFolders = null //error in json-dispatcher ): Promise { $this->clientInfo = $clientInfo; $this->clientCapabilities = $capabilities; diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index b24862e7460..508af5aeb9f 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -398,10 +398,8 @@ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $po * The code action request is sent from the client to the server to compute commands * for a given text document and range. These commands are typically code fixes to * either fix problems or to beautify/refactor code. - * - * @psalm-suppress PossiblyUnusedParam */ - public function codeAction(TextDocumentIdentifier $textDocument, Range $range, CodeActionContext $context): Promise + public function codeAction(TextDocumentIdentifier $textDocument, CodeActionContext $context): Promise { if (!$this->server->client->clientConfiguration->provideCodeActions) { return new Success(null); diff --git a/src/Psalm/Internal/LanguageServer/Server/Workspace.php b/src/Psalm/Internal/LanguageServer/Server/Workspace.php index 113a8f17974..6d2e1622575 100644 --- a/src/Psalm/Internal/LanguageServer/Server/Workspace.php +++ b/src/Psalm/Internal/LanguageServer/Server/Workspace.php @@ -108,10 +108,9 @@ public function didChangeWatchedFiles(array $changes): void /** * A notification sent from the client to the server to signal the change of configuration settings. * - * @param mixed $settings - * @psalm-suppress PossiblyUnusedMethod, PossiblyUnusedParam + * @psalm-suppress PossiblyUnusedMethod */ - public function didChangeConfiguration($settings): void + public function didChangeConfiguration(): void { $this->server->logDebug( 'workspace/didChangeConfiguration', From fc74ae83e66ef2ce329269aa4d123690f6319312 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Thu, 27 Jul 2023 00:40:26 +0200 Subject: [PATCH 49/56] #9974 added return type detection for PDOStatement::fetchAll, extended return type detection for PDOStatement::fetch --- .../PdoStatementReturnTypeProvider.php | 311 ++++++++++++++---- tests/MethodCallTest.php | 111 ++++++- 2 files changed, 351 insertions(+), 71 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index 0e09aa85a7c..3f1addedc85 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -27,88 +27,261 @@ public static function getClassLikeNames(): array public static function getMethodReturnType(MethodReturnTypeProviderEvent $event): ?Union { $config = Config::getInstance(); + $method_name_lowercase = $event->getMethodNameLowercase(); + + if (!$config->php_extensions["pdo"]) { + return null; + } + + if ($method_name_lowercase === 'setfetchmode') { + return self::handleSetFetchMode($event); + } + + if ($method_name_lowercase === 'fetch') { + return self::handleFetch($event); + } + + if ($method_name_lowercase === 'fetchall') { + return self::handleFetchAll($event); + } + + return null; + } + + private static function handleSetFetchMode(MethodReturnTypeProviderEvent $event) + { $source = $event->getSource(); $call_args = $event->getCallArgs(); - $method_name_lowercase = $event->getMethodNameLowercase(); - if ($method_name_lowercase === 'fetch' - && $config->php_extensions["pdo"] - && isset($call_args[0]) + $context = $event->getContext(); + + if (isset($call_args[0]) + && ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value)) + && $first_arg_type->isSingleIntLiteral() + ) { + $context->references_in_scope['fetch_mode'] = $first_arg_type->getSingleIntLiteral()->value; + } + + if (isset($call_args[1]) + && ($second_arg_type = $source->getNodeTypeProvider()->getType($call_args[1]->value)) + && $second_arg_type->isSingleStringLiteral() + ) { + $context->references_in_scope['fetch_class'] = $second_arg_type->getSingleStringLiteral()->value; + } + + return null; + } + + private static function handleFetch(MethodReturnTypeProviderEvent $event): ?Union + { + $source = $event->getSource(); + $call_args = $event->getCallArgs(); + $context = $event->getContext(); + + $fetch_mode = $context->references_in_scope['fetch_mode'] ?? null; + + if (isset($call_args[0]) && ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value)) && $first_arg_type->isSingleIntLiteral() ) { $fetch_mode = $first_arg_type->getSingleIntLiteral()->value; + } - switch ($fetch_mode) { - case 2: // PDO::FETCH_ASSOC - array|false - return new Union([ - new TArray([ - Type::getString(), - new Union([ - new TScalar(), - new TNull(), - ]), + $fetch_class_name = $context->references_in_scope['fetch_class'] ?? null; + + switch ($fetch_mode) { + case 2: // PDO::FETCH_ASSOC - array|false + return new Union([ + new TArray([ + Type::getString(), + new Union([ + new TScalar(), + new TNull(), ]), - new TFalse(), - ]); - - case 4: // PDO::FETCH_BOTH - array|false - return new Union([ - new TArray([ - Type::getArrayKey(), - new Union([ - new TScalar(), - new TNull(), + ]), + new TFalse(), + ]); + + case 4: // PDO::FETCH_BOTH - array|false + return new Union([ + new TArray([ + Type::getArrayKey(), + new Union([ + new TScalar(), + new TNull(), + ]), + ]), + new TFalse(), + ]); + + case 6: // PDO::FETCH_BOUND - bool + return Type::getBool(); + + case 8: // PDO::FETCH_CLASS - object|false + return new Union([ + $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject(), + new TFalse(), + ]); + + case 1: // PDO::FETCH_LAZY - object|false + // This actually returns a PDORow object, but that class is + // undocumented, and its attributes are all dynamic anyway + return new Union([ + new TObject(), + new TFalse(), + ]); + + case 11: // PDO::FETCH_NAMED - array>|false + return new Union([ + new TArray([ + Type::getString(), + new Union([ + new TScalar(), + Type::getListAtomic(Type::getScalar()), + ]), + ]), + new TFalse(), + ]); + + case 3: // PDO::FETCH_NUM - list|false + return new Union([ + Type::getListAtomic( + new Union([ + new TScalar(), + new TNull(), + ]), + ), + new TFalse(), + ]); + + case 5: // PDO::FETCH_OBJ - stdClass|false + return new Union([ + new TNamedObject('stdClass'), + new TFalse(), + ]); + } + + return null; + } + + private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?Union + { + $source = $event->getSource(); + $call_args = $event->getCallArgs(); + $context = $event->getContext(); + + $fetch_mode = $context->references_in_scope['fetch_mode'] ?? null; + + if (isset($call_args[0]) + && ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value)) + && $first_arg_type->isSingleIntLiteral() + ) { + $fetch_mode = $first_arg_type->getSingleIntLiteral()->value; + } + + $fetch_class_name = $context->references_in_scope['fetch_class'] ?? null; + + if (isset($call_args[1]) + && ($second_arg_type = $source->getNodeTypeProvider()->getType($call_args[1]->value)) + && $second_arg_type->isSingleStringLiteral() + ) { + $fetch_class_name = $second_arg_type->getSingleStringLiteral()->value; + } + + switch ($fetch_mode) { + case 2: // PDO::FETCH_ASSOC - list> + return new Union([ + Type::getListAtomic( + new Union([ + new TArray([ + Type::getString(), + new Union([ + new TScalar(), + new TNull(), + ]), ]), ]), - new TFalse(), - ]); - - case 6: // PDO::FETCH_BOUND - bool - return Type::getBool(); - - case 8: // PDO::FETCH_CLASS - object|false - return new Union([ - new TObject(), - new TFalse(), - ]); - - case 1: // PDO::FETCH_LAZY - object|false - // This actually returns a PDORow object, but that class is - // undocumented, and its attributes are all dynamic anyway - return new Union([ - new TObject(), - new TFalse(), - ]); - - case 11: // PDO::FETCH_NAMED - array>|false - return new Union([ - new TArray([ - Type::getString(), - new Union([ - new TScalar(), - Type::getListAtomic(Type::getScalar()), + ), + ]); + + case 4: // PDO::FETCH_BOTH - list> + return new Union([ + Type::getListAtomic( + new Union([ + new TArray([ + Type::getArrayKey(), + new Union([ + new TScalar(), + new TNull(), + ]), ]), ]), - new TFalse(), - ]); - - case 3: // PDO::FETCH_NUM - list|false - return new Union([ - Type::getListAtomic( - new Union([ - new TScalar(), - new TNull(), + ), + ]); + + case 6: // PDO::FETCH_BOUND - list + return new Union([ + Type::getListAtomic( + Type::getBool() + ), + ]); + + case 8: // PDO::FETCH_CLASS - list + return new Union([ + Type::getListAtomic( + new Union([ + $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject() + ]), + ), + ]); + + case 1: // PDO::FETCH_LAZY - list + // This actually returns a PDORow object, but that class is + // undocumented, and its attributes are all dynamic anyway + return new Union([ + Type::getListAtomic( + new Union([ + new TObject() + ]), + ), + ]); + + case 11: // PDO::FETCH_NAMED - list>> + return new Union([ + Type::getListAtomic( + new Union([ + new TArray([ + Type::getString(), + new Union([ + new TScalar(), + Type::getListAtomic(Type::getScalar()), + ]), ]), - ), - new TFalse(), - ]); - - case 5: // PDO::FETCH_OBJ - stdClass|false - return new Union([ - new TNamedObject('stdClass'), - new TFalse(), - ]); - } + ]), + ), + ]); + + case 3: // PDO::FETCH_NUM - list> + return new Union([ + Type::getListAtomic( + new Union([ + Type::getListAtomic( + new Union([ + new TScalar(), + new TNull(), + ]), + ), + ]), + ), + ]); + + case 5: // PDO::FETCH_OBJ - list + return new Union([ + Type::getListAtomic( + new Union([ + new TNamedObject('stdClass') + ]), + ), + ]); } return null; diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 3b656752bcf..760d93b65b5 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -503,14 +503,29 @@ class A { /** @var ?string */ public $a; } + class B extends A {} $db = new PDO("sqlite::memory:"); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $stmt = $db->prepare("select \"a\" as a"); $stmt->setFetchMode(PDO::FETCH_CLASS, A::class); $stmt->execute(); - /** @psalm-suppress MixedAssignment */ - $a = $stmt->fetch();', + $a = $stmt->fetch(); + $b = $stmt->fetchAll(); + $c = $stmt->fetch(PDO::FETCH_CLASS); + $d = $stmt->fetchAll(PDO::FETCH_CLASS); + $e = $stmt->fetchAll(PDO::FETCH_CLASS, B::class); + $f = $stmt->fetch(PDO::FETCH_ASSOC); + $g = $stmt->fetchAll(PDO::FETCH_ASSOC);', + 'assertions' => [ + '$a' => 'A|false', + '$b' => 'list', + '$c' => 'A|false', + '$d' => 'list', + '$e' => 'list', + '$f' => 'array|false', + '$g' => 'list>', + ], ], 'datePeriodConstructor' => [ 'code' => 'fetch(PDO::FETCH_ASSOC); }', ], + 'pdoStatementFetchAllAssoc' => [ + 'code' => '> */ + function fetch_assoc() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_ASSOC); + }', + ], 'pdoStatementFetchBoth' => [ 'code' => '|false */ @@ -627,6 +652,16 @@ function fetch_both() { return $sth->fetch(PDO::FETCH_BOTH); }', ], + 'pdoStatementFetchAllBoth' => [ + 'code' => '> */ + function fetch_both() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_BOTH); + }', + ], 'pdoStatementFetchBound' => [ 'code' => 'fetch(PDO::FETCH_BOUND); }', ], + 'pdoStatementFetchAllBound' => [ + 'code' => ' */ + function fetch_both() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_BOUND); + }', + ], 'pdoStatementFetchClass' => [ 'code' => 'fetch(PDO::FETCH_CLASS); }', ], + 'pdoStatementFetchAllClass' => [ + 'code' => ' */ + function fetch_class() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_CLASS); + }', + ], + 'pdoStatementFetchAllNamedClass' => [ + 'code' => ' */ + function fetch_class() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_CLASS, Foo::class); + }', + ], 'pdoStatementFetchLazy' => [ 'code' => 'fetch(PDO::FETCH_LAZY); }', ], + 'pdoStatementFetchAllLazy' => [ + 'code' => ' */ + function fetch_lazy() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_LAZY); + }', + ], 'pdoStatementFetchNamed' => [ 'code' => '>|false */ @@ -667,6 +744,16 @@ function fetch_named() { return $sth->fetch(PDO::FETCH_NAMED); }', ], + 'pdoStatementFetchAllNamed' => [ + 'code' => '>> */ + function fetch_named() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_NAMED); + }', + ], 'pdoStatementFetchNum' => [ 'code' => '|false */ @@ -677,6 +764,16 @@ function fetch_named() { return $sth->fetch(PDO::FETCH_NUM); }', ], + 'pdoStatementFetchAllNum' => [ + 'code' => '> */ + function fetch_named() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_NUM); + }', + ], 'pdoStatementFetchObj' => [ 'code' => 'fetch(PDO::FETCH_OBJ); }', ], + 'pdoStatementFetchAllObj' => [ + 'code' => ' */ + function fetch_named() : array { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_OBJ); + }', + ], 'dateTimeSecondArg' => [ 'code' => ' Date: Thu, 27 Jul 2023 01:39:21 +0200 Subject: [PATCH 50/56] #9974 extended return type detection for PDOStatement::fetchAll/fetch with FETCH_COLUMN and FETCH_KEY_PAIR --- .../PdoStatementReturnTypeProvider.php | 77 ++++++++++++++----- tests/MethodCallTest.php | 54 ++++++++++--- 2 files changed, 101 insertions(+), 30 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index 3f1addedc85..2c245caa157 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -116,6 +116,13 @@ private static function handleFetch(MethodReturnTypeProviderEvent $event): ?Unio case 6: // PDO::FETCH_BOUND - bool return Type::getBool(); + case 7: // PDO::FETCH_COLUMN - scalar|null|false + return new Union([ + new TScalar(), + new TNull(), + new TFalse(), + ]); + case 8: // PDO::FETCH_CLASS - object|false return new Union([ $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject(), @@ -130,18 +137,35 @@ private static function handleFetch(MethodReturnTypeProviderEvent $event): ?Unio new TFalse(), ]); - case 11: // PDO::FETCH_NAMED - array>|false + case 11: // PDO::FETCH_NAMED - array>|false return new Union([ new TArray([ Type::getString(), new Union([ new TScalar(), - Type::getListAtomic(Type::getScalar()), + new TNull(), + Type::getListAtomic( + new Union([ + new TScalar(), + new TNull(), + ]) + ), ]), ]), new TFalse(), ]); + case 12: // PDO::FETCH_KEY_PAIR - array + return new Union([ + new TArray([ + Type::getArrayKey(), + new Union([ + new TScalar(), + new TNull(), + ]), + ]), + ]); + case 3: // PDO::FETCH_NUM - list|false return new Union([ Type::getListAtomic( @@ -199,7 +223,7 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U new TNull(), ]), ]), - ]), + ]) ), ]); @@ -214,7 +238,7 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U new TNull(), ]), ]), - ]), + ]) ), ]); @@ -225,27 +249,27 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U ), ]); - case 8: // PDO::FETCH_CLASS - list + case 7: // PDO::FETCH_COLUMN - scalar|null|false return new Union([ Type::getListAtomic( new Union([ - $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject() - ]), + new TScalar(), + new TNull(), + new TFalse(), + ]) ), ]); - case 1: // PDO::FETCH_LAZY - list - // This actually returns a PDORow object, but that class is - // undocumented, and its attributes are all dynamic anyway + case 8: // PDO::FETCH_CLASS - list return new Union([ Type::getListAtomic( new Union([ - new TObject() - ]), + $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject() + ]) ), ]); - case 11: // PDO::FETCH_NAMED - list>> + case 11: // PDO::FETCH_NAMED - list>> return new Union([ Type::getListAtomic( new Union([ @@ -253,13 +277,30 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U Type::getString(), new Union([ new TScalar(), - Type::getListAtomic(Type::getScalar()), + new TNull(), + Type::getListAtomic( + new Union([ + new TScalar(), + new TNull(), + ]) + ), ]), ]), - ]), + ]) ), ]); + case 12: // PDO::FETCH_KEY_PAIR - array + return new Union([ + new TArray([ + Type::getArrayKey(), + new Union([ + new TScalar(), + new TNull(), + ]), + ]), + ]); + case 3: // PDO::FETCH_NUM - list> return new Union([ Type::getListAtomic( @@ -268,9 +309,9 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U new Union([ new TScalar(), new TNull(), - ]), + ]) ), - ]), + ]) ), ]); @@ -279,7 +320,7 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U Type::getListAtomic( new Union([ new TNamedObject('stdClass') - ]), + ]) ), ]); } diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 760d93b65b5..f15f55ab489 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -622,6 +622,46 @@ function foo(string $foo): string { return $foo; }', ], + 'pdoStatementFetchColumn' => [ + 'code' => 'prepare("SELECT 1"); + $sth->execute(); + return $sth->fetch(PDO::FETCH_COLUMN); + }', + ], + 'pdoStatementFetchAllColumn' => [ + 'code' => ' */ + function fetch_column() { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_COLUMN); + }', + ], + 'pdoStatementFetchKeyPair' => [ + 'code' => ' */ + function fetch_column() { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetch(PDO::FETCH_KEY_PAIR); + }', + ], + 'pdoStatementFetchAllKeyPair' => [ + 'code' => ' */ + function fetch_column() { + $p = new PDO("sqlite::memory:"); + $sth = $p->prepare("SELECT 1"); + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_KEY_PAIR); + }', + ], 'pdoStatementFetchAssoc' => [ 'code' => '|false */ @@ -724,19 +764,9 @@ function fetch_lazy() { return $sth->fetch(PDO::FETCH_LAZY); }', ], - 'pdoStatementFetchAllLazy' => [ - 'code' => ' */ - function fetch_lazy() : array { - $p = new PDO("sqlite::memory:"); - $sth = $p->prepare("SELECT 1"); - $sth->execute(); - return $sth->fetchAll(PDO::FETCH_LAZY); - }', - ], 'pdoStatementFetchNamed' => [ 'code' => '>|false */ + /** @return array>|false */ function fetch_named() { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); @@ -746,7 +776,7 @@ function fetch_named() { ], 'pdoStatementFetchAllNamed' => [ 'code' => '>> */ + /** @return list>> */ function fetch_named() : array { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); From 8a6774ba82f8e8b952b8d89f00ce56ac03a19fcb Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Thu, 27 Jul 2023 01:45:21 +0200 Subject: [PATCH 51/56] #9974 fixed coding styles --- .../PdoStatementReturnTypeProvider.php | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index 2c245caa157..11c49f42ee0 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -48,7 +48,7 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) return null; } - private static function handleSetFetchMode(MethodReturnTypeProviderEvent $event) + private static function handleSetFetchMode(MethodReturnTypeProviderEvent $event): ?Union { $source = $event->getSource(); $call_args = $event->getCallArgs(); @@ -148,7 +148,7 @@ private static function handleFetch(MethodReturnTypeProviderEvent $event): ?Unio new Union([ new TScalar(), new TNull(), - ]) + ]), ), ]), ]), @@ -223,7 +223,7 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U new TNull(), ]), ]), - ]) + ]), ), ]); @@ -238,14 +238,14 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U new TNull(), ]), ]), - ]) + ]), ), ]); case 6: // PDO::FETCH_BOUND - list return new Union([ Type::getListAtomic( - Type::getBool() + Type::getBool(), ), ]); @@ -256,7 +256,7 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U new TScalar(), new TNull(), new TFalse(), - ]) + ]), ), ]); @@ -264,8 +264,8 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U return new Union([ Type::getListAtomic( new Union([ - $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject() - ]) + $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject(), + ]), ), ]); @@ -282,11 +282,11 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U new Union([ new TScalar(), new TNull(), - ]) + ]), ), ]), ]), - ]) + ]), ), ]); @@ -309,9 +309,9 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U new Union([ new TScalar(), new TNull(), - ]) + ]), ), - ]) + ]), ), ]); @@ -319,8 +319,8 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U return new Union([ Type::getListAtomic( new Union([ - new TNamedObject('stdClass') - ]) + new TNamedObject('stdClass'), + ]), ), ]); } From 3d525cbd0eef1a4f57e90788228acfe059b0ab5e Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Thu, 27 Jul 2023 01:48:00 +0200 Subject: [PATCH 52/56] #9974 cleanup --- .../ReturnTypeProvider/PdoStatementReturnTypeProvider.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index 11c49f42ee0..e4236b24e5a 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -54,6 +54,9 @@ private static function handleSetFetchMode(MethodReturnTypeProviderEvent $event) $call_args = $event->getCallArgs(); $context = $event->getContext(); + $context->references_in_scope['fetch_mode'] = null; + $context->references_in_scope['fetch_class'] = null; + if (isset($call_args[0]) && ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value)) && $first_arg_type->isSingleIntLiteral() From 13d53ecbf10c31492e17bd87fda662e43ed4a920 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Thu, 27 Jul 2023 01:59:06 +0200 Subject: [PATCH 53/56] #9974 cleanup --- .../ReturnTypeProvider/PdoStatementReturnTypeProvider.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index e4236b24e5a..a1c5963d798 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -54,14 +54,14 @@ private static function handleSetFetchMode(MethodReturnTypeProviderEvent $event) $call_args = $event->getCallArgs(); $context = $event->getContext(); - $context->references_in_scope['fetch_mode'] = null; - $context->references_in_scope['fetch_class'] = null; + unset($context->references_in_scope['fetch_mode']); + unset($context->references_in_scope['fetch_class']); if (isset($call_args[0]) && ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value)) && $first_arg_type->isSingleIntLiteral() ) { - $context->references_in_scope['fetch_mode'] = $first_arg_type->getSingleIntLiteral()->value; + $context->references_in_scope['fetch_mode'] = (string) $first_arg_type->getSingleIntLiteral()->value; } if (isset($call_args[1]) From d600705b0c8ab0424fff0be3cb9872fc97778519 Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Thu, 27 Jul 2023 15:53:05 +0200 Subject: [PATCH 54/56] #9974 removed references_in_scope for setFetchMode --- .../PdoStatementReturnTypeProvider.php | 44 ++----------------- tests/MethodCallTest.php | 23 +++++++--- 2 files changed, 22 insertions(+), 45 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index a1c5963d798..593be23ce1d 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -33,10 +33,6 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) return null; } - if ($method_name_lowercase === 'setfetchmode') { - return self::handleSetFetchMode($event); - } - if ($method_name_lowercase === 'fetch') { return self::handleFetch($event); } @@ -48,39 +44,11 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) return null; } - private static function handleSetFetchMode(MethodReturnTypeProviderEvent $event): ?Union - { - $source = $event->getSource(); - $call_args = $event->getCallArgs(); - $context = $event->getContext(); - - unset($context->references_in_scope['fetch_mode']); - unset($context->references_in_scope['fetch_class']); - - if (isset($call_args[0]) - && ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value)) - && $first_arg_type->isSingleIntLiteral() - ) { - $context->references_in_scope['fetch_mode'] = (string) $first_arg_type->getSingleIntLiteral()->value; - } - - if (isset($call_args[1]) - && ($second_arg_type = $source->getNodeTypeProvider()->getType($call_args[1]->value)) - && $second_arg_type->isSingleStringLiteral() - ) { - $context->references_in_scope['fetch_class'] = $second_arg_type->getSingleStringLiteral()->value; - } - - return null; - } - private static function handleFetch(MethodReturnTypeProviderEvent $event): ?Union { $source = $event->getSource(); $call_args = $event->getCallArgs(); - $context = $event->getContext(); - - $fetch_mode = $context->references_in_scope['fetch_mode'] ?? null; + $fetch_mode = 0; if (isset($call_args[0]) && ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value)) @@ -89,8 +57,6 @@ private static function handleFetch(MethodReturnTypeProviderEvent $event): ?Unio $fetch_mode = $first_arg_type->getSingleIntLiteral()->value; } - $fetch_class_name = $context->references_in_scope['fetch_class'] ?? null; - switch ($fetch_mode) { case 2: // PDO::FETCH_ASSOC - array|false return new Union([ @@ -128,7 +94,7 @@ private static function handleFetch(MethodReturnTypeProviderEvent $event): ?Unio case 8: // PDO::FETCH_CLASS - object|false return new Union([ - $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject(), + new TObject(), new TFalse(), ]); @@ -194,9 +160,7 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U { $source = $event->getSource(); $call_args = $event->getCallArgs(); - $context = $event->getContext(); - - $fetch_mode = $context->references_in_scope['fetch_mode'] ?? null; + $fetch_mode = 0; if (isset($call_args[0]) && ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value)) @@ -205,7 +169,7 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U $fetch_mode = $first_arg_type->getSingleIntLiteral()->value; } - $fetch_class_name = $context->references_in_scope['fetch_class'] ?? null; + $fetch_class_name = null; if (isset($call_args[1]) && ($second_arg_type = $source->getNodeTypeProvider()->getType($call_args[1]->value)) diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index f15f55ab489..4b8ce13ea94 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -509,22 +509,35 @@ class B extends A {} $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $stmt = $db->prepare("select \"a\" as a"); $stmt->setFetchMode(PDO::FETCH_CLASS, A::class); + $stmt2 = $db->prepare("select \"a\" as a"); + $stmt2->setFetchMode(PDO::FETCH_ASSOC); $stmt->execute(); + $stmt2->execute(); + /** @psalm-suppress MixedAssignment */ $a = $stmt->fetch(); $b = $stmt->fetchAll(); $c = $stmt->fetch(PDO::FETCH_CLASS); $d = $stmt->fetchAll(PDO::FETCH_CLASS); $e = $stmt->fetchAll(PDO::FETCH_CLASS, B::class); $f = $stmt->fetch(PDO::FETCH_ASSOC); - $g = $stmt->fetchAll(PDO::FETCH_ASSOC);', + $g = $stmt->fetchAll(PDO::FETCH_ASSOC); + /** @psalm-suppress MixedAssignment */ + $h = $stmt2->fetch(); + $i = $stmt2->fetchAll(); + $j = $stmt2->fetch(PDO::FETCH_BOTH); + $k = $stmt2->fetchAll(PDO::FETCH_BOTH);', 'assertions' => [ - '$a' => 'A|false', - '$b' => 'list', - '$c' => 'A|false', - '$d' => 'list', + '$a' => 'mixed', + '$b' => 'array|false', + '$c' => 'false|object', + '$d' => 'list', '$e' => 'list', '$f' => 'array|false', '$g' => 'list>', + '$h' => 'mixed', + '$i' => 'array|false', + '$j' => 'array|false', + '$k' => 'list>', ], ], 'datePeriodConstructor' => [ From ee505259aab463c39b8b802c6db27a4f6c04c94f Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Thu, 27 Jul 2023 15:59:31 +0200 Subject: [PATCH 55/56] #9974 extended test with PDO::ATTR_DEFAULT_FETCH_MODE for future implementation --- tests/MethodCallTest.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 4b8ce13ea94..f6cb3e75099 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -507,10 +507,13 @@ class B extends A {} $db = new PDO("sqlite::memory:"); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $stmt = $db->prepare("select \"a\" as a"); $stmt->setFetchMode(PDO::FETCH_CLASS, A::class); $stmt2 = $db->prepare("select \"a\" as a"); $stmt2->setFetchMode(PDO::FETCH_ASSOC); + $stmt3 = $db->prepare("select \"a\" as a"); + $stmt3->setFetchMode(PDO::ATTR_DEFAULT_FETCH_MODE); $stmt->execute(); $stmt2->execute(); /** @psalm-suppress MixedAssignment */ @@ -525,7 +528,9 @@ class B extends A {} $h = $stmt2->fetch(); $i = $stmt2->fetchAll(); $j = $stmt2->fetch(PDO::FETCH_BOTH); - $k = $stmt2->fetchAll(PDO::FETCH_BOTH);', + $k = $stmt2->fetchAll(PDO::FETCH_BOTH); + /** @psalm-suppress MixedAssignment */ + $l = $stmt3->fetch();', 'assertions' => [ '$a' => 'mixed', '$b' => 'array|false', @@ -538,6 +543,7 @@ class B extends A {} '$i' => 'array|false', '$j' => 'array|false', '$k' => 'list>', + '$l' => 'mixed', ], ], 'datePeriodConstructor' => [ From 88ddb8976305ceae7b6cdb609270393195b8435e Mon Sep 17 00:00:00 2001 From: Thomas Bley Date: Thu, 27 Jul 2023 16:11:32 +0200 Subject: [PATCH 56/56] #9974 fixed return type for FETCH_COLUMN --- .../ReturnTypeProvider/PdoStatementReturnTypeProvider.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index 593be23ce1d..3edc2342bd1 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -216,13 +216,12 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U ), ]); - case 7: // PDO::FETCH_COLUMN - scalar|null|false + case 7: // PDO::FETCH_COLUMN - list return new Union([ Type::getListAtomic( new Union([ new TScalar(), new TNull(), - new TFalse(), ]), ), ]);