diff --git a/.devcontainer/library-scripts/node-debian.sh b/.devcontainer/library-scripts/node-debian.sh index f35e77fe1b..d230a14e82 100644 --- a/.devcontainer/library-scripts/node-debian.sh +++ b/.devcontainer/library-scripts/node-debian.sh @@ -18,7 +18,7 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi -# Treat a user name of "none" or non-existant user as root +# Treat a user name of "none" or non-existent user as root if [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then USERNAME=root fi diff --git a/.github/renovate.json b/.github/renovate.json index ddd4bc50c7..8bafa45fd1 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -3,8 +3,10 @@ "config:base", "schedule:weekly" ], + "dependencyDashboard": true, "rangeStrategy": "update-lockfile", "rebaseWhen": "conflicted", + "baseBranches": ["1.11.x"], "packageRules": [ { "matchPackagePatterns": ["*"], @@ -13,7 +15,7 @@ { "matchPaths": ["+(composer.json)"], "enabled": true, - "groupName": "root-composer" + "matchBaseBranches": ["1.11.x"] }, { "matchPaths": ["build-cs/**"], @@ -25,6 +27,16 @@ "enabled": true, "groupName": "apigen" }, + { + "matchPaths": ["issue-bot/**"], + "enabled": true, + "groupName": "issue-bot" + }, + { + "matchPaths": ["changelog-generator/**"], + "enabled": true, + "groupName": "changelog-generator" + }, { "matchPaths": ["compiler/**"], "enabled": true, diff --git a/.github/workflows/apiref.yml b/.github/workflows/apiref.yml index e0218169ad..07e59496e9 100644 --- a/.github/workflows/apiref.yml +++ b/.github/workflows/apiref.yml @@ -3,9 +3,10 @@ name: "API Reference" on: + workflow_dispatch: push: branches: - - "1.9.x" + - "1.11.x" paths: - 'src/**' - 'composer.lock' @@ -25,7 +26,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -44,7 +45,7 @@ jobs: run: "apigen/vendor/bin/apigen -c apigen/apigen.neon --output docs -- src vendor/nikic/php-parser vendor/ondrejmirtes/better-reflection vendor/phpstan/phpdoc-parser" - name: "Upload docs" - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: docs path: docs @@ -57,12 +58,12 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Install Node" - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: "16" - name: "Download docs" - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: docs path: docs @@ -88,7 +89,7 @@ jobs: AWS_ACCESS_KEY_ID: ${{ secrets.APIREF_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.APIREF_AWS_SECRET_ACCESS_KEY }} - - uses: peter-evans/repository-dispatch@v2 + - uses: peter-evans/repository-dispatch@v3 with: token: ${{ secrets.PHPSTAN_BOT_TOKEN }} repository: "phpstan/phpstan" diff --git a/.github/workflows/backward-compatibility.yml b/.github/workflows/backward-compatibility.yml index 7b8844e271..5411d5b9e3 100644 --- a/.github/workflows/backward-compatibility.yml +++ b/.github/workflows/backward-compatibility.yml @@ -6,13 +6,13 @@ on: pull_request: push: branches: - - "1.8.x" + - "1.11.x" paths: - 'src/**' - '.github/workflows/backward-compatibility.yml' env: - COMPOSER_ROOT_VERSION: "1.8.x-dev" + COMPOSER_ROOT_VERSION: "1.11.x-dev" concurrency: group: bc-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches @@ -27,7 +27,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -44,7 +44,7 @@ jobs: run: | composer global config minimum-stability dev composer global config prefer-stable true - composer global require --dev ondrejmirtes/backward-compatibility-check:^7.3.0 + composer global require --dev ondrejmirtes/backward-compatibility-check:^7.3.0.1 - name: "Check" run: "$(composer global config bin-dir --absolute)/roave-backward-compatibility-check" diff --git a/.github/workflows/build-issue-bot.yml b/.github/workflows/build-issue-bot.yml new file mode 100644 index 0000000000..4769c56165 --- /dev/null +++ b/.github/workflows/build-issue-bot.yml @@ -0,0 +1,57 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Build Issue Bot" + +on: + pull_request: + paths: + - 'issue-bot/**' + - '.github/workflows/build-issue-bot.yml' + push: + branches: + - "1.11.x" + paths: + - 'issue-bot/**' + - '.github/workflows/build-issue-bot.yml' + +env: + COMPOSER_ROOT_VERSION: "1.11.x-dev" + +concurrency: + group: build-issue-bot-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true + +jobs: + build-issue-bot: + name: "Build Issue Bot" + + runs-on: "ubuntu-latest" + timeout-minutes: 60 + + strategy: + fail-fast: false + matrix: + script: + - "../bin/phpstan" + - "vendor/bin/phpunit" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.3" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Install Issue Bot dependencies" + working-directory: "issue-bot" + run: "composer install --no-interaction --no-progress" + + - name: "Tests" + working-directory: "issue-bot" + run: ${{ matrix.script }} diff --git a/.github/workflows/changelog-generator.yml b/.github/workflows/changelog-generator.yml index 105b024a1e..1681e3a6e0 100644 --- a/.github/workflows/changelog-generator.yml +++ b/.github/workflows/changelog-generator.yml @@ -9,11 +9,14 @@ on: - '.github/workflows/changelog-generator.yml' push: branches: - - "1.8.x" + - "1.11.x" paths: - 'changelog-generator/**' - '.github/workflows/changelog-generator.yml' +env: + COMPOSER_ROOT_VERSION: "1.11.x-dev" + concurrency: group: changelog-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches cancel-in-progress: true @@ -27,7 +30,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/.github/workflows/checksum-phar.yml b/.github/workflows/checksum-phar.yml new file mode 100644 index 0000000000..1558436ad4 --- /dev/null +++ b/.github/workflows/checksum-phar.yml @@ -0,0 +1,127 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +# This workflow checks that PHAR checksum changes only when it's supposed to +# It should stay the same when the PHAR contents do not change + +name: "Check PHAR checksum" + +on: + pull_request: + paths: + - 'compiler/**' + - '.github/workflows/checksum-phar.yml' + push: + branches: + - "1.11.x" + paths: + - 'compiler/**' + - '.github/workflows/checksum-phar.yml' + +env: + COMPOSER_ROOT_VERSION: "1.11.x-dev" + +concurrency: + group: checksum-phar-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true + +jobs: + check-phar-checksum: + name: "Check PHAR checksum" + + runs-on: "ubuntu-latest" + timeout-minutes: 60 + + steps: + - name: "Checkout phpstan-dist" + uses: actions/checkout@v4 + with: + repository: phpstan/phpstan + path: phpstan-dist + ref: 1.11.x + + - name: "Get info" + id: info + working-directory: phpstan-dist + run: | + echo "checksum=$(head -n 1 .phar-checksum)" >> $GITHUB_OUTPUT + echo "commit=$(tail -n 1 .phar-checksum)" >> $GITHUB_OUTPUT + + - name: "Delete phpstan-dist" + run: "rm -r phpstan-dist" + + - name: "Checkout" + uses: actions/checkout@v4 + with: + ref: ${{ steps.info.outputs.commit }} + + - name: "Checkout latest PHAR compiler" + uses: actions/checkout@v4 + with: + path: phpstan-src + ref: ${{ github.sha }} + + - name: "Delete old compiler" + run: "rm -r compiler" + + - name: "Move new compiler" + run: "mv phpstan-src/compiler/ ." + + - name: "Delete phpstan-src" + run: "rm -r phpstan-src" + + - name: "Change and commit README.md" + run: | + echo Testing > README.md + git config --global user.name "phpstan-bot" + git config --global user.email "ondrej+phpstanbot@mirtes.cz" + git commit -a -m 'Changed README' + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.1" + extensions: mbstring, intl + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Install compiler dependencies" + run: "composer install --no-interaction --no-progress --working-dir=compiler" + + # same steps as in phar.yml + + - name: "Prepare for PHAR compilation" + working-directory: "compiler" + run: "php bin/prepare" + + - name: "Set autoloader suffix" + run: "composer config autoloader-suffix PHPStanChecksum" + + - name: "Composer dump" + run: "composer install --no-interaction --no-progress" + env: + COMPOSER_ROOT_VERSION: "1.11.x-dev" + + - name: "Compile PHAR for checksum" + working-directory: "compiler/build" + run: "php box.phar compile --no-parallel" + env: + PHAR_CHECKSUM: "1" + COMPOSER_ROOT_VERSION: "1.11.x-dev" + + - name: "Re-sign PHAR" + run: "php compiler/build/resign.php tmp/phpstan.phar" + + - name: "Unset autoloader suffix" + run: "composer config autoloader-suffix --unset" + + - name: "Save checksum" + id: "new_checksum" + run: echo "md5=$(md5sum tmp/phpstan.phar | cut -d' ' -f1)" >> $GITHUB_OUTPUT + + - name: "Assert checksum" + run: | + checksum=${{ steps.info.outputs.checksum }} + new_checksum=${{ steps.new_checksum.outputs.md5 }} + [[ "$checksum" == "$new_checksum" ]]; diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index 6b664a48e8..a853501487 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -21,10 +21,10 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - token: ${{ secrets.PAT }} + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - name: 'Get Previous tag' id: previoustag diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 852afb4c9b..eef9af0cbb 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -8,16 +8,18 @@ on: - 'compiler/**' - 'apigen/**' - 'changelog-generator/**' + - 'issue-bot/**' push: branches: - - "1.8.x" + - "1.11.x" paths-ignore: - 'compiler/**' - 'apigen/**' - 'changelog-generator/**' + - 'issue-bot/**' env: - COMPOSER_ROOT_VERSION: "1.8.x-dev" + COMPOSER_ROOT_VERSION: "1.11.x-dev" concurrency: group: e2e-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches @@ -37,7 +39,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -102,10 +104,90 @@ jobs: mv src/Bar.php.orig src/Bar.php echo -n > phpstan-baseline.neon ../../bin/phpstan -vvv + - script: | + cd e2e/result-cache-5 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Baz.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Baz.php.orig src/Baz.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/result-cache-6 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Baz.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Baz.php.orig src/Baz.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/result-cache-7 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Bar.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Bar.php.orig src/Bar.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/bug10449 + ../../bin/phpstan analyze + git apply patch.diff + rm phpstan-baseline.neon + mv after-phpstan-baseline.neon phpstan-baseline.neon + ../../bin/phpstan analyze -vvv + - script: | + cd e2e/bug10449b + ../../bin/phpstan analyze + git apply patch.diff + rm phpstan-baseline.neon + mv after-phpstan-baseline.neon phpstan-baseline.neon + ../../bin/phpstan analyze -vvv + - script: | + cd e2e/bug-9622 + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Foo.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Foo.php.orig src/Foo.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/bug-9622-trait + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + patch -b src/Foo.php < patch-1.patch + cat baseline-1.neon > phpstan-baseline.neon + ../../bin/phpstan -vvv + mv src/Foo.php.orig src/Foo.php + echo -n > phpstan-baseline.neon + ../../bin/phpstan -vvv + - script: | + cd e2e/env-parameter + export PHPSTAN_SCOPE_CLASS=MyTestScope + ACTUAL=$(../../bin/phpstan dump-parameters -c phpstan.neon --json -l 9 | jq --raw-output '.scopeClass') + [[ "$ACTUAL" == "MyTestScope" ]]; + - script: | + cd e2e/result-cache-8 + composer install + ../../bin/phpstan + echo -en '\n' >> build/CustomRule.php + OUTPUT=$(../../bin/phpstan 2>&1) + grep 'Result cache might not behave correctly' <<< "$OUTPUT" + grep 'ResultCache8E2E\\CustomRule' <<< "$OUTPUT" + - script: | + cd e2e/env-int-key + env 1=1 ../../bin/phpstan analyse test.php steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -132,19 +214,33 @@ jobs: strategy: matrix: include: - - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/timecop.php tests/e2e/data/timecop.php" + - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/timecop.php -c tests/e2e/data/empty.neon tests/e2e/data/timecop.php" tools: "pecl" extensions: "timecop-beta" - - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/soap.php tests/e2e/data/soap.php" + - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/soap.php -c tests/e2e/data/empty.neon tests/e2e/data/soap.php" extensions: "soap" - - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/soap.php tests/e2e/data/soap.php" + - script: "bin/phpstan analyse -l 8 -a tests/e2e/data/soap.php -c tests/e2e/data/empty.neon tests/e2e/data/soap.php" extensions: "" - script: "bin/phpstan analyse -l 8 tests/e2e/anon-class/Granularity.php" extensions: "" + - script: "bin/phpstan analyse -l 8 e2e/phpstan-phpunit-190/test.php -c e2e/phpstan-phpunit-190/test.neon" + extensions: "" + - script: "bin/phpstan analyse e2e/only-files-not-analysed-trait/src -c e2e/only-files-not-analysed-trait/ignore.neon" + extensions: "" + - script: "bin/phpstan analyse e2e/only-files-not-analysed-trait/src/Foo.php e2e/only-files-not-analysed-trait/src/BarTrait.php -c e2e/only-files-not-analysed-trait/no-ignore.neon" + extensions: "" + - script: | + cd e2e/baseline-uninit-prop-trait + ../../bin/phpstan analyse --debug --configuration test-no-baseline.neon --generate-baseline test-baseline.neon + ../../bin/phpstan analyse --debug --configuration test.neon + - script: | + cd e2e/baseline-uninit-prop-trait + ../../bin/phpstan analyse --configuration test-no-baseline.neon --generate-baseline test-baseline.neon + ../../bin/phpstan analyse --configuration test.neon steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/.github/workflows/issue-bot.yml b/.github/workflows/issue-bot.yml new file mode 100644 index 0000000000..aacbe1cd3b --- /dev/null +++ b/.github/workflows/issue-bot.yml @@ -0,0 +1,175 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Issue bot" + +on: + workflow_dispatch: + pull_request: + paths-ignore: + - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + push: + branches: + - "1.11.x" + paths-ignore: + - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + +env: + COMPOSER_ROOT_VERSION: "1.11.x-dev" + +concurrency: + group: run-issue-bot-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true + +jobs: + download: + name: "Download data" + + runs-on: "ubuntu-latest" + + outputs: + matrix: ${{ steps.download-data.outputs.matrix }} + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.3" + + - name: "Install Issue Bot dependencies" + working-directory: "issue-bot" + run: "composer install --no-interaction --no-progress" + + - name: "Cache downloads" + uses: actions/cache@v4 + with: + path: ./issue-bot/tmp + key: "issue-bot-download-v6-${{ github.run_id }}" + restore-keys: | + issue-bot-download-v6- + + - name: "Download data" + working-directory: "issue-bot" + id: download-data + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + run: echo "matrix=$(./console.php download)" >> $GITHUB_OUTPUT + + + - uses: actions/upload-artifact@v4 + with: + name: playground-cache + path: issue-bot/tmp/playgroundCache.tmp + + - uses: actions/upload-artifact@v4 + with: + name: issue-cache + path: issue-bot/tmp/issueCache.tmp + + analyse: + name: "Analyse" + needs: download + + runs-on: "ubuntu-latest" + + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.download.outputs.matrix) }} + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.3" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress --no-dev" + + - name: "Install Issue Bot dependencies" + working-directory: "issue-bot" + run: "composer install --no-interaction --no-progress" + + - uses: Wandalen/wretry.action@v3.4.0 + with: + action: actions/download-artifact@v4 + with: | + name: playground-cache + path: issue-bot/tmp + attempt_limit: 5 + attempt_delay: 1000 + + - name: "Run PHPStan" + working-directory: "issue-bot" + timeout-minutes: 5 + run: ./console.php run ${{ matrix.phpVersion }} ${{ matrix.playgroundExamples }} + + - uses: actions/upload-artifact@v4 + with: + name: results-${{ matrix.phpVersion }}-${{ matrix.chunkNumber }} + path: issue-bot/tmp/results-${{ matrix.phpVersion }}-*.tmp + + evaluate: + name: "Evaluate results" + needs: analyse + + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.3" + + - name: "Install Issue Bot dependencies" + working-directory: "issue-bot" + run: "composer install --no-interaction --no-progress" + + - uses: actions/download-artifact@v4 + with: + name: playground-cache + path: issue-bot/tmp + + - uses: actions/download-artifact@v4 + with: + name: issue-cache + path: issue-bot/tmp + + - uses: actions/download-artifact@v4 + with: + pattern: results-* + merge-multiple: true + path: issue-bot/tmp + + - name: "List tmp" + run: "ls -lA issue-bot/tmp" + + - name: "Evaluate results - pull request" + working-directory: "issue-bot" + if: github.event_name == 'pull_request' + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + run: echo "$(./console.php evaluate)" >> $GITHUB_STEP_SUMMARY + + - name: "Evaluate results - push" + working-directory: "issue-bot" + if: "github.repository_owner == 'phpstan' && github.ref == 'refs/heads/1.11.x'" + env: + GITHUB_PAT: ${{ secrets.PHPSTAN_BOT_TOKEN }} + PHPSTAN_SRC_COMMIT_BEFORE: ${{ github.event.before }} + PHPSTAN_SRC_COMMIT_AFTER: ${{ github.event.after }} + run: echo "$(./console.php evaluate --post-comments)" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e04e551a97..14fbfbc500 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,10 +6,10 @@ on: pull_request: push: branches: - - "1.8.x" + - "1.11.x" env: - COMPOSER_ROOT_VERSION: "1.8.x-dev" + COMPOSER_ROOT_VERSION: "1.11.x-dev" concurrency: group: lint-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches @@ -31,10 +31,11 @@ jobs: - "8.0" - "8.1" - "8.2" + - "8.3" steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -48,39 +49,9 @@ jobs: - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - name: "Install PHP for code transform" - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' - uses: "shivammathur/setup-php@v2" - with: - coverage: "none" - php-version: 8.1 - extensions: mbstring, intl - - - name: "Rector downgrade cache key" - id: rector-cache-key - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' - run: | - echo "::set-output name=sha::$(php build/rector-cache-files-hash.php)" - - - name: "Rector downgrade cache" - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' - uses: actions/cache@v3 - with: - path: ./tmp/rectorCache.php - key: "rector-v2-lint-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-${{ steps.rector-cache-key.outputs.sha }}" - restore-keys: | - rector-v2-lint-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}- - - name: "Transform source code" - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' - run: "build/transform-source ${{ matrix.php-version }}" - - - name: "Reinstall matrix PHP version" - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' - uses: "shivammathur/setup-php@v2" - with: - coverage: "none" - php-version: "${{ matrix.php-version }}" + if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3' + run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}" - name: "Lint" run: "make lint" @@ -93,7 +64,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -121,7 +92,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -132,5 +103,27 @@ jobs: - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - name: "Composer Require Checker" - run: "make composer-require-checker" + - name: "Composer Dependency Analyser" + run: "make composer-dependency-analyser" + + name-collision: + name: "Name Collision Detector" + + runs-on: "ubuntu-latest" + timeout-minutes: 60 + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.3" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Name Collision Detector" + run: "make name-collision" diff --git a/.github/workflows/merge-maintained-branch.yml b/.github/workflows/merge-maintained-branch.yml index 9a2b9772c9..7c7cb8f853 100644 --- a/.github/workflows/merge-maintained-branch.yml +++ b/.github/workflows/merge-maintained-branch.yml @@ -5,19 +5,20 @@ name: Merge maintained branch on: push: branches: - - "1.8.x" + - "1.10.x" jobs: merge: name: Merge branch + if: github.repository_owner == 'phpstan' runs-on: ubuntu-latest steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Merge branch" - uses: everlytic/branch-merge@1.1.4 + uses: everlytic/branch-merge@1.1.5 with: github_token: "${{ secrets.PHPSTAN_BOT_TOKEN }}" source_ref: ${{ github.ref }} - target_branch: '1.9.x' + target_branch: '1.11.x' commit_message_template: 'Merge branch {source_ref} into {target_branch}' diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index dfcafa819c..af601aa93b 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -6,9 +6,9 @@ on: pull_request: push: branches: - - "1.8.x" + - "1.11.x" tags: - - '1.8.*' + - '1.11.*' concurrency: group: phar-${{ github.ref }} # will be canceled on subsequent pushes in both branches and pull requests @@ -26,7 +26,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -51,19 +51,6 @@ jobs: working-directory: "compiler" run: "../bin/phpstan analyse -l 8 src tests" - - name: "Rector downgrade cache key" - id: rector-cache-key - run: | - echo "::set-output name=sha::$(php build/rector-cache-files-hash.php)" - - - name: "Rector downgrade cache" - uses: actions/cache@v3 - with: - path: ./tmp/rectorCache.php - key: "rector-v2-phar-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ steps.rector-cache-key.outputs.sha }}" - restore-keys: | - rector-v2-phar-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}- - - name: "Prepare for PHAR compilation" working-directory: "compiler" run: "php bin/prepare" @@ -72,7 +59,7 @@ jobs: working-directory: "compiler/build" run: "php box.phar compile --no-parallel" - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: phar-file path: tmp/phpstan.phar @@ -90,14 +77,14 @@ jobs: - name: "Composer dump" run: "composer install --no-interaction --no-progress" env: - COMPOSER_ROOT_VERSION: "1.8.x-dev" + COMPOSER_ROOT_VERSION: "1.11.x-dev" - name: "Compile PHAR for checksum" working-directory: "compiler/build" run: "php box.phar compile --no-parallel" env: PHAR_CHECKSUM: "1" - COMPOSER_ROOT_VERSION: "1.8.x-dev" + COMPOSER_ROOT_VERSION: "1.11.x-dev" - name: "Re-sign PHAR" run: "php compiler/build/resign.php tmp/phpstan.phar" @@ -107,9 +94,9 @@ jobs: - name: "Save checksum" id: "checksum" - run: echo "::set-output name=md5::$(md5sum tmp/phpstan.phar | cut -d' ' -f1)" + run: echo "md5=$(md5sum tmp/phpstan.phar | cut -d' ' -f1)" >> $GITHUB_OUTPUT - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: phar-file-checksum path: tmp/phpstan.phar @@ -120,54 +107,60 @@ jobs: integration-tests: if: github.event_name == 'pull_request' needs: compiler-tests - uses: phpstan/phpstan/.github/workflows/integration-tests.yml@1.8.x + uses: phpstan/phpstan/.github/workflows/integration-tests.yml@1.11.x with: - ref: 1.8.x + ref: 1.11.x phar-checksum: ${{needs.compiler-tests.outputs.checksum}} extension-tests: if: github.event_name == 'pull_request' needs: compiler-tests - uses: phpstan/phpstan/.github/workflows/extension-tests.yml@1.8.x + uses: phpstan/phpstan/.github/workflows/extension-tests.yml@1.11.x with: - ref: 1.8.x + ref: 1.11.x phar-checksum: ${{needs.compiler-tests.outputs.checksum}} other-tests: if: github.event_name == 'pull_request' needs: compiler-tests - uses: phpstan/phpstan/.github/workflows/other-tests.yml@1.8.x + uses: phpstan/phpstan/.github/workflows/other-tests.yml@1.11.x with: - ref: 1.8.x + ref: 1.11.x phar-checksum: ${{needs.compiler-tests.outputs.checksum}} commit: name: "Commit PHAR" - if: "github.repository_owner == 'phpstan' && (github.ref == 'refs/heads/1.8.x' || startsWith(github.ref, 'refs/tags/'))" + if: "github.repository_owner == 'phpstan' && (github.ref == 'refs/heads/1.11.x' || startsWith(github.ref, 'refs/tags/'))" needs: compiler-tests runs-on: "ubuntu-latest" timeout-minutes: 60 steps: - - name: "Configure GPG signing key" - run: echo "$GPG_SIGNING_KEY" | base64 --decode | gpg --import --no-tty --batch --yes - env: - GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} + - + name: Import GPG key + id: import-gpg + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PHPSTANBOT_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PHPSTANBOT_KEY_PASSPHRASE }} + git_config_global: true + git_user_signingkey: true + git_commit_gpgsign: true - name: "Checkout phpstan-dist" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: phpstan/phpstan path: phpstan-dist - token: ${{ secrets.PAT }} - ref: 1.8.x + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + ref: 1.11.x - name: "Get previous pushed dist commit" id: previous-commit working-directory: phpstan-dist - run: echo ::set-output name=sha::$(sed -n '2p' .phar-checksum) + run: echo "sha=$(sed -n '2p' .phar-checksum)" >> $GITHUB_OUTPUT - name: "Checkout phpstan-src" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 path: phpstan-src @@ -175,7 +168,15 @@ jobs: - name: "Get Git log" id: git-log working-directory: phpstan-src - run: echo ::set-output name=log::$(git log ${{ steps.previous-commit.outputs.sha }}..${{ github.event.after }} --reverse --pretty='%H %s' | sed -e 's/^/https:\/\/github.com\/phpstan\/phpstan-src\/commit\//') + run: | + echo "log<> $GITHUB_OUTPUT + echo "$(git log ${{ steps.previous-commit.outputs.sha }}..${{ github.event.after }} --reverse --pretty='https://github.com/phpstan/phpstan-src/commit/%H %s')" >> $GITHUB_OUTPUT + echo 'MESSAGE' >> $GITHUB_OUTPUT + + - name: "Get short phpstan-src SHA" + id: short-src-sha + working-directory: phpstan-src + run: echo "sha=$(git rev-parse --short=7 HEAD)" >> $GITHUB_OUTPUT - name: "Check PHAR checksum" id: checksum-difference @@ -183,13 +184,13 @@ jobs: run: | checksum=${{needs.compiler-tests.outputs.checksum}} if [[ $(head -n 1 .phar-checksum) != "$checksum" ]]; then - echo "::set-output name=result::different + echo "result=different" >> $GITHUB_OUTPUT else - echo "::set-output name=result::same + echo "result=same" >> $GITHUB_OUTPUT fi - name: "Download phpstan.phar" - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: phar-file @@ -208,36 +209,38 @@ jobs: working-directory: phpstan-dist run: rm phpstan.phar.asc && gpg --command-fd 0 --pinentry-mode loopback -u "$GPG_ID" --batch --detach-sign --armor --output phpstan.phar.asc phpstan.phar env: - GPG_ID: ${{ secrets.GPG_ID }} + GPG_ID: ${{ steps.import-gpg.outputs.fingerprint }} - name: "Verify PHAR" working-directory: phpstan-dist run: "gpg --verify phpstan.phar.asc" - - name: "Set Git signing key" - working-directory: phpstan-dist - run: git config user.signingkey "$GPG_ID" - env: - GPG_ID: ${{ secrets.GPG_ID }} + - name: "Install lucky_commit" + uses: baptiste0928/cargo-install@v3 + with: + crate: lucky_commit + args: --no-default-features - name: "Commit PHAR - development" if: "!startsWith(github.ref, 'refs/tags/') && steps.checksum-difference.outputs.result == 'different'" - uses: stefanzweifel/git-auto-commit-action@v4 - with: - commit_user_name: "Ondrej Mirtes" - commit_user_email: "ondrej@mirtes.cz" - commit_author: "Ondrej Mirtes " - commit_options: "--gpg-sign" - repository: phpstan-dist - commit_message: "Updated PHPStan to commit ${{ github.event.after }}\n\n${{ steps.git-log.outputs.log }}" + working-directory: phpstan-dist + env: + INPUT_LOG: ${{ steps.git-log.outputs.log }} + run: | + git config --global user.name "phpstan-bot" + git config --global user.email "ondrej+phpstanbot@mirtes.cz" + git add . + git commit --gpg-sign -m "Updated PHPStan to commit ${{ github.event.after }}" -m "$INPUT_LOG" --author "phpstan-bot " + lucky_commit ${{ steps.short-src-sha.outputs.sha }} + git push - name: "Commit PHAR - tag" if: "startsWith(github.ref, 'refs/tags/')" - uses: stefanzweifel/git-auto-commit-action@v4 + uses: stefanzweifel/git-auto-commit-action@v5 with: - commit_user_name: "Ondrej Mirtes" - commit_user_email: "ondrej@mirtes.cz" - commit_author: "Ondrej Mirtes " + commit_user_name: "phpstan-bot" + commit_user_email: "ondrej+phpstanbot@mirtes.cz" + commit_author: "phpstan-bot " commit_options: "--gpg-sign" repository: phpstan-dist commit_message: "PHPStan ${{github.ref_name}}" diff --git a/.github/workflows/pr-base-on-previous-branch.yml b/.github/workflows/pr-base-on-previous-branch.yml new file mode 100644 index 0000000000..84015d215a --- /dev/null +++ b/.github/workflows/pr-base-on-previous-branch.yml @@ -0,0 +1,24 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Base PR on previous branch" + +on: + pull_request_target: + types: + - opened + branches: + - '1.12.x' + + +jobs: + comment: + name: "Comment on pull request" + runs-on: 'ubuntu-latest' + + steps: + - name: Comment PR + uses: peter-evans/create-or-update-comment@v4 + with: + body: "You've opened the pull request against the latest branch 1.12.x. If your code is relevant on 1.11.x and you want it to be released sooner, please rebase your pull request and change its target to 1.11.x." + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/pr-marked-as-ready.yml b/.github/workflows/pr-marked-as-ready.yml new file mode 100644 index 0000000000..b9785a2a3c --- /dev/null +++ b/.github/workflows/pr-marked-as-ready.yml @@ -0,0 +1,21 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Pull request ready for review" + +on: + pull_request_target: + types: + - ready_for_review + +jobs: + comment: + name: "Comment on pull request" + runs-on: 'ubuntu-latest' + + steps: + - name: Comment PR + uses: peter-evans/create-or-update-comment@v4 + with: + body: "This pull request has been marked as ready for review." + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/reflection-golden-test.yml b/.github/workflows/reflection-golden-test.yml new file mode 100644 index 0000000000..12e0757e4f --- /dev/null +++ b/.github/workflows/reflection-golden-test.yml @@ -0,0 +1,128 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Reflection golden test" + +on: + pull_request: + paths-ignore: + - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + - 'issue-bot/**' + push: + branches: + - "1.11.x" + paths-ignore: + - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + - 'issue-bot/**' + +env: + COMPOSER_ROOT_VERSION: "1.11.x-dev" + REFLECTION_GOLDEN_TEST_FILE: "/tmp/reflection-golden.test" + REFLECTION_GOLDEN_SYMBOLS_FILE: "/tmp/reflection-golden-symbols.txt" + +concurrency: + group: reflection-golden-test-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true + +jobs: + dump-php-symbols: + name: "Dump PHP symbols" + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.3" + # Include exotic extensions to discover more symbols + extensions: ds,mbstring,runkit7,scoutapm,seaslog,simdjson,var_representation,yac + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Dump phpSymbols.txt" + run: "php tests/dump-reflection-test-symbols.php" + + - uses: actions/upload-artifact@v4 + with: + name: phpSymbols + path: ${{ env.REFLECTION_GOLDEN_SYMBOLS_FILE }} + + reflection-golden-test: + name: "Reflection golden test" + needs: dump-php-symbols + runs-on: "ubuntu-latest" + timeout-minutes: 60 + + strategy: + fail-fast: false + matrix: + php-version: + - "7.3" + - "7.4" + - "8.0" + - "8.1" + - "8.2" + - "8.3" + + steps: + - uses: Wandalen/wretry.action@v3.4.0 + with: + action: actions/download-artifact@v4 + with: | + name: phpSymbols + path: /tmp + attempt_limit: 5 + attempt_delay: 1000 + + - name: "Checkout base commit" + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.sha || github.event.before }} + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + tools: pecl + extensions: ds,mbstring + ini-file: development + ini-values: memory_limit=2G + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Transform source code" + if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3' + shell: bash + run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}" + + - name: "Dump previous reflection data" + run: "php tests/generate-reflection-test.php" + + - uses: actions/upload-artifact@v4 + with: + name: reflection-${{ matrix.php-version }}.test + path: ${{ env.REFLECTION_GOLDEN_TEST_FILE }} + + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Transform source code" + if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3' + shell: bash + run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}" + + - name: "Reflection golden test" + run: "make tests-golden-reflection || true" diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 50e3664a8f..d5bd56b0e7 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -7,17 +7,15 @@ on: paths-ignore: - 'compiler/**' - 'apigen/**' - - 'changelog-generator/**' push: branches: - - "1.8.x" + - "1.11.x" paths-ignore: - 'compiler/**' - 'apigen/**' - - 'changelog-generator/**' env: - COMPOSER_ROOT_VERSION: "1.8.x-dev" + COMPOSER_ROOT_VERSION: "1.11.x-dev" concurrency: group: sa-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches @@ -39,61 +37,37 @@ jobs: - "8.0" - "8.1" - "8.2" + - "8.3" operating-system: [ubuntu-latest, windows-latest] steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" php-version: "${{ matrix.php-version }}" + ini-file: development extensions: mbstring - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - name: "Install PHP for code transform" - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' - uses: "shivammathur/setup-php@v2" - with: - coverage: "none" - php-version: 8.1 - extensions: mbstring, intl - - - name: "Rector downgrade cache key" - id: rector-cache-key - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' - run: | - echo "::set-output name=sha::$(php build/rector-cache-files-hash.php)" - - - name: "Rector downgrade cache" - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' - uses: actions/cache@v3 - with: - path: ./tmp/rectorCache.php - key: "rector-v2-sa-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-${{ steps.rector-cache-key.outputs.sha }}" - restore-keys: | - rector-v2-sa-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}- - - name: "Transform source code" - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' + if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3' shell: bash - run: "build/transform-source ${{ matrix.php-version }}" + run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}" - - name: "Reinstall matrix PHP version" - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' - uses: "shivammathur/setup-php@v2" - with: - coverage: "none" - php-version: "${{ matrix.php-version }}" - extensions: mbstring + - name: "Paratest patch" + if: matrix.php-version == '7.2' + run: composer config extra.patches.brianium/paratest --json --merge '["patches/paratest.patch"]' + shell: bash - name: "Downgrade PHPUnit" if: matrix.php-version == '7.2' - run: "composer require --dev phpunit/phpunit:^7.5.20 brianium/paratest:^4.0 --update-with-dependencies --ignore-platform-reqs" + run: "composer require --dev phpunit/phpunit:^8.5.31 brianium/paratest:^4.0 --update-with-dependencies --ignore-platform-reqs" - name: "PHPStan" run: "make phpstan" @@ -110,26 +84,30 @@ jobs: php-version: - "8.1" - "8.2" + - "8.3" steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" php-version: "${{ matrix.php-version }}" + ini-file: development extensions: mbstring - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - name: "Cache Result cache" - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ./tmp - key: "result-cache-v4-${{ matrix.php-version }}" + key: "result-cache-v13-${{ matrix.php-version }}-${{ github.run_id }}" + restore-keys: | + result-cache-v13-${{ matrix.php-version }}- - name: "PHPStan with result cache" run: | @@ -140,12 +118,6 @@ jobs: make phpstan-result-cache make phpstan-result-cache - - name: "Upload result cache artifact" - uses: actions/upload-artifact@v3 - with: - name: resultCache-ubuntu-latest.php - path: tmp/resultCache.php - generate-baseline: name: "Generate baseline" @@ -154,13 +126,14 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" php-version: "8.1" + ini-file: development - name: "Install dependencies" run: "composer install --no-interaction --no-progress" @@ -170,3 +143,29 @@ jobs: cp phpstan-baseline.neon phpstan-baseline-orig.neon && \ make phpstan-generate-baseline && \ diff phpstan-baseline.neon phpstan-baseline-orig.neon + + generate-baseline-php: + name: "Generate PHP baseline" + + runs-on: "ubuntu-latest" + timeout-minutes: 60 + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.1" + ini-file: development + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Generate baseline" + run: | + > phpstan-baseline.neon && \ + make phpstan-generate-baseline-php && \ + make phpstan-result-cache diff --git a/.github/workflows/tests-levels-matrix.php b/.github/workflows/tests-levels-matrix.php new file mode 100644 index 0000000000..1344e8e81e --- /dev/null +++ b/.github/workflows/tests-levels-matrix.php @@ -0,0 +1,40 @@ +testCaseClass as $testCaseClass) { + foreach($testCaseClass->testCaseMethod as $testCaseMethod) { + if ((string) $testCaseMethod['groups'] !== 'levels') { + continue; + } + + $testCaseName = (string) $testCaseMethod['id']; + + [$className, $testName] = explode('::', $testCaseName, 2); + $fileName = 'tests/'. str_replace('\\', DIRECTORY_SEPARATOR, $className) . '.php'; + + $filter = str_replace('\\', '\\\\', $testCaseName); + + $testFilters[] = sprintf("%s --filter %s", escapeshellarg($fileName), escapeshellarg($filter)); + } +} + +if ($testFilters === []) { + throw new RuntimeException('No tests found'); +} + +$chunkSize = (int) ceil(count($testFilters) / 10); +$chunks = array_chunk($testFilters, $chunkSize); + +$commands = []; +foreach ($chunks as $chunk) { + $commands[] = implode("\n", array_map(fn (string $ch) => sprintf('php vendor/bin/phpunit %s --group levels', $ch), $chunk)); +} + +echo json_encode($commands); diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6d532bb9b9..c6efcbab32 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,16 +8,18 @@ on: - 'compiler/**' - 'apigen/**' - 'changelog-generator/**' + - 'issue-bot/**' push: branches: - - "1.8.x" + - "1.11.x" paths-ignore: - 'compiler/**' - 'apigen/**' - 'changelog-generator/**' + - 'issue-bot/**' env: - COMPOSER_ROOT_VERSION: "1.8.x-dev" + COMPOSER_ROOT_VERSION: "1.11.x-dev" concurrency: group: tests-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches @@ -38,11 +40,12 @@ jobs: - "8.0" - "8.1" - "8.2" + - "8.3" operating-system: [ ubuntu-latest, windows-latest ] steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -51,48 +54,16 @@ jobs: php-version: "${{ matrix.php-version }}" tools: pecl extensions: ds,mbstring + ini-file: development ini-values: memory_limit=2G - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - name: "Install PHP for code transform" - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' - uses: "shivammathur/setup-php@v2" - with: - coverage: "none" - php-version: 8.1 - extensions: mbstring, intl - - - name: "Rector downgrade cache key" - id: rector-cache-key - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' - run: | - echo "::set-output name=sha::$(php build/rector-cache-files-hash.php)" - - - name: "Rector downgrade cache" - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' - uses: actions/cache@v3 - with: - path: ./tmp/rectorCache.php - key: "rector-v2-tests-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-${{ steps.rector-cache-key.outputs.sha }}" - restore-keys: | - rector-v2-tests-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}- - - name: "Transform source code" - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' + if: matrix.php-version != '8.1' && matrix.php-version != '8.2' && matrix.php-version != '8.3' shell: bash - run: "build/transform-source ${{ matrix.php-version }}" - - - name: "Reinstall matrix PHP version" - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' - uses: "shivammathur/setup-php@v2" - with: - coverage: "none" - php-version: "${{ matrix.php-version }}" - tools: pecl - extensions: ds,mbstring - ini-values: memory_limit=2G + run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}" - name: "Tests" run: "make tests" @@ -109,7 +80,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -118,6 +89,7 @@ jobs: php-version: "8.1" tools: pecl extensions: ds,mbstring + ini-file: development ini-values: memory_limit=1G - name: "Install dependencies" @@ -126,29 +98,65 @@ jobs: - name: "Tests" run: "make tests-integration" + tests-levels-matrix: + name: "Determine levels tests matrix" + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.3" + tools: pecl + extensions: ds,mbstring + ini-file: development + ini-values: memory_limit=1G + + - name: "Install PHPUnit 10.x" + run: "composer remove --dev brianium/paratest && composer require --dev --with-all-dependencies phpunit/phpunit:^10" + + - id: set-matrix + run: echo "matrix=$(php .github/workflows/tests-levels-matrix.php)" >> $GITHUB_OUTPUT + + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + tests-levels: + needs: tests-levels-matrix + name: "Levels tests" runs-on: ubuntu-latest timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + script: "${{fromJson(needs.tests-levels-matrix.outputs.matrix)}}" + steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.1" + php-version: "8.3" tools: pecl extensions: ds,mbstring + ini-file: development ini-values: memory_limit=1G - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - name: "Tests" - run: "make tests-levels" + run: "${{ matrix.script }}" tests-old-phpunit: name: "Tests with old PHPUnit" @@ -164,7 +172,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -173,47 +181,22 @@ jobs: php-version: "${{ matrix.php-version }}" tools: pecl extensions: ds,mbstring + ini-file: development ini-values: memory_limit=2G - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - name: "Install PHP for code transform" - uses: "shivammathur/setup-php@v2" - with: - coverage: "none" - php-version: "8.1" - extensions: mbstring, intl - - - name: "Rector downgrade cache key" - id: rector-cache-key - if: matrix.php-version != '8.1' && matrix.php-version != '8.2' - run: | - echo "::set-output name=sha::$(php build/rector-cache-files-hash.php)" - - - name: "Rector downgrade cache" - uses: actions/cache@v3 - with: - path: ./tmp/rectorCache.php - key: "rector-v2-tests-old-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-${{ steps.rector-cache-key.outputs.sha }}" - restore-keys: | - rector-v2-tests-old-${{ matrix.script }}-${{ matrix.operating-system }}-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}- - - name: "Transform source code" shell: bash - run: "build/transform-source ${{ matrix.php-version }}" + run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}" - - name: "Reinstall matrix PHP version" - uses: "shivammathur/setup-php@v2" - with: - coverage: "none" - php-version: "${{ matrix.php-version }}" - tools: pecl - extensions: ds,mbstring - ini-values: memory_limit=2G + - name: "Paratest patch" + run: composer config extra.patches.brianium/paratest --json --merge '["patches/paratest.patch"]' + shell: bash - name: "Downgrade PHPUnit" - run: "composer require --dev phpunit/phpunit:^7.5.20 brianium/paratest:^4.0 --update-with-dependencies --ignore-platform-reqs" + run: "composer require --dev phpunit/phpunit:^8.5.31 brianium/paratest:^4.0 --update-with-dependencies --ignore-platform-reqs" - name: "Tests" run: "make tests-coverage" diff --git a/.github/workflows/update-phpstorm-stubs.yml b/.github/workflows/update-phpstorm-stubs.yml index 6809f43227..f0fc56a9b1 100644 --- a/.github/workflows/update-phpstorm-stubs.yml +++ b/.github/workflows/update-phpstorm-stubs.yml @@ -14,9 +14,9 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: - ref: ${{ github.head_ref }} + ref: 1.11.x fetch-depth: '0' token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - name: "Install PHP" @@ -27,7 +27,7 @@ jobs: - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - name: "Checkout stubs" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: "phpstorm-stubs" repository: "jetbrains/phpstorm-stubs" @@ -39,7 +39,7 @@ jobs: run: "./bin/generate-function-metadata.php" - name: "Create Pull Request" id: create-pr - uses: peter-evans/create-pull-request@v4 + uses: peter-evans/create-pull-request@v6 with: token: ${{ secrets.PHPSTAN_BOT_TOKEN }} branch-suffix: random diff --git a/.gitignore b/.gitignore index 65cd70f464..f138e3cb50 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ !.idea/icon.png /tests/tmp /tests/.phpunit.result.cache +/tests/PHPStan/Reflection/data/golden/ tmp/.memory_limit diff --git a/Makefile b/Makefile index 30f9881b4a..d6ac3b5717 100644 --- a/Makefile +++ b/Makefile @@ -14,11 +14,15 @@ tests-levels: tests-coverage: php vendor/bin/paratest --runner WrapperRunner +tests-golden-reflection: + php vendor/bin/paratest --runner WrapperRunner --no-coverage tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php + lint: php vendor/bin/parallel-lint --colors \ --exclude tests/PHPStan/Analyser/data \ --exclude tests/PHPStan/Rules/Methods/data \ --exclude tests/PHPStan/Rules/Functions/data \ + --exclude tests/PHPStan/Rules/Names/data \ --exclude tests/PHPStan/Rules/Operators/data/invalid-inc-dec.php \ --exclude tests/PHPStan/Rules/Arrays/data/offset-access-without-dim-for-reading.php \ --exclude tests/PHPStan/Rules/Classes/data/duplicate-declarations.php \ @@ -28,6 +32,7 @@ lint: --exclude tests/PHPStan/Rules/Classes/data/implements-error.php \ --exclude tests/PHPStan/Rules/Classes/data/interface-extends-error.php \ --exclude tests/PHPStan/Rules/Classes/data/trait-use-error.php \ + --exclude tests/PHPStan/Rules/Methods/data/method-in-enum-without-body.php \ --exclude tests/PHPStan/Rules/Properties/data/default-value-for-native-property-type.php \ --exclude tests/PHPStan/Rules/Arrays/data/empty-array-item.php \ --exclude tests/PHPStan/Rules/Classes/data/invalid-promoted-properties.php \ @@ -37,6 +42,8 @@ lint: --exclude tests/PHPStan/Rules/Functions/data/arrow-function-nullsafe-by-ref.php \ --exclude tests/PHPStan/Levels/data/namedArguments.php \ --exclude tests/PHPStan/Rules/Keywords/data/continue-break.php \ + --exclude tests/PHPStan/Rules/Properties/data/invalid-callable-property-type.php \ + --exclude tests/PHPStan/Rules/Properties/data/properties-in-interface.php \ --exclude tests/PHPStan/Rules/Properties/data/read-only-property.php \ --exclude tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-and-native.php \ --exclude tests/PHPStan/Rules/Properties/data/read-only-property-readonly-class.php \ @@ -45,6 +52,28 @@ lint: --exclude tests/PHPStan/Rules/Properties/data/intersection-types.php \ --exclude tests/PHPStan/Rules/Classes/data/first-class-instantiation-callable.php \ --exclude tests/PHPStan/Rules/Classes/data/instantiation-callable.php \ + --exclude tests/PHPStan/Rules/Classes/data/bug-9402.php \ + --exclude tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant-native-type.php \ + --exclude tests/PHPStan/Rules/Constants/data/overriding-constant-native-types.php \ + --exclude tests/PHPStan/Rules/Methods/data/bug-10043.php \ + --exclude tests/PHPStan/Rules/Methods/data/bug-7859.php \ + --exclude tests/PHPStan/Rules/Methods/data/bug-8081.php \ + --exclude tests/PHPStan/Rules/Methods/data/bug-9014.php \ + --exclude tests/PHPStan/Rules/Methods/data/bug-10101.php \ + --exclude tests/PHPStan/Rules/Methods/data/final-method-by-phpdoc.php \ + --exclude tests/PHPStan/Rules/Traits/data/conflicting-trait-constants-types.php \ + --exclude tests/PHPStan/Rules/Types/data/invalid-union-with-mixed.php \ + --exclude tests/PHPStan/Rules/Types/data/invalid-union-with-never.php \ + --exclude tests/PHPStan/Rules/Types/data/invalid-union-with-void.php \ + --exclude tests/PHPStan/Rules/Constants/data/dynamic-class-constant-fetch.php \ + --exclude tests/PHPStan/Rules/Keywords/data/declare-position.php \ + --exclude tests/PHPStan/Rules/Keywords/data/declare-position2.php \ + --exclude tests/PHPStan/Rules/Keywords/data/declare-position-nested.php \ + --exclude tests/PHPStan/Rules/Keywords/data/declare-strict-nonsense.php \ + --exclude tests/PHPStan/Rules/Keywords/data/declare-strict-nonsense-bool.php \ + --exclude tests/PHPStan/Rules/Keywords/data/declare-inline-html.php \ + --exclude tests/PHPStan/Rules/Classes/data/extends-readonly-class.php \ + --exclude tests/PHPStan/Rules/Classes/data/instantiation-promoted-properties.php \ src tests cs: @@ -62,8 +91,14 @@ phpstan-result-cache: phpstan-generate-baseline: php -d memory_limit=448M bin/phpstan --generate-baseline +phpstan-generate-baseline-php: + php -d memory_limit=448M bin/phpstan analyse --generate-baseline phpstan-baseline.php + phpstan-pro: php -d memory_limit=448M bin/phpstan --pro -composer-require-checker: - php build/composer-require-checker.phar check --config-file $(CURDIR)/build/composer-require-checker.json +name-collision: + php vendor/bin/detect-collisions --configuration build/collision-detector.json + +composer-dependency-analyser: + php vendor/bin/composer-dependency-analyser --config build/composer-dependency-analyser.php diff --git a/README.md b/README.md index 2c54109d72..6d2e16963c 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,16 @@ This repository (`phpstan/phpstan-src`) is for PHPStan's development only. Head Any contributions are welcome. +### Installation + +```bash +composer install +``` + +If you encounter dependency problem, try using `export COMPOSER_ROOT_VERSION=1.11.x-dev` + +If you are using macOS and are using an older version of `patch`, you may have problems with patch application failure during `composer install`. Try using `brew install gpatch` to install a newer and supported `patch` version. + ### Building PHPStan's source code is developed on PHP 8.1. For distribution in `phpstan/phpstan` package and as a PHAR file, the source code is transformed to run on PHP 7.2 and higher. @@ -54,7 +64,7 @@ make tests ### Debugging -1. Make sure XDebug is installed and configured. +1. Make sure Xdebug is installed and configured. 2. Add `--xdebug` option when running PHPStan. Without it PHPStan turns the debugger off at runtime. 3. If you're not debugging the [result cache](https://phpstan.org/user-guide/result-cache), also add the `--debug` option. diff --git a/apigen/theme/blocks/head.latte b/apigen/theme/blocks/head.latte index c7e24d22fb..16f25417e3 100644 --- a/apigen/theme/blocks/head.latte +++ b/apigen/theme/blocks/head.latte @@ -2,3 +2,7 @@ {/define} +{define menu} + + {include #parent} +{/define} diff --git a/bin/functionMetadata_original.php b/bin/functionMetadata_original.php index 618ec3b44f..60f8e8198c 100644 --- a/bin/functionMetadata_original.php +++ b/bin/functionMetadata_original.php @@ -31,11 +31,14 @@ 'array_merge' => ['hasSideEffects' => false], 'array_merge_recursive' => ['hasSideEffects' => false], 'array_pad' => ['hasSideEffects' => false], + 'array_pop' => ['hasSideEffects' => true], 'array_product' => ['hasSideEffects' => false], + 'array_push' => ['hasSideEffects' => true], 'array_rand' => ['hasSideEffects' => false], 'array_replace' => ['hasSideEffects' => false], 'array_replace_recursive' => ['hasSideEffects' => false], 'array_reverse' => ['hasSideEffects' => false], + 'array_shift' => ['hasSideEffects' => true], 'array_slice' => ['hasSideEffects' => false], 'array_sum' => ['hasSideEffects' => false], 'array_udiff' => ['hasSideEffects' => false], @@ -45,6 +48,7 @@ 'array_uintersect_assoc' => ['hasSideEffects' => false], 'array_uintersect_uassoc' => ['hasSideEffects' => false], 'array_unique' => ['hasSideEffects' => false], + 'array_unshift' => ['hasSideEffects' => true], 'array_values' => ['hasSideEffects' => false], 'asin' => ['hasSideEffects' => false], 'asinh' => ['hasSideEffects' => false], @@ -66,12 +70,16 @@ 'chown' => ['hasSideEffects' => true], 'copy' => ['hasSideEffects' => true], 'count' => ['hasSideEffects' => false], + 'connection_aborted' => ['hasSideEffects' => true], + 'connection_status' => ['hasSideEffects' => true], + 'error_log' => ['hasSideEffects' => true], 'fclose' => ['hasSideEffects' => true], 'fflush' => ['hasSideEffects' => true], 'fgetc' => ['hasSideEffects' => true], 'fgetcsv' => ['hasSideEffects' => true], 'fgets' => ['hasSideEffects' => true], 'fgetss' => ['hasSideEffects' => true], + 'file_get_contents' => ['hasSideEffects' => true], 'file_put_contents' => ['hasSideEffects' => true], 'flock' => ['hasSideEffects' => true], 'fopen' => ['hasSideEffects' => true], @@ -83,9 +91,11 @@ 'fseek' => ['hasSideEffects' => true], 'ftruncate' => ['hasSideEffects' => true], 'fwrite' => ['hasSideEffects' => true], + 'json_validate' => ['hasSideEffects' => false], 'lchgrp' => ['hasSideEffects' => true], 'lchown' => ['hasSideEffects' => true], 'link' => ['hasSideEffects' => true], + 'mb_str_pad' => ['hasSideEffects' => false], 'mkdir' => ['hasSideEffects' => true], 'move_uploaded_file' => ['hasSideEffects' => true], 'pclose' => ['hasSideEffects' => true], @@ -95,6 +105,8 @@ 'rewind' => ['hasSideEffects' => true], 'rmdir' => ['hasSideEffects' => true], 'sprintf' => ['hasSideEffects' => false], + 'str_decrement' => ['hasSideEffects' => false], + 'str_increment' => ['hasSideEffects' => false], 'symlink' => ['hasSideEffects' => true], 'tempnam' => ['hasSideEffects' => true], 'tmpfile' => ['hasSideEffects' => true], diff --git a/bin/generate-function-metadata.php b/bin/generate-function-metadata.php index 4adbb1d038..f6f5131a53 100755 --- a/bin/generate-function-metadata.php +++ b/bin/generate-function-metadata.php @@ -11,12 +11,13 @@ use PHPStan\File\FileReader; use PHPStan\File\FileWriter; use PHPStan\ShouldNotHappenException; +use Symfony\Component\Finder\Finder; (function (): void { require_once __DIR__ . '/../vendor/autoload.php'; $parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7); - $finder = new Symfony\Component\Finder\Finder(); + $finder = new Finder(); $finder->in(__DIR__ . '/../vendor/jetbrains/phpstorm-stubs')->files()->name('*.php'); $visitor = new class() extends NodeVisitorAbstract { @@ -82,6 +83,9 @@ public function enterNode(Node $node) 'rand', 'random_bytes', 'random_int', + 'connection_aborted', + 'connection_status', + 'file_get_contents', ], true)) { continue; } diff --git a/bin/generate-rule-error-classes.php b/bin/generate-rule-error-classes.php index 991f72503b..b8b3219e5d 100755 --- a/bin/generate-rule-error-classes.php +++ b/bin/generate-rule-error-classes.php @@ -33,17 +33,13 @@ class RuleError%s implements %s } $properties = []; $interfaces = []; - foreach ($ruleErrorTypes as $typeNumber => [$interface, $propertyName, $nativePropertyType, $phpDocPropertyType]) { + foreach ($ruleErrorTypes as $typeNumber => [$interface, $typeProperties]) { if (!(($typeCombination & $typeNumber) === $typeNumber)) { continue; } $interfaces[] = '\\' . $interface; - if ($propertyName === null || $nativePropertyType === null || $phpDocPropertyType === null) { - continue; - } - - $properties[] = [$propertyName, $nativePropertyType, $phpDocPropertyType]; + $properties = array_merge($properties, $typeProperties); } $phpClass = sprintf( diff --git a/bin/phpstan b/bin/phpstan index 0f153744e3..537f3e123d 100755 --- a/bin/phpstan +++ b/bin/phpstan @@ -44,8 +44,8 @@ use Symfony\Component\Console\Helper\ProgressBar; || !array_key_exists('e69f7f6ee287b969198c3c9d6777bd38', $composerAutoloadFiles) || !array_key_exists('0d59ee240a4cd96ddbb4ff164fccea4d', $composerAutoloadFiles) || !array_key_exists('b686b8e46447868025a15ce5d0cb2634', $composerAutoloadFiles) - || !array_key_exists('25072dd6e2470089de65ae7bf11d3109', $composerAutoloadFiles) || !array_key_exists('8825ede83f2f289127722d4e842cf7e8', $composerAutoloadFiles) + || !array_key_exists('23c18046f52bef3eea034657bafda50f', $composerAutoloadFiles) ) { echo "Composer autoloader changed\n"; exit(1); @@ -72,11 +72,11 @@ use Symfony\Component\Console\Helper\ProgressBar; // vendor/symfony/polyfill-php74/bootstrap.php 'b686b8e46447868025a15ce5d0cb2634' => true, - // vendor/symfony/polyfill-php72/bootstrap.php - '25072dd6e2470089de65ae7bf11d3109' => true, - // vendor/symfony/polyfill-intl-grapheme/bootstrap.php '8825ede83f2f289127722d4e842cf7e8' => true, + + // vendor/symfony/polyfill-php81/bootstrap.php + '23c18046f52bef3eea034657bafda50f' => true, ]; $autoloaderInWorkingDirectory = $vendorDirectory . '/autoload.php'; diff --git a/build-cs/composer.json b/build-cs/composer.json index 60184b6b66..16a240bc97 100644 --- a/build-cs/composer.json +++ b/build-cs/composer.json @@ -1,8 +1,8 @@ { "require-dev": { "consistence-community/coding-standard": "^3.11.0", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "slevomat/coding-standard": "^8.0.0", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", + "slevomat/coding-standard": "^8.8.0", "squizlabs/php_codesniffer": "^3.5.3" }, "config": { diff --git a/build-cs/composer.lock b/build-cs/composer.lock index da896907b0..e0bfbd4cae 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -4,21 +4,21 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a6c963dcf5d33dad94841883f3b6cbe3", + "content-hash": "e69c1916405a7e3c8001c1b609a0ee61", "packages": [], "packages-dev": [ { "name": "consistence-community/coding-standard", - "version": "3.11.2", + "version": "3.11.3", "source": { "type": "git", "url": "https://github.com/consistence-community/coding-standard.git", - "reference": "adb4be482e76990552bf624309d2acc8754ba1bd" + "reference": "f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/adb4be482e76990552bf624309d2acc8754ba1bd", - "reference": "adb4be482e76990552bf624309d2acc8754ba1bd", + "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1", + "reference": "f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1", "shasum": "" }, "require": { @@ -70,41 +70,44 @@ ], "support": { "issues": "https://github.com/consistence-community/coding-standard/issues", - "source": "https://github.com/consistence-community/coding-standard/tree/3.11.2" + "source": "https://github.com/consistence-community/coding-standard/tree/3.11.3" }, - "time": "2022-06-21T08:36:36+00:00" + "time": "2023-03-27T14:55:41+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v0.7.2", + "version": "v1.0.0", "source": { "type": "git", - "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", - "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db" + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "4be43904336affa5c2f70744a348312336afd0da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", - "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", + "reference": "4be43904336affa5c2f70744a348312336afd0da", "shasum": "" }, "require": { "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.3", + "php": ">=5.4", "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { "composer/composer": "*", + "ext-json": "*", + "ext-zip": "*", "php-parallel-lint/php-parallel-lint": "^1.3.1", - "phpcompatibility/php-compatibility": "^9.0" + "phpcompatibility/php-compatibility": "^9.0", + "yoast/phpunit-polyfills": "^1.0" }, "type": "composer-plugin", "extra": { - "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" }, "autoload": { "psr-4": { - "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -120,7 +123,7 @@ }, { "name": "Contributors", - "homepage": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer/graphs/contributors" + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", @@ -144,29 +147,31 @@ "tests" ], "support": { - "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", - "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2022-02-04T12:51:07+00:00" + "time": "2023-01-05T11:28:13+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.8.0", + "version": "1.24.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "8dd908dd6156e974b9a0f8bb4cd5ad0707830f04" + "reference": "bcad8d995980440892759db0c32acae7c8e79442" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/8dd908dd6156e974b9a0f8bb4cd5ad0707830f04", - "reference": "8dd908dd6156e974b9a0f8bb4cd5ad0707830f04", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bcad8d995980440892759db0c32acae7c8e79442", + "reference": "bcad8d995980440892759db0c32acae7c8e79442", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^1.5", @@ -190,38 +195,38 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.8.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.2" }, - "time": "2022-09-04T18:59:06+00:00" + "time": "2023-09-26T12:28:12+00:00" }, { "name": "slevomat/coding-standard", - "version": "8.5.2", + "version": "8.14.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "f32937dc41b587f3500efed1dbca2f82aa519373" + "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/f32937dc41b587f3500efed1dbca2f82aa519373", - "reference": "f32937dc41b587f3500efed1dbca2f82aa519373", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/fea1fd6f137cc84f9cba0ae30d549615dbc6a926", + "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": ">=1.7.0 <1.9.0", + "phpstan/phpdoc-parser": "^1.23.1", "squizlabs/php_codesniffer": "^3.7.1" }, "require-dev": { "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.8.6", - "phpstan/phpstan-deprecation-rules": "1.0.0", - "phpstan/phpstan-phpunit": "1.0.0|1.1.1", - "phpstan/phpstan-strict-rules": "1.4.4", - "phpunit/phpunit": "7.5.20|8.5.21|9.5.25" + "phpstan/phpstan": "1.10.37", + "phpstan/phpstan-deprecation-rules": "1.1.4", + "phpstan/phpstan-phpunit": "1.3.14", + "phpstan/phpstan-strict-rules": "1.5.1", + "phpunit/phpunit": "8.5.21|9.6.8|10.3.5" }, "type": "phpcodesniffer-standard", "extra": { @@ -231,7 +236,7 @@ }, "autoload": { "psr-4": { - "SlevomatCodingStandard\\": "SlevomatCodingStandard" + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" } }, "notification-url": "https://packagist.org/downloads/", @@ -239,9 +244,13 @@ "MIT" ], "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "keywords": [ + "dev", + "phpcs" + ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.5.2" + "source": "https://github.com/slevomat/coding-standard/tree/8.14.1" }, "funding": [ { @@ -253,20 +262,20 @@ "type": "tidelift" } ], - "time": "2022-09-27T16:45:37+00:00" + "time": "2023-10-08T07:28:08+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.7.1", + "version": "3.7.2", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619" + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619", - "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", "shasum": "" }, "require": { @@ -302,14 +311,15 @@ "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", "keywords": [ "phpcs", - "standards" + "standards", + "static analysis" ], "support": { "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", "source": "https://github.com/squizlabs/PHP_CodeSniffer", "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" }, - "time": "2022-06-18T07:21:10+00:00" + "time": "2023-02-22T23:07:41+00:00" } ], "aliases": [], @@ -319,5 +329,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/build/PHPStan/Build/RectorCache.php b/build/PHPStan/Build/RectorCache.php deleted file mode 100644 index daeb527e91..0000000000 --- a/build/PHPStan/Build/RectorCache.php +++ /dev/null @@ -1,180 +0,0 @@ - */ - private static $restoreAlreadyRun = null; - - /** - * @return array - */ - public function restore(): array - { - if (self::$restoreAlreadyRun !== null) { - return self::$restoreAlreadyRun; - } - - if (!is_file(self::CACHE_FILE)) { - echo "Rector downgrade cache does not exist\n"; - return self::$restoreAlreadyRun = self::PATHS; - } - $cache = Json::decode(FileReader::read(self::CACHE_FILE), Json::FORCE_ARRAY); - $files = $this->findFiles(); - $filesToDowngrade = []; - foreach ($files as $file) { - if (!isset($cache[$file])) { - echo sprintf("File %s not found in cache - will be downgraded\n", $file); - $filesToDowngrade[] = $file; - continue; - } - - $fileCache = $cache[$file]; - $hash = sha1_file($file); - if ($hash === $fileCache['originalFileHash']) { - FileWriter::write($file, $fileCache['downgradedContents']); - continue; - } - - echo sprintf("File %s has different hash - will be downgraded\n", $file); - echo sprintf("%s vs. %s\n", $hash, $fileCache['originalFileHash']); - - $filesToDowngrade[] = $file; - } - - if (count($filesToDowngrade) === 0) { - echo "No new files to downgrade - done\n"; - exit(0); - } - - return self::$restoreAlreadyRun = $filesToDowngrade; - } - - public function saveHashes(): void - { - $files = $this->findFiles(); - $hashes = []; - foreach ($files as $file) { - $hashes[$file] = sha1_file($file); - } - - FileWriter::write(self::HASHES_FILE, Json::encode($hashes)); - } - - public function getOriginalFilesHash(): string - { - $files = $this->findFiles(); - $hashes = []; - foreach ($files as $file) { - $hashes[] = $file . '~' . sha1_file($file); - } - - return sha1(implode('-', $hashes)); - } - - /** - * @return array - */ - private function findFiles(): array - { - $finder = new Finder(); - $finder->followLinks(); - $finder->filter(function (SplFileInfo $splFileInfo) : bool { - $realPath = $splFileInfo->getRealPath(); - if ($realPath === '') { - // dead symlink - return \false; - } - // make the path work accross different OSes - $realPath = \str_replace('\\', '/', $realPath); - // return false to remove file - foreach (self::SKIP_PATHS as $excludePath) { - // make the path work accross different OSes - $excludePath = \str_replace('\\', '/', $excludePath); - if (Strings::match($realPath, '#' . \preg_quote($excludePath, '#') . '#') !== null) { - return \false; - } - $excludePath = $this->normalizeForFnmatch($excludePath); - if (\fnmatch($excludePath, $realPath)) { - return \false; - } - } - return \true; - }); - $files = []; - foreach ($finder->files()->name('*.php')->in(self::PATHS) as $fileInfo) { - $files[] = $fileInfo->getRealPath(); - } - - return $files; - } - - private function normalizeForFnmatch(string $path) : string - { - // ends with * - if (Strings::match($path, self::ENDS_WITH_ASTERISK_REGEX) !== null) { - return '*' . $path; - } - // starts with * - if (Strings::match($path, self::STARTS_WITH_ASTERISK_REGEX) !== null) { - return $path . '*'; - } - return $path; - } - - public function save(): void - { - $files = $this->findFiles(); - $originalHashes = Json::decode(FileReader::read(self::HASHES_FILE), Json::FORCE_ARRAY); - $cache = []; - foreach ($files as $file) { - $cache[$file] = [ - 'originalFileHash' => $originalHashes[$file], - 'downgradedContents' => FileReader::read($file), - ]; - } - - FileWriter::write(self::CACHE_FILE, Json::encode($cache)); - } - -} diff --git a/build/PHPStan/Build/ServiceLocatorDynamicReturnTypeExtension.php b/build/PHPStan/Build/ServiceLocatorDynamicReturnTypeExtension.php index da67c961e6..51874b7a23 100644 --- a/build/PHPStan/Build/ServiceLocatorDynamicReturnTypeExtension.php +++ b/build/PHPStan/Build/ServiceLocatorDynamicReturnTypeExtension.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -34,10 +33,14 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method $returnType = ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); - if ($methodReflection->getName() === 'getByType' && count($methodCall->getArgs()) >= 2) { - $argType = $scope->getType($methodCall->getArgs()[1]->value); - if ($argType instanceof ConstantBooleanType && $argType->getValue()) { - $returnType = TypeCombinator::addNull($returnType); + if ($methodReflection->getName() === 'getByType') { + if (count($methodCall->getArgs()) < 2) { + $returnType = TypeCombinator::removeNull($returnType); + } else { + $argType = $scope->getType($methodCall->getArgs()[1]->value); + if ($argType->isTrue()->yes()) { + $returnType = TypeCombinator::removeNull($returnType); + } } } diff --git a/build/baseline-7.4.neon b/build/baseline-7.4.neon index f1efeccdf6..82e6b89e0c 100644 --- a/build/baseline-7.4.neon +++ b/build/baseline-7.4.neon @@ -4,10 +4,6 @@ parameters: message: "#^Class PHPStan\\\\Command\\\\ErrorsConsoleStyle has an uninitialized property \\$progressBar\\. Give it default value or assign it in the constructor\\.$#" count: 1 path: ../src/Command/ErrorsConsoleStyle.php - - - message: "#^Class PHPStan\\\\DependencyInjection\\\\Reflection\\\\DirectClassReflectionExtensionRegistryProvider has an uninitialized property \\$broker\\. Give it default value or assign it in the constructor\\.$#" - count: 1 - path: ../src/DependencyInjection/Reflection/DirectClassReflectionExtensionRegistryProvider.php - message: "#^Class PHPStan\\\\Parallel\\\\ParallelAnalyser has an uninitialized property \\$processPool\\. Give it default value or assign it in the constructor\\.$#" @@ -18,10 +14,6 @@ parameters: message: "#^Class PHPStan\\\\Parallel\\\\Process has an uninitialized property \\$process\\. Give it default value or assign it in the constructor\\.$#" count: 1 path: ../src/Parallel/Process.php - - - message: "#^Class PHPStan\\\\Parallel\\\\Process has an uninitialized property \\$in\\. Give it default value or assign it in the constructor\\.$#" - count: 1 - path: ../src/Parallel/Process.php - message: "#^Class PHPStan\\\\PhpDoc\\\\ResolvedPhpDocBlock has an uninitialized property \\$phpDocNodes\\. Give it default value or assign it in the constructor\\.$#" count: 1 @@ -55,6 +47,12 @@ parameters: message: "#^Class PHPStan\\\\PhpDoc\\\\ResolvedPhpDocBlock has an uninitialized property \\$phpDocNodeResolver\\. Give it default value or assign it in the constructor\\.$#" count: 1 path: ../src/PhpDoc/ResolvedPhpDocBlock.php + + - + message: "#^Class PHPStan\\\\PhpDoc\\\\ResolvedPhpDocBlock has an uninitialized property \\$reflectionProvider\\. Give it default value or assign it in the constructor\\.$#" + count: 1 + path: ../src/PhpDoc/ResolvedPhpDocBlock.php + - message: "#^Class PHPStan\\\\Reflection\\\\BetterReflection\\\\SourceLocator\\\\CachingVisitor has an uninitialized property \\$fileName\\. Give it default value or assign it in the constructor\\.$#" count: 1 diff --git a/build/baseline-8.0.neon b/build/baseline-8.0.neon index 8e69f2f10e..3e0d07184d 100644 --- a/build/baseline-8.0.neon +++ b/build/baseline-8.0.neon @@ -6,9 +6,9 @@ parameters: path: ../src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php - - message: "#^Strict comparison using \\=\\=\\= between array and false will always evaluate to false\\.$#" - count: 2 - path: ../src/Type/Php/MbFunctionsReturnTypeExtensionTrait.php + message: "#^Strict comparison using \\=\\=\\= between list\\ and false will always evaluate to false\\.$#" + count: 1 + path: ../src/Type/Php/MbFunctionsReturnTypeExtension.php - message: "#^Strict comparison using \\=\\=\\= between int<0, max> and false will always evaluate to false\\.$#" @@ -16,12 +16,17 @@ parameters: path: ../src/Type/Php/MbStrlenFunctionReturnTypeExtension.php - - message: "#^Strict comparison using \\=\\=\\= between array and false will always evaluate to false\\.$#" + message: "#^Strict comparison using \\=\\=\\= between list\\ and false will always evaluate to false\\.$#" + count: 1 + path: ../src/Type/Php/MbStrlenFunctionReturnTypeExtension.php + + - + message: "#^Strict comparison using \\=\\=\\= between list\\ and false will always evaluate to false\\.$#" count: 1 path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php - - message: "#^Strict comparison using \\=\\=\\= between array and false will always evaluate to false\\.$#" + message: "#^Strict comparison using \\=\\=\\= between list and false will always evaluate to false\\.$#" count: 1 path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php diff --git a/build/baseline-lt-7.3.neon b/build/baseline-lt-7.3.neon index 8d088b9056..aab4991158 100644 --- a/build/baseline-lt-7.3.neon +++ b/build/baseline-lt-7.3.neon @@ -1,6 +1,2 @@ parameters: - ignoreErrors: - - - message: "#^Call to an undefined static method PHPUnit\\\\Framework\\\\TestCase\\:\\:assertFileDoesNotExist\\(\\)\\.$#" - count: 1 - path: ../src/Testing/LevelsTestCase.php + ignoreErrors: [] diff --git a/build/collision-detector.json b/build/collision-detector.json new file mode 100644 index 0000000000..21228704f5 --- /dev/null +++ b/build/collision-detector.json @@ -0,0 +1,16 @@ +{ + "scanPaths": ["../src", "../build", "../tests"], + "excludePaths": [ + "../tests/PHPStan/Analyser/data/parse-error.php", + "../tests/PHPStan/Analyser/data/multipleParseErrors.php", + "../tests/PHPStan/Parser/data/cleaning-1-before.php", + "../tests/PHPStan/Parser/data/cleaning-1-after.php", + "../tests/PHPStan/Rules/Functions/data/duplicate-function.php", + "../tests/PHPStan/Rules/Classes/data/duplicate-class.php", + "../tests/PHPStan/Rules/Names/data/multiple-namespaces.php", + "../tests/PHPStan/Rules/Names/data/no-namespace.php", + "../tests/notAutoloaded", + "../tests/PHPStan/Rules/Functions/data/define-bug-3349.php", + "../tests/PHPStan/Levels/data/stubs/function.php" + ] +} diff --git a/build/composer-dependency-analyser.php b/build/composer-dependency-analyser.php new file mode 100644 index 0000000000..5797d95bcf --- /dev/null +++ b/build/composer-dependency-analyser.php @@ -0,0 +1,40 @@ +addPathToScan(__DIR__ . '/../bin', true) + ->ignoreErrorsOnPackages( + [ + 'hoa/regex', // used only via stream wrapper hoa:// + ...$pinnedToSupportPhp72, // those are unused, but we need to pin them to support PHP 7.2 + ...$polyfills, // not detected by composer-dependency-analyser + ], + [ErrorType::UNUSED_DEPENDENCY], + ) + ->ignoreErrorsOnPackage('phpunit/phpunit', [ErrorType::DEV_DEPENDENCY_IN_PROD]) // prepared test tooling + ->ignoreErrorsOnPackage('jetbrains/phpstorm-stubs', [ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV]) // there is no direct usage, but we need newer version then required by ondrejmirtes/BetterReflection + ->ignoreErrorsOnPath(__DIR__ . '/../tests', [ErrorType::UNKNOWN_CLASS, ErrorType::UNKNOWN_FUNCTION]) // to be able to test invalid symbols + ->ignoreUnknownClasses([ + 'JetBrains\PhpStorm\Pure', // not present on composer's classmap + 'PHPStan\ExtensionInstaller\GeneratedConfig', // generated + ]); diff --git a/build/composer-require-checker.json b/build/composer-require-checker.json deleted file mode 100644 index 67fb8c0dff..0000000000 --- a/build/composer-require-checker.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "symbol-whitelist" : [ - "null", "true", "false", - "static", "self", "parent", - "array", "string", "int", "float", "bool", "iterable", "callable", "void", "object", "mixed", - "PHPUnit\\Framework\\TestCase", "PHPUnit\\Framework\\AssertionFailedError", "Composer\\Autoload\\ClassLoader", "PHPUnit\\Framework\\ExpectationFailedException", - "JSON_THROW_ON_ERROR", "JSON_INVALID_UTF8_IGNORE", "JsonSerializable", "SimpleXMLElement", "PHPStan\\ExtensionInstaller\\GeneratedConfig", "Nette\\DI\\InvalidConfigurationException", - "CURLOPT_SSL_VERIFYHOST", "FILTER_SANITIZE_EMAIL", "FILTER_SANITIZE_EMAIL", "FILTER_SANITIZE_ENCODED", "FILTER_SANITIZE_MAGIC_QUOTES", "FILTER_SANITIZE_NUMBER_FLOAT", - "FILTER_SANITIZE_NUMBER_INT", "FILTER_SANITIZE_SPECIAL_CHARS", "FILTER_SANITIZE_STRING", "FILTER_SANITIZE_URL", "FILTER_VALIDATE_BOOLEAN", - "FILTER_VALIDATE_EMAIL", "FILTER_VALIDATE_FLOAT", "FILTER_VALIDATE_INT", "FILTER_VALIDATE_IP", "FILTER_VALIDATE_MAC", "FILTER_VALIDATE_REGEXP", - "FILTER_VALIDATE_URL", "FILTER_NULL_ON_FAILURE", "FILTER_FORCE_ARRAY", "FILTER_SANITIZE_ADD_SLASHES", "FILTER_DEFAULT", "FILTER_UNSAFE_RAW", "opcache_invalidate", "ValueError", "ReflectionUnionType", "ReflectionIntersectionType", "Attribute", "ReflectionEnum", "ReflectionEnumBackedCase", "enum_exists", - "React\\Async\\await", "Hoa\\File\\Read" - ], - "php-core-extensions" : [ - "json", - "Core", - "date", - "pcre", - "Phar", - "Reflection", - "SPL", - "standard", - "mbstring", - "hash", - "tokenizer", - "dom" - ] -} diff --git a/build/composer-require-checker.phar b/build/composer-require-checker.phar deleted file mode 100644 index 12b80f423a..0000000000 Binary files a/build/composer-require-checker.phar and /dev/null differ diff --git a/build/downgrade.php b/build/downgrade.php new file mode 100644 index 0000000000..b9b3f182f5 --- /dev/null +++ b/build/downgrade.php @@ -0,0 +1,18 @@ + [ + __DIR__ . '/../src', + __DIR__ . '/../tests/PHPStan', + __DIR__ . '/../tests/e2e', + ], + 'excludePaths' => [ + 'tests/*/data/*', + 'tests/*/Fixture/*', + 'tests/PHPStan/Analyser/traits/*', + 'tests/PHPStan/Generics/functions.php', + 'tests/e2e/resultCache_1.php', + 'tests/e2e/resultCache_2.php', + 'tests/e2e/resultCache_3.php', + ], +]; diff --git a/build/enum-adapter-errors.neon b/build/enum-adapter-errors.neon index 587860f8b3..04a18bb846 100644 --- a/build/enum-adapter-errors.neon +++ b/build/enum-adapter-errors.neon @@ -1,48 +1,8 @@ parameters: ignoreErrors: - - - message: "#^Call to method getInterfaceNames\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Analyser/NodeScopeResolver.php - - - - message: "#^Call to method getStartLine\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 2 - path: ../src/Analyser/NodeScopeResolver.php - - - - message: "#^Call to method getInterfaceNames\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Analyser/TypeSpecifier.php - - - - message: "#^Call to method getMethod\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/PhpDoc/PhpDocInheritanceResolver.php - - - - message: "#^Call to method hasConstant\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/PhpDoc/PhpDocInheritanceResolver.php - - - - message: "#^Call to method hasMethod\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/PhpDoc/PhpDocInheritanceResolver.php - - - - message: "#^Call to method hasProperty\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/PhpDoc/PhpDocInheritanceResolver.php - - - - message: "#^Call to method getReflectionConstants\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/PhpDoc/TypeNodeResolver.php - - message: "#^Call to method getAttributes\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 3 + count: 2 path: ../src/Reflection/ClassReflection.php - @@ -80,16 +40,6 @@ parameters: count: 1 path: ../src/Reflection/ClassReflection.php - - - message: "#^Call to method getInterfaceNames\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 2 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method getInterfaces\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 2 - path: ../src/Reflection/ClassReflection.php - - message: "#^Call to method getName\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" count: 1 @@ -97,31 +47,16 @@ parameters: - message: "#^Call to method getParentClass\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 8 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method getReflectionConstant\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 2 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method getTraitNames\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 + count: 4 path: ../src/Reflection/ClassReflection.php - message: "#^Call to method getTraits\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 2 - path: ../src/Reflection/ClassReflection.php - - - - message: "#^Call to method hasCase\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" count: 1 path: ../src/Reflection/ClassReflection.php - - message: "#^Call to method hasConstant\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" + message: "#^Call to method hasCase\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" count: 1 path: ../src/Reflection/ClassReflection.php @@ -177,7 +112,7 @@ parameters: - message: "#^Class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum not found\\.$#" - count: 5 + count: 4 path: ../src/Reflection/ClassReflection.php - @@ -205,152 +140,12 @@ parameters: count: 1 path: ../src/Reflection/ClassReflection.php - - - message: "#^Call to method getMethod\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ConstructorsHelper.php - - - - message: "#^Call to method getName\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ConstructorsHelper.php - - - - message: "#^Call to method hasMethod\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/ConstructorsHelper.php - - - - message: "#^Call to method getName\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/Native/NativeMethodReflection.php - - message: "#^Method PHPStan\\\\Reflection\\\\Php\\\\BuiltinMethodReflection\\:\\:getDeclaringClass\\(\\) has invalid return type PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" count: 1 path: ../src/Reflection/Php/BuiltinMethodReflection.php - - - message: "#^Method PHPStan\\\\Reflection\\\\Php\\\\FakeBuiltinMethodReflection\\:\\:getDeclaringClass\\(\\) has invalid return type PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/Php/FakeBuiltinMethodReflection.php - - - - message: "#^Parameter \\$declaringClass of method PHPStan\\\\Reflection\\\\Php\\\\FakeBuiltinMethodReflection\\:\\:__construct\\(\\) has invalid type PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/Php/FakeBuiltinMethodReflection.php - - - - message: "#^Property PHPStan\\\\Reflection\\\\Php\\\\FakeBuiltinMethodReflection\\:\\:\\$declaringClass has unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum as its type\\.$#" - count: 1 - path: ../src/Reflection/Php/FakeBuiltinMethodReflection.php - - message: "#^Method PHPStan\\\\Reflection\\\\Php\\\\NativeBuiltinMethodReflection\\:\\:getDeclaringClass\\(\\) has invalid return type PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" count: 1 path: ../src/Reflection/Php/NativeBuiltinMethodReflection.php - - - - message: "#^Call to method getConstructor\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 2 - path: ../src/Reflection/Php/PhpClassReflectionExtension.php - - - - message: "#^Call to method getMethod\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 3 - path: ../src/Reflection/Php/PhpClassReflectionExtension.php - - - - message: "#^Call to method getName\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 4 - path: ../src/Reflection/Php/PhpClassReflectionExtension.php - - - - message: "#^Call to method getProperty\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 3 - path: ../src/Reflection/Php/PhpClassReflectionExtension.php - - - - message: "#^Call to method hasMethod\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 3 - path: ../src/Reflection/Php/PhpClassReflectionExtension.php - - - - message: "#^Call to method hasProperty\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 2 - path: ../src/Reflection/Php/PhpClassReflectionExtension.php - - - - message: "#^Call to method isTrait\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 2 - path: ../src/Reflection/Php/PhpClassReflectionExtension.php - - - - message: "#^Call to method getName\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Reflection/Php/PhpMethodReflection.php - - - - message: "#^Call to method getTraitAliases\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 2 - path: ../src/Reflection/Php/PhpMethodReflection.php - - - - message: "#^Call to method getMethods\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Rules/Api/ApiClassConstFetchRule.php - - - - message: "#^Call to method getShortName\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Rules/Cast/InvalidCastRule.php - - - - message: "#^Call to method getMethods\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Rules/Methods/MissingMethodImplementationRule.php - - - - message: "#^Call to method isFinal\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 2 - path: ../src/Type/Constant/ConstantStringType.php - - - - message: "#^Call to method getInterfaceNames\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Type/DynamicReturnTypeExtensionRegistry.php - - - - message: "#^Call to method getProperties\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Type/ObjectType.php - - - - message: "#^Call to method getStartLine\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 2 - path: ../src/Type/ObjectType.php - - - - message: "#^Call to method isFinal\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 3 - path: ../src/Type/ObjectType.php - - - - message: "#^Call to method isUserDefined\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Type/ObjectType.php - - - - message: "#^Call to method getProperty\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../tests/PHPStan/Analyser/AnalyserIntegrationTest.php - - - - message: "#^Call to method getProperty\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/PhpDoc/PhpDocInheritanceResolver.php - - - - message: "#^Call to method getName\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../tests/PHPStan/Reflection/ClassReflectionTest.php diff --git a/build/enums.neon b/build/enums.neon index 06e04047d6..3ec87ab42e 100644 --- a/build/enums.neon +++ b/build/enums.neon @@ -2,8 +2,14 @@ parameters: excludePaths: - ../tests/PHPStan/Fixture/TestEnum.php - ../tests/PHPStan/Fixture/AnotherTestEnum.php + - ../tests/PHPStan/Fixture/ManyCasesTestEnum.php ignoreErrors: - message: '#^Access to constant ONE on an unknown class EnumTypeAssertions\\Foo\.$#' path: ../tests/PHPStan/Analyser/NodeScopeResolverTest.php + - + message: '#^Class ObjectTypeEnums\\FooEnum not found\.$#' + paths: + - ../tests/PHPStan/Type/ObjectTypeTest.php + - ../tests/PHPStan/Type/IntersectionTypeTest.php diff --git a/build/even-more-enum-adapter-errors.neon b/build/even-more-enum-adapter-errors.neon index 64020aa676..364905f714 100644 --- a/build/even-more-enum-adapter-errors.neon +++ b/build/even-more-enum-adapter-errors.neon @@ -1,16 +1,2 @@ parameters: ignoreErrors: - - - message: "#^Strict comparison using \\!\\=\\= between class\\-string and 'UnitEnum' will always evaluate to true\\.$#" - count: 1 - path: ../src/Reflection/Php/PhpClassReflectionExtension.php - - - - message: "#^Access to property \\$name on an unknown class UnitEnum\\.$#" - count: 1 - path: ../src/Type/ConstantTypeHelper.php - - - - message: "#^PHPDoc tag @var for variable \\$value contains unknown class UnitEnum\\.$#" - count: 1 - path: ../src/Type/ConstantTypeHelper.php diff --git a/build/ignore-by-php-version.neon.php b/build/ignore-by-php-version.neon.php index 1e60597046..cd049179a5 100644 --- a/build/ignore-by-php-version.neon.php +++ b/build/ignore-by-php-version.neon.php @@ -24,12 +24,14 @@ $includes[] = __DIR__ . '/enum-adapter-errors.neon'; } -if (PHP_VERSION_ID >= 70300 && PHP_VERSION_ID < 80000) { +if (PHP_VERSION_ID < 80000) { $includes[] = __DIR__ . '/more-enum-adapter-errors.neon'; } if (PHP_VERSION_ID < 80000) { - $includes[] = __DIR__ . '/even-more-enum-adapter-errors.neon'; + $includes[] = __DIR__ . '/spl-autoload-functions-pre-php-7.neon'; +} else { + $includes[] = __DIR__ . '/spl-autoload-functions-php-8.neon'; } $config = []; diff --git a/build/more-enum-adapter-errors.neon b/build/more-enum-adapter-errors.neon index 2af1ea4faf..69882bcfc5 100644 --- a/build/more-enum-adapter-errors.neon +++ b/build/more-enum-adapter-errors.neon @@ -4,3 +4,22 @@ parameters: message: "#^Parameter \\#1 \\$expected of method PHPUnit\\\\Framework\\\\Assert\\:\\:assertInstanceOf\\(\\) expects class\\-string\\, string given\\.$#" count: 1 path: ../tests/PHPStan/Reflection/ClassReflectionTest.php + - + message: "#^Strict comparison using \\!\\=\\= between class\\-string and 'UnitEnum' will always evaluate to true\\.$#" + count: 1 + path: ../src/Reflection/Php/PhpClassReflectionExtension.php + + - + message: "#^Access to property \\$name on an unknown class UnitEnum\\.$#" + count: 1 + path: ../src/Type/ConstantTypeHelper.php + + - + message: "#^PHPDoc tag @var for variable \\$value contains unknown class UnitEnum\\.$#" + count: 1 + path: ../src/Type/ConstantTypeHelper.php + + - + message: "#^Class BackedEnum not found\\.$#" + count: 1 + path: ../src/Type/Php/BackedEnumFromMethodDynamicReturnTypeExtension.php diff --git a/build/phpstan.neon b/build/phpstan.neon index baad4edee6..511d5735c7 100644 --- a/build/phpstan.neon +++ b/build/phpstan.neon @@ -1,12 +1,12 @@ includes: - ../vendor/phpstan/phpstan-deprecation-rules/rules.neon - ../vendor/phpstan/phpstan-nette/rules.neon - - ../vendor/phpstan/phpstan-php-parser/extension.neon - ../vendor/phpstan/phpstan-phpunit/extension.neon - ../vendor/phpstan/phpstan-phpunit/rules.neon - ../vendor/phpstan/phpstan-strict-rules/rules.neon - ../conf/bleedingEdge.neon - ../phpstan-baseline.neon + - ../phpstan-baseline.php - ignore-by-php-version.neon.php - ignore-by-architecture.neon.php parameters: @@ -65,8 +65,7 @@ parameters: - 'PHPStan\Type\CircularTypeAliasDefinitionException' - 'PHPStan\Broker\ClassAutoloadingException' - 'LogicException' - - 'TypeError' - - 'DivisionByZeroError' + - 'Error' check: missingCheckedExceptionInThrows: true tooWideThrowType: true @@ -82,13 +81,14 @@ parameters: message: '#Fetching class constant class of deprecated class DeprecatedAnnotations\\DeprecatedWithMultipleTags.#' path: ../tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php - - message: '#^Variable property access on PHPStan\\Rules\\RuleError\.$#' + message: '#^Variable property access on T of PHPStan\\Rules\\RuleError\.$#' path: ../src/Rules/RuleErrorBuilder.php - message: "#^Parameter \\#1 (?:\\$argument|\\$objectOrClass) of class ReflectionClass constructor expects class\\-string\\\\|PHPStan\\\\ExtensionInstaller\\\\GeneratedConfig, string given\\.$#" count: 1 path: ../src/Command/CommandHelper.php - '#^Parameter \#1 \$offsetType of class PHPStan\\Type\\Accessory\\HasOffsetType constructor expects PHPStan\\Type\\Constant\\ConstantIntegerType\|PHPStan\\Type\\Constant\\ConstantStringType#' + - '#^Short ternary operator is not allowed#' reportStaticMethodSignatures: true tmpDir: %rootDir%/tmp stubFiles: @@ -104,19 +104,3 @@ services: class: PHPStan\Internal\ContainerDynamicReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension - - scopeIsInClass: - class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension - arguments: - isInMethodName: isInClass - removeNullMethodName: getClassReflection - tags: - - phpstan.typeSpecifier.methodTypeSpecifyingExtension - - scopeIsInTrait: - class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension - arguments: - isInMethodName: isInTrait - removeNullMethodName: getTraitReflection - tags: - - phpstan.typeSpecifier.methodTypeSpecifyingExtension diff --git a/build/rector-cache-files-hash.php b/build/rector-cache-files-hash.php deleted file mode 100644 index 05cf12938f..0000000000 --- a/build/rector-cache-files-hash.php +++ /dev/null @@ -1,8 +0,0 @@ -getOriginalFilesHash(); diff --git a/build/rector-downgrade.php b/build/rector-downgrade.php deleted file mode 100644 index e3e5e5627d..0000000000 --- a/build/rector-downgrade.php +++ /dev/null @@ -1,56 +0,0 @@ -paths($cache->restore()); - $config->phpVersion($targetPhpVersionId); - $config->skip(RectorCache::SKIP_PATHS); - $config->disableParallel(); - - if ($targetPhpVersionId < 80100) { - $config->rule(DowngradeReadonlyPropertyRector::class); - $config->rule(DowngradePureIntersectionTypeRector::class); - } - - if ($targetPhpVersionId < 80000) { - $config->rule(DowngradeTrailingCommasInParamUseRector::class); - $config->rule(DowngradeNonCapturingCatchesRector::class); - $config->rule(DowngradeUnionTypeTypedPropertyRector::class); - $config->rule(DowngradePropertyPromotionRector::class); - $config->rule(DowngradeUnionTypeDeclarationRector::class); - $config->rule(DowngradeMixedTypeDeclarationRector::class); - } - - if ($targetPhpVersionId < 70400) { - $config->rule(DowngradeTypedPropertyRector::class); - $config->rule(DowngradeNullCoalescingOperatorRector::class); - $config->rule(ArrowFunctionToAnonymousFunctionRector::class); - } - - if ($targetPhpVersionId < 70300) { - $config->rule(DowngradeTrailingCommasInFunctionCallsRector::class); - } -}; diff --git a/build/save-rector-cache.php b/build/save-rector-cache.php deleted file mode 100644 index eae2ceaaba..0000000000 --- a/build/save-rector-cache.php +++ /dev/null @@ -1,8 +0,0 @@ -save(); diff --git a/build/save-rector-hashes.php b/build/save-rector-hashes.php deleted file mode 100644 index 3ff7422a84..0000000000 --- a/build/save-rector-hashes.php +++ /dev/null @@ -1,8 +0,0 @@ -saveHashes(); diff --git a/build/spl-autoload-functions-php-8.neon b/build/spl-autoload-functions-php-8.neon new file mode 100644 index 0000000000..b534d6c94f --- /dev/null +++ b/build/spl-autoload-functions-php-8.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: "#^PHPDoc tag @var with type array\\\\|false is not subtype of native type array\\.$#" + count: 2 + path: ../src/Command/CommandHelper.php diff --git a/build/spl-autoload-functions-pre-php-7.neon b/build/spl-autoload-functions-pre-php-7.neon new file mode 100644 index 0000000000..abafcfbf08 --- /dev/null +++ b/build/spl-autoload-functions-pre-php-7.neon @@ -0,0 +1,10 @@ +parameters: + ignoreErrors: + - + message: "#^PHPDoc tag @var with type array\\\\|false is not subtype of native type list\\\\|false\\.$#" + count: 2 + path: ../src/Command/CommandHelper.php + + - + message: '#^Parameter \#1 \$array \(list\) of array_values is already a list, call has no effect\.$#' + path: ../src/Type/TypeCombinator.php diff --git a/build/transform-source b/build/transform-source deleted file mode 100755 index 78ff336eff..0000000000 --- a/build/transform-source +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -set -o errexit -set -o pipefail -set -o nounset - -export TARGET_PHP_VERSION=$1 - -php ./build/save-rector-hashes.php - -vendor/bin/rector process -c build/rector-downgrade.php --no-diffs - -php ./build/save-rector-cache.php diff --git a/changelog-generator/composer.json b/changelog-generator/composer.json index d72199eea5..d4527f1c45 100644 --- a/changelog-generator/composer.json +++ b/changelog-generator/composer.json @@ -13,5 +13,10 @@ "psr-4": { "PHPStan\\ChangelogGenerator\\": "src" } + }, + "config": { + "allow-plugins": { + "php-http/discovery": true + } } } diff --git a/changelog-generator/composer.lock b/changelog-generator/composer.lock index 102869a3d4..3b830c7c5d 100644 --- a/changelog-generator/composer.lock +++ b/changelog-generator/composer.lock @@ -74,22 +74,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.4.5", + "version": "7.8.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "1dd98b0564cb3f6bd16ce683cb755f94c10fbd82" + "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/1dd98b0564cb3f6bd16ce683cb755f94c10fbd82", - "reference": "1dd98b0564cb3f6bd16ce683cb755f94c10fbd82", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/1110f66a6530a40fe7aea0378fe608ee2b2248f9", + "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5", - "guzzlehttp/psr7": "^1.9 || ^2.4", + "guzzlehttp/promises": "^1.5.3 || ^2.0.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -98,10 +98,11 @@ "psr/http-client-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", + "bamarni/composer-bin-plugin": "^1.8.1", "ext-curl": "*", - "php-http/client-integration-tests": "^3.0", - "phpunit/phpunit": "^8.5.5 || ^9.3.5", + "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.29 || ^9.5.23", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -111,8 +112,9 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "7.4-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": false } }, "autoload": { @@ -178,7 +180,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.4.5" + "source": "https://github.com/guzzle/guzzle/tree/7.8.0" }, "funding": [ { @@ -194,38 +196,37 @@ "type": "tidelift" } ], - "time": "2022-06-20T22:16:13+00:00" + "time": "2023-08-27T10:20:53+00:00" }, { "name": "guzzlehttp/promises", - "version": "1.5.1", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da" + "reference": "111166291a0f8130081195ac4556a5587d7f1b5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da", - "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "url": "https://api.github.com/repos/guzzle/promises/zipball/111166291a0f8130081195ac4556a5587d7f1b5d", + "reference": "111166291a0f8130081195ac4556a5587d7f1b5d", "shasum": "" }, "require": { - "php": ">=5.5" + "php": "^7.2.5 || ^8.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4 || ^5.1" + "bamarni/composer-bin-plugin": "^1.8.1", + "phpunit/phpunit": "^8.5.29 || ^9.5.23" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.5-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": false } }, "autoload": { - "files": [ - "src/functions_include.php" - ], "psr-4": { "GuzzleHttp\\Promise\\": "src/" } @@ -262,7 +263,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.5.1" + "source": "https://github.com/guzzle/promises/tree/2.0.1" }, "funding": [ { @@ -278,26 +279,26 @@ "type": "tidelift" } ], - "time": "2021-10-22T20:56:57+00:00" + "time": "2023-08-03T15:11:55+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.4.0", + "version": "2.6.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "13388f00956b1503577598873fffb5ae994b5737" + "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/13388f00956b1503577598873fffb5ae994b5737", - "reference": "13388f00956b1503577598873fffb5ae994b5737", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/be45764272e8873c72dbe3d2edcfdfcc3bc9f727", + "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", "psr/http-factory": "^1.0", - "psr/http-message": "^1.0", + "psr/http-message": "^1.1 || ^2.0", "ralouphie/getallheaders": "^3.0" }, "provide": { @@ -305,17 +306,18 @@ "psr/http-message-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", + "bamarni/composer-bin-plugin": "^1.8.1", "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.8 || ^9.3.10" + "phpunit/phpunit": "^8.5.29 || ^9.5.23" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.4-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": false } }, "autoload": { @@ -377,7 +379,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.4.0" + "source": "https://github.com/guzzle/psr7/tree/2.6.1" }, "funding": [ { @@ -393,7 +395,7 @@ "type": "tidelift" } ], - "time": "2022-06-20T21:43:11+00:00" + "time": "2023-08-27T10:13:57+00:00" }, { "name": "http-interop/http-factory-guzzle", @@ -455,16 +457,16 @@ }, { "name": "knplabs/github-api", - "version": "v3.7.0", + "version": "v3.13.0", "source": { "type": "git", "url": "https://github.com/KnpLabs/php-github-api.git", - "reference": "c230ab0162afeaec4318276e6a470af4b52da5fe" + "reference": "47024f3483520c0fafdfc5c10d2a20d87b4c7ceb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/KnpLabs/php-github-api/zipball/c230ab0162afeaec4318276e6a470af4b52da5fe", - "reference": "c230ab0162afeaec4318276e6a470af4b52da5fe", + "url": "https://api.github.com/repos/KnpLabs/php-github-api/zipball/47024f3483520c0fafdfc5c10d2a20d87b4c7ceb", + "reference": "47024f3483520c0fafdfc5c10d2a20d87b4c7ceb", "shasum": "" }, "require": { @@ -478,7 +480,7 @@ "psr/cache": "^1.0|^2.0|^3.0", "psr/http-client-implementation": "^1.0", "psr/http-factory-implementation": "^1.0", - "psr/http-message": "^1.0", + "psr/http-message": "^1.0|^2.0", "symfony/deprecation-contracts": "^2.2|^3.0", "symfony/polyfill-php80": "^1.17" }, @@ -498,7 +500,7 @@ "extra": { "branch-alias": { "dev-2.x": "2.20.x-dev", - "dev-master": "3.4.x-dev" + "dev-master": "3.12-dev" } }, "autoload": { @@ -531,7 +533,7 @@ ], "support": { "issues": "https://github.com/KnpLabs/php-github-api/issues", - "source": "https://github.com/KnpLabs/php-github-api/tree/v3.7.0" + "source": "https://github.com/KnpLabs/php-github-api/tree/v3.13.0" }, "funding": [ { @@ -539,20 +541,20 @@ "type": "github" } ], - "time": "2022-06-12T17:59:07+00:00" + "time": "2023-11-19T21:08:19+00:00" }, { "name": "php-http/cache-plugin", - "version": "1.7.5", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/php-http/cache-plugin.git", - "reference": "63bc3f7242825c9a817db8f78e4c9703b0c471e2" + "reference": "6bf9fbf66193f61d90c2381b75eb1fa0202fd314" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/cache-plugin/zipball/63bc3f7242825c9a817db8f78e4c9703b0c471e2", - "reference": "63bc3f7242825c9a817db8f78e4c9703b0c471e2", + "url": "https://api.github.com/repos/php-http/cache-plugin/zipball/6bf9fbf66193f61d90c2381b75eb1fa0202fd314", + "reference": "6bf9fbf66193f61d90c2381b75eb1fa0202fd314", "shasum": "" }, "require": { @@ -563,14 +565,9 @@ "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0 || ^6.0" }, "require-dev": { - "phpspec/phpspec": "^5.1 || ^6.0" + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.6-dev" - } - }, "autoload": { "psr-4": { "Http\\Client\\Common\\Plugin\\": "src/" @@ -596,32 +593,31 @@ ], "support": { "issues": "https://github.com/php-http/cache-plugin/issues", - "source": "https://github.com/php-http/cache-plugin/tree/1.7.5" + "source": "https://github.com/php-http/cache-plugin/tree/1.8.0" }, - "time": "2022-01-18T12:24:56+00:00" + "time": "2023-04-28T10:56:55+00:00" }, { "name": "php-http/client-common", - "version": "2.5.0", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/php-http/client-common.git", - "reference": "d135751167d57e27c74de674d6a30cef2dc8e054" + "reference": "880509727a447474d2a71b7d7fa5d268ddd3db4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/client-common/zipball/d135751167d57e27c74de674d6a30cef2dc8e054", - "reference": "d135751167d57e27c74de674d6a30cef2dc8e054", + "url": "https://api.github.com/repos/php-http/client-common/zipball/880509727a447474d2a71b7d7fa5d268ddd3db4b", + "reference": "880509727a447474d2a71b7d7fa5d268ddd3db4b", "shasum": "" }, "require": { "php": "^7.1 || ^8.0", "php-http/httplug": "^2.0", "php-http/message": "^1.6", - "php-http/message-factory": "^1.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", - "psr/http-message": "^1.0", + "psr/http-message": "^1.0 || ^2.0", "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0", "symfony/polyfill-php80": "^1.17" }, @@ -631,7 +627,7 @@ "nyholm/psr7": "^1.2", "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", "phpspec/prophecy": "^1.10.2", - "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3" + "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" }, "suggest": { "ext-json": "To detect JSON responses with the ContentTypePlugin", @@ -641,11 +637,6 @@ "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.3.x-dev" - } - }, "autoload": { "psr-4": { "Http\\Client\\Common\\": "src/" @@ -671,49 +662,59 @@ ], "support": { "issues": "https://github.com/php-http/client-common/issues", - "source": "https://github.com/php-http/client-common/tree/2.5.0" + "source": "https://github.com/php-http/client-common/tree/2.7.0" }, - "time": "2021-11-26T15:01:24+00:00" + "time": "2023-05-17T06:46:59+00:00" }, { "name": "php-http/discovery", - "version": "1.14.3", + "version": "1.19.1", "source": { "type": "git", "url": "https://github.com/php-http/discovery.git", - "reference": "31d8ee46d0215108df16a8527c7438e96a4d7735" + "reference": "57f3de01d32085fea20865f9b16fb0e69347c39e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/discovery/zipball/31d8ee46d0215108df16a8527c7438e96a4d7735", - "reference": "31d8ee46d0215108df16a8527c7438e96a4d7735", + "url": "https://api.github.com/repos/php-http/discovery/zipball/57f3de01d32085fea20865f9b16fb0e69347c39e", + "reference": "57f3de01d32085fea20865f9b16fb0e69347c39e", "shasum": "" }, "require": { + "composer-plugin-api": "^1.0|^2.0", "php": "^7.1 || ^8.0" }, "conflict": { - "nyholm/psr7": "<1.0" + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" }, "require-dev": { + "composer/composer": "^1.0.2|^2.0", "graham-campbell/phpspec-skip-example-extension": "^5.0", "php-http/httplug": "^1.0 || ^2.0", "php-http/message-factory": "^1.0", - "phpspec/phpspec": "^5.1 || ^6.1" - }, - "suggest": { - "php-http/message": "Allow to use Guzzle, Diactoros or Slim Framework factories" + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "symfony/phpunit-bridge": "^6.2" }, - "type": "library", + "type": "composer-plugin", "extra": { - "branch-alias": { - "dev-master": "1.9-dev" - } + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true }, "autoload": { "psr-4": { "Http\\Discovery\\": "src/" - } + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -725,7 +726,7 @@ "email": "mark.sagikazar@gmail.com" } ], - "description": "Finds installed HTTPlug implementations and PSR-7 message factories", + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", "homepage": "http://php-http.org", "keywords": [ "adapter", @@ -734,44 +735,40 @@ "factory", "http", "message", + "psr17", "psr7" ], "support": { "issues": "https://github.com/php-http/discovery/issues", - "source": "https://github.com/php-http/discovery/tree/1.14.3" + "source": "https://github.com/php-http/discovery/tree/1.19.1" }, - "time": "2022-07-11T14:04:40+00:00" + "time": "2023-07-11T07:02:26+00:00" }, { "name": "php-http/httplug", - "version": "2.3.0", + "version": "2.4.0", "source": { "type": "git", "url": "https://github.com/php-http/httplug.git", - "reference": "f640739f80dfa1152533976e3c112477f69274eb" + "reference": "625ad742c360c8ac580fcc647a1541d29e257f67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/httplug/zipball/f640739f80dfa1152533976e3c112477f69274eb", - "reference": "f640739f80dfa1152533976e3c112477f69274eb", + "url": "https://api.github.com/repos/php-http/httplug/zipball/625ad742c360c8ac580fcc647a1541d29e257f67", + "reference": "625ad742c360c8ac580fcc647a1541d29e257f67", "shasum": "" }, "require": { "php": "^7.1 || ^8.0", "php-http/promise": "^1.1", "psr/http-client": "^1.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "require-dev": { - "friends-of-phpspec/phpspec-code-coverage": "^4.1", - "phpspec/phpspec": "^5.1 || ^6.0" + "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, "autoload": { "psr-4": { "Http\\Client\\": "src/" @@ -800,29 +797,28 @@ ], "support": { "issues": "https://github.com/php-http/httplug/issues", - "source": "https://github.com/php-http/httplug/tree/2.3.0" + "source": "https://github.com/php-http/httplug/tree/2.4.0" }, - "time": "2022-02-21T09:52:22+00:00" + "time": "2023-04-14T15:10:03+00:00" }, { "name": "php-http/message", - "version": "1.13.0", + "version": "1.16.0", "source": { "type": "git", "url": "https://github.com/php-http/message.git", - "reference": "7886e647a30a966a1a8d1dad1845b71ca8678361" + "reference": "47a14338bf4ebd67d317bf1144253d7db4ab55fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/message/zipball/7886e647a30a966a1a8d1dad1845b71ca8678361", - "reference": "7886e647a30a966a1a8d1dad1845b71ca8678361", + "url": "https://api.github.com/repos/php-http/message/zipball/47a14338bf4ebd67d317bf1144253d7db4ab55fd", + "reference": "47a14338bf4ebd67d317bf1144253d7db4ab55fd", "shasum": "" }, "require": { "clue/stream-filter": "^1.5", - "php": "^7.1 || ^8.0", - "php-http/message-factory": "^1.0.2", - "psr/http-message": "^1.0" + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.1 || ^2.0" }, "provide": { "php-http/message-factory-implementation": "1.0" @@ -830,8 +826,9 @@ "require-dev": { "ergebnis/composer-normalize": "^2.6", "ext-zlib": "*", - "guzzlehttp/psr7": "^1.0", - "laminas/laminas-diactoros": "^2.0", + "guzzlehttp/psr7": "^1.0 || ^2.0", + "laminas/laminas-diactoros": "^2.0 || ^3.0", + "php-http/message-factory": "^1.0.2", "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", "slim/slim": "^3.0" }, @@ -842,11 +839,6 @@ "slim/slim": "Used with Slim Framework PSR-7 implementation" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, "autoload": { "files": [ "src/filters.php" @@ -874,32 +866,32 @@ ], "support": { "issues": "https://github.com/php-http/message/issues", - "source": "https://github.com/php-http/message/tree/1.13.0" + "source": "https://github.com/php-http/message/tree/1.16.0" }, - "time": "2022-02-11T13:41:14+00:00" + "time": "2023-05-17T06:43:38+00:00" }, { "name": "php-http/message-factory", - "version": "v1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-http/message-factory.git", - "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1" + "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/message-factory/zipball/a478cb11f66a6ac48d8954216cfed9aa06a501a1", - "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1", + "url": "https://api.github.com/repos/php-http/message-factory/zipball/4d8778e1c7d405cbb471574821c1ff5b68cc8f57", + "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57", "shasum": "" }, "require": { "php": ">=5.4", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "1.x-dev" } }, "autoload": { @@ -928,42 +920,37 @@ ], "support": { "issues": "https://github.com/php-http/message-factory/issues", - "source": "https://github.com/php-http/message-factory/tree/master" + "source": "https://github.com/php-http/message-factory/tree/1.1.0" }, - "time": "2015-12-19T14:08:53+00:00" + "abandoned": "psr/http-factory", + "time": "2023-04-14T14:16:17+00:00" }, { "name": "php-http/multipart-stream-builder", - "version": "1.2.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/php-http/multipart-stream-builder.git", - "reference": "11c1d31f72e01c738bbce9e27649a7cca829c30e" + "reference": "f5938fd135d9fa442cc297dc98481805acfe2b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/11c1d31f72e01c738bbce9e27649a7cca829c30e", - "reference": "11c1d31f72e01c738bbce9e27649a7cca829c30e", + "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/f5938fd135d9fa442cc297dc98481805acfe2b6a", + "reference": "f5938fd135d9fa442cc297dc98481805acfe2b6a", "shasum": "" }, "require": { "php": "^7.1 || ^8.0", - "php-http/discovery": "^1.7", - "php-http/message-factory": "^1.0.2", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.0" + "php-http/discovery": "^1.15", + "psr/http-factory-implementation": "^1.0" }, "require-dev": { "nyholm/psr7": "^1.0", "php-http/message": "^1.5", + "php-http/message-factory": "^1.0.2", "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, "autoload": { "psr-4": { "Http\\Message\\MultipartStream\\": "src/" @@ -990,37 +977,32 @@ ], "support": { "issues": "https://github.com/php-http/multipart-stream-builder/issues", - "source": "https://github.com/php-http/multipart-stream-builder/tree/1.2.0" + "source": "https://github.com/php-http/multipart-stream-builder/tree/1.3.0" }, - "time": "2021-05-21T08:32:01+00:00" + "time": "2023-04-28T14:10:22+00:00" }, { "name": "php-http/promise", - "version": "1.1.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/php-http/promise.git", - "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88" + "reference": "44a67cb59f708f826f3bec35f22030b3edb90119" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/promise/zipball/4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", - "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", + "url": "https://api.github.com/repos/php-http/promise/zipball/44a67cb59f708f826f3bec35f22030b3edb90119", + "reference": "44a67cb59f708f826f3bec35f22030b3edb90119", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "friends-of-phpspec/phpspec-code-coverage": "^4.3.2", - "phpspec/phpspec": "^5.1.2 || ^6.2" + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3", + "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, "autoload": { "psr-4": { "Http\\Promise\\": "src/" @@ -1047,9 +1029,9 @@ ], "support": { "issues": "https://github.com/php-http/promise/issues", - "source": "https://github.com/php-http/promise/tree/1.1.0" + "source": "https://github.com/php-http/promise/tree/1.2.1" }, - "time": "2020-07-07T09:29:14+00:00" + "time": "2023-11-08T12:57:08+00:00" }, { "name": "psr/cache", @@ -1155,21 +1137,21 @@ }, { "name": "psr/http-client", - "version": "1.0.1", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/php-fig/http-client.git", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", "shasum": "" }, "require": { "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -1189,7 +1171,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP clients", @@ -1201,27 +1183,27 @@ "psr-18" ], "support": { - "source": "https://github.com/php-fig/http-client/tree/master" + "source": "https://github.com/php-fig/http-client" }, - "time": "2020-06-29T06:28:15+00:00" + "time": "2023-09-23T14:17:50+00:00" }, { "name": "psr/http-factory", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + "reference": "e616d01114759c4c489f93b099585439f795fe35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", + "reference": "e616d01114759c4c489f93b099585439f795fe35", "shasum": "" }, "require": { "php": ">=7.0.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -1241,7 +1223,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interfaces for PSR-7 HTTP message factories", @@ -1256,31 +1238,31 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/master" + "source": "https://github.com/php-fig/http-factory/tree/1.0.2" }, - "time": "2019-04-30T12:38:16+00:00" + "time": "2023-04-10T20:10:41+00:00" }, { "name": "psr/http-message", - "version": "1.0.1", + "version": "2.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -1295,7 +1277,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP messages", @@ -1309,9 +1291,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/master" + "source": "https://github.com/php-fig/http-message/tree/2.0" }, - "time": "2016-08-06T14:39:51+00:00" + "time": "2023-04-04T09:54:51+00:00" }, { "name": "ralouphie/getallheaders", @@ -1359,23 +1341,23 @@ }, { "name": "symfony/console", - "version": "v6.1.2", + "version": "v6.3.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "7a86c1c42fbcb69b59768504c7bca1d3767760b7" + "reference": "0d14a9f6d04d4ac38a8cea1171f4554e325dae92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/7a86c1c42fbcb69b59768504c7bca1d3767760b7", - "reference": "7a86c1c42fbcb69b59768504c7bca1d3767760b7", + "url": "https://api.github.com/repos/symfony/console/zipball/0d14a9f6d04d4ac38a8cea1171f4554e325dae92", + "reference": "0d14a9f6d04d4ac38a8cea1171f4554e325dae92", "shasum": "" }, "require": { "php": ">=8.1", - "symfony/deprecation-contracts": "^2.1|^3", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^1.1|^2|^3", + "symfony/service-contracts": "^2.5|^3", "symfony/string": "^5.4|^6.0" }, "conflict": { @@ -1397,12 +1379,6 @@ "symfony/process": "^5.4|^6.0", "symfony/var-dumper": "^5.4|^6.0" }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" - }, "type": "library", "autoload": { "psr-4": { @@ -1430,12 +1406,12 @@ "homepage": "https://symfony.com", "keywords": [ "cli", - "command line", + "command-line", "console", "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.1.2" + "source": "https://github.com/symfony/console/tree/v6.3.8" }, "funding": [ { @@ -1451,20 +1427,20 @@ "type": "tidelift" } ], - "time": "2022-06-26T13:01:30+00:00" + "time": "2023-10-31T08:09:35+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.1.1", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918" + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", - "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", "shasum": "" }, "require": { @@ -1473,7 +1449,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.1-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -1502,7 +1478,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" }, "funding": [ { @@ -1518,25 +1494,25 @@ "type": "tidelift" } ], - "time": "2022-02-25T11:15:52+00:00" + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/options-resolver", - "version": "v6.1.0", + "version": "v6.3.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "a3016f5442e28386ded73c43a32a5b68586dd1c4" + "reference": "a10f19f5198d589d5c33333cffe98dc9820332dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/a3016f5442e28386ded73c43a32a5b68586dd1c4", - "reference": "a3016f5442e28386ded73c43a32a5b68586dd1c4", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/a10f19f5198d589d5c33333cffe98dc9820332dd", + "reference": "a10f19f5198d589d5c33333cffe98dc9820332dd", "shasum": "" }, "require": { "php": ">=8.1", - "symfony/deprecation-contracts": "^2.1|^3" + "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", "autoload": { @@ -1569,7 +1545,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v6.1.0" + "source": "https://github.com/symfony/options-resolver/tree/v6.3.0" }, "funding": [ { @@ -1585,20 +1561,20 @@ "type": "tidelift" } ], - "time": "2022-02-25T11:15:52+00:00" + "time": "2023-05-12T14:21:09+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.26.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", - "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", "shasum": "" }, "require": { @@ -1613,7 +1589,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1651,7 +1627,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" }, "funding": [ { @@ -1667,20 +1643,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.26.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "433d05519ce6990bf3530fba6957499d327395c2" + "reference": "875e90aeea2777b6f135677f618529449334a612" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2", - "reference": "433d05519ce6990bf3530fba6957499d327395c2", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", "shasum": "" }, "require": { @@ -1692,7 +1668,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1732,7 +1708,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" }, "funding": [ { @@ -1748,20 +1724,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.26.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "219aa369ceff116e673852dce47c3a41794c14bd" + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", - "reference": "219aa369ceff116e673852dce47c3a41794c14bd", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", "shasum": "" }, "require": { @@ -1773,7 +1749,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1816,7 +1792,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" }, "funding": [ { @@ -1832,20 +1808,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.26.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" + "reference": "42292d99c55abe617799667f454222c54c60e229" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", - "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", "shasum": "" }, "require": { @@ -1860,7 +1836,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1899,7 +1875,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" }, "funding": [ { @@ -1915,20 +1891,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2023-07-28T09:04:16+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.26.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace" + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace", - "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", "shasum": "" }, "require": { @@ -1937,7 +1913,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1982,7 +1958,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" }, "funding": [ { @@ -1998,20 +1974,20 @@ "type": "tidelift" } ], - "time": "2022-05-10T07:21:04+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.1.1", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239" + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/925e713fe8fcacf6bc05e936edd8dd5441a21239", - "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", "shasum": "" }, "require": { @@ -2021,13 +1997,10 @@ "conflict": { "ext-psr": "<1.1|>=2" }, - "suggest": { - "symfony/service-implementation": "" - }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.1-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -2067,7 +2040,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.1.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.3.0" }, "funding": [ { @@ -2083,20 +2056,20 @@ "type": "tidelift" } ], - "time": "2022-05-30T19:18:58+00:00" + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/string", - "version": "v6.1.2", + "version": "v6.3.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "1903f2879875280c5af944625e8246d81c2f0604" + "reference": "13880a87790c76ef994c91e87efb96134522577a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/1903f2879875280c5af944625e8246d81c2f0604", - "reference": "1903f2879875280c5af944625e8246d81c2f0604", + "url": "https://api.github.com/repos/symfony/string/zipball/13880a87790c76ef994c91e87efb96134522577a", + "reference": "13880a87790c76ef994c91e87efb96134522577a", "shasum": "" }, "require": { @@ -2107,12 +2080,13 @@ "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "symfony/translation-contracts": "<2.0" + "symfony/translation-contracts": "<2.5" }, "require-dev": { "symfony/error-handler": "^5.4|^6.0", "symfony/http-client": "^5.4|^6.0", - "symfony/translation-contracts": "^2.0|^3.0", + "symfony/intl": "^6.2", + "symfony/translation-contracts": "^2.5|^3.0", "symfony/var-exporter": "^5.4|^6.0" }, "type": "library", @@ -2152,7 +2126,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.1.2" + "source": "https://github.com/symfony/string/tree/v6.3.8" }, "funding": [ { @@ -2168,7 +2142,7 @@ "type": "tidelift" } ], - "time": "2022-06-26T16:35:04+00:00" + "time": "2023-11-09T08:28:21+00:00" } ], "packages-dev": [], @@ -2181,5 +2155,5 @@ "php": "^8.1" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/changelog-generator/phpstan.neon b/changelog-generator/phpstan.neon index 883d31af7b..d187d34c96 100644 --- a/changelog-generator/phpstan.neon +++ b/changelog-generator/phpstan.neon @@ -1,5 +1,14 @@ +includes: + - ../vendor/phpstan/phpstan-deprecation-rules/rules.neon + - ../vendor/phpstan/phpstan-nette/rules.neon + - ../vendor/phpstan/phpstan-phpunit/extension.neon + - ../vendor/phpstan/phpstan-phpunit/rules.neon + - ../vendor/phpstan/phpstan-strict-rules/rules.neon + - ../conf/bleedingEdge.neon + parameters: level: 8 paths: - src - run.php + checkGenericClassInNonGenericObjectType: false diff --git a/changelog-generator/run.php b/changelog-generator/run.php index beabb69288..fc709b521a 100755 --- a/changelog-generator/run.php +++ b/changelog-generator/run.php @@ -14,6 +14,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use function array_map; use function array_merge; @@ -34,6 +35,8 @@ protected function configure(): void $this->setName('run'); $this->addArgument('fromCommit', InputArgument::REQUIRED); $this->addArgument('toCommit', InputArgument::REQUIRED); + $this->addOption('exclude-branch', null, InputOption::VALUE_REQUIRED); + $this->addOption('include-headings', null, InputOption::VALUE_NONE); } protected function execute(InputInterface $input, OutputInterface $output) @@ -51,7 +54,17 @@ protected function execute(InputInterface $input, OutputInterface $output) /** @var Search $searchApi */ $searchApi = $gitHubClient->api('search'); - $commitLines = $this->exec(['git', 'log', sprintf('%s..%s', $input->getArgument('fromCommit'), $input->getArgument('toCommit')), '--reverse', '--pretty=%H %s']); + $command = ['git', 'log', sprintf('%s..%s', $input->getArgument('fromCommit'), $input->getArgument('toCommit'))]; + $excludeBranch = $input->getOption('exclude-branch'); + if ($excludeBranch !== null) { + $command[] = '--not'; + $command[] = $excludeBranch; + $command[] = '--no-merges'; + } + $command[] = '--reverse'; + $command[] = '--pretty=%H %s'; + + $commitLines = $this->exec($command); $commits = array_map(static function (string $line): array { [$hash, $message] = explode(' ', $line, 2); @@ -61,6 +74,41 @@ protected function execute(InputInterface $input, OutputInterface $output) ]; }, explode("\n", $commitLines)); + if ($input->getOption('include-headings') === true) { + $output->writeln(<<<'MARKDOWN' + Major new features 🚀 + ===================== + + Bleeding edge 🔪 + ===================== + + * + + *If you want to see the shape of things to come and adopt bleeding edge features early, you can include this config file in your project's `phpstan.neon`:* + + ``` + includes: + - vendor/phpstan/phpstan/conf/bleedingEdge.neon + ``` + + *Of course, there are no backwards compatibility guarantees when you include this file. The behaviour and reported errors can change in minor versions with this file included. [Learn more](https://phpstan.org/blog/what-is-bleeding-edge)* + + Improvements 🔧 + ===================== + + Bugfixes 🐛 + ===================== + + Function signature fixes 🤖 + ======================= + + Internals 🔍 + ===================== + + + MARKDOWN); + } + foreach ($commits as $commit) { $pullRequests = $searchApi->issues(sprintf('repo:phpstan/phpstan-src %s', $commit['hash'])); $issues = $searchApi->issues(sprintf('repo:phpstan/phpstan %s', $commit['hash']), 'created'); diff --git a/compiler/build/scoper.inc.php b/compiler/build/scoper.inc.php index 11df14200d..0ea6df31ec 100644 --- a/compiler/build/scoper.inc.php +++ b/compiler/build/scoper.inc.php @@ -17,11 +17,11 @@ '../../vendor/jetbrains/phpstorm-stubs', '../../vendor/phpstan/php-8-stubs/stubs', '../../vendor/symfony/polyfill-php80', + '../../vendor/symfony/polyfill-php81', '../../vendor/symfony/polyfill-mbstring', '../../vendor/symfony/polyfill-intl-normalizer', '../../vendor/symfony/polyfill-php73', '../../vendor/symfony/polyfill-php74', - '../../vendor/symfony/polyfill-php72', '../../vendor/symfony/polyfill-intl-grapheme', ]) as $file) { if ($file->getPathName() === '../../vendor/jetbrains/phpstorm-stubs/PhpStormStubsMap.php') { @@ -65,7 +65,7 @@ function (string $filePath, string $prefix, string $content): string { return str_replace('|Nette\\\\DI\\\\Statement', sprintf('|\\\\%s\\\\Nette\\\\DI\\\\Statement', $prefix), $content); }, function (string $filePath, string $prefix, string $content): string { - if ($filePath !== 'vendor/nette/di/src/DI/Config/DefinitionSchema.php') { + if ($filePath !== 'vendor/nette/di/src/DI/Extensions/DefinitionSchema.php') { return $content; } $content = str_replace( @@ -225,11 +225,11 @@ function (string $filePath, string $prefix, string $content): string { 'PhpParser', 'Hoa', 'Symfony\Polyfill\Php80', + 'Symfony\Polyfill\Php81', 'Symfony\Polyfill\Mbstring', 'Symfony\Polyfill\Intl\Normalizer', 'Symfony\Polyfill\Php73', 'Symfony\Polyfill\Php74', - 'Symfony\Polyfill\Php72', 'Symfony\Polyfill\Intl\Grapheme', ], 'expose-global-functions' => false, diff --git a/compiler/composer.lock b/compiler/composer.lock index 65427d6de7..d970a7b6f7 100644 --- a/compiler/composer.lock +++ b/compiler/composer.lock @@ -8,25 +8,25 @@ "packages": [ { "name": "nette/neon", - "version": "v3.3.3", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/nette/neon.git", - "reference": "22e384da162fab42961d48eb06c06d3ad0c11b95" + "reference": "372d945c156ee7f35c953339fb164538339e6283" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/neon/zipball/22e384da162fab42961d48eb06c06d3ad0c11b95", - "reference": "22e384da162fab42961d48eb06c06d3ad0c11b95", + "url": "https://api.github.com/repos/nette/neon/zipball/372d945c156ee7f35c953339fb164538339e6283", + "reference": "372d945c156ee7f35c953339fb164538339e6283", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=7.1" + "php": ">=8.0 <8.3" }, "require-dev": { - "nette/tester": "^2.0", - "phpstan/phpstan": "^0.12", + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.0", "tracy/tracy": "^2.7" }, "bin": [ @@ -35,7 +35,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.3-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -70,9 +70,9 @@ ], "support": { "issues": "https://github.com/nette/neon/issues", - "source": "https://github.com/nette/neon/tree/v3.3.3" + "source": "https://github.com/nette/neon/tree/v3.4.0" }, - "time": "2022-03-10T02:04:26+00:00" + "time": "2023-01-13T03:08:29+00:00" }, { "name": "psr/container", @@ -177,16 +177,16 @@ }, { "name": "symfony/console", - "version": "v6.0.12", + "version": "v6.0.19", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c5c2e313aa682530167c25077d6bdff36346251e" + "reference": "c3ebc83d031b71c39da318ca8b7a07ecc67507ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c5c2e313aa682530167c25077d6bdff36346251e", - "reference": "c5c2e313aa682530167c25077d6bdff36346251e", + "url": "https://api.github.com/repos/symfony/console/zipball/c3ebc83d031b71c39da318ca8b7a07ecc67507ed", + "reference": "c3ebc83d031b71c39da318ca8b7a07ecc67507ed", "shasum": "" }, "require": { @@ -252,7 +252,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.0.12" + "source": "https://github.com/symfony/console/tree/v6.0.19" }, "funding": [ { @@ -268,20 +268,20 @@ "type": "tidelift" } ], - "time": "2022-08-23T20:52:30+00:00" + "time": "2023-01-01T08:36:10+00:00" }, { "name": "symfony/filesystem", - "version": "v6.0.12", + "version": "v6.0.19", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "a36b782dc19dce3ab7e47d4b92b13cefb3511da3" + "reference": "3d49eec03fda1f0fc19b7349fbbe55ebc1004214" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/a36b782dc19dce3ab7e47d4b92b13cefb3511da3", - "reference": "a36b782dc19dce3ab7e47d4b92b13cefb3511da3", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/3d49eec03fda1f0fc19b7349fbbe55ebc1004214", + "reference": "3d49eec03fda1f0fc19b7349fbbe55ebc1004214", "shasum": "" }, "require": { @@ -315,7 +315,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.0.12" + "source": "https://github.com/symfony/filesystem/tree/v6.0.19" }, "funding": [ { @@ -331,20 +331,20 @@ "type": "tidelift" } ], - "time": "2022-08-02T16:01:06+00:00" + "time": "2023-01-20T17:44:14+00:00" }, { "name": "symfony/finder", - "version": "v6.0.11", + "version": "v6.0.19", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "09cb683ba5720385ea6966e5e06be2a34f2568b1" + "reference": "5cc9cac6586fc0c28cd173780ca696e419fefa11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/09cb683ba5720385ea6966e5e06be2a34f2568b1", - "reference": "09cb683ba5720385ea6966e5e06be2a34f2568b1", + "url": "https://api.github.com/repos/symfony/finder/zipball/5cc9cac6586fc0c28cd173780ca696e419fefa11", + "reference": "5cc9cac6586fc0c28cd173780ca696e419fefa11", "shasum": "" }, "require": { @@ -376,7 +376,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.0.11" + "source": "https://github.com/symfony/finder/tree/v6.0.19" }, "funding": [ { @@ -392,20 +392,20 @@ "type": "tidelift" } ], - "time": "2022-07-29T07:39:48+00:00" + "time": "2023-01-20T17:44:14+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", - "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", "shasum": "" }, "require": { @@ -420,7 +420,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -458,7 +458,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" }, "funding": [ { @@ -474,20 +474,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "433d05519ce6990bf3530fba6957499d327395c2" + "reference": "511a08c03c1960e08a883f4cffcacd219b758354" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2", - "reference": "433d05519ce6990bf3530fba6957499d327395c2", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", + "reference": "511a08c03c1960e08a883f4cffcacd219b758354", "shasum": "" }, "require": { @@ -499,7 +499,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -539,7 +539,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" }, "funding": [ { @@ -555,20 +555,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "219aa369ceff116e673852dce47c3a41794c14bd" + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", - "reference": "219aa369ceff116e673852dce47c3a41794c14bd", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", "shasum": "" }, "require": { @@ -580,7 +580,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -623,7 +623,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" }, "funding": [ { @@ -639,20 +639,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", - "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", "shasum": "" }, "require": { @@ -667,7 +667,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -706,7 +706,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" }, "funding": [ { @@ -722,20 +722,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/process", - "version": "v6.0.11", + "version": "v6.0.19", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "44270a08ccb664143dede554ff1c00aaa2247a43" + "reference": "2114fd60f26a296cc403a7939ab91478475a33d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/44270a08ccb664143dede554ff1c00aaa2247a43", - "reference": "44270a08ccb664143dede554ff1c00aaa2247a43", + "url": "https://api.github.com/repos/symfony/process/zipball/2114fd60f26a296cc403a7939ab91478475a33d4", + "reference": "2114fd60f26a296cc403a7939ab91478475a33d4", "shasum": "" }, "require": { @@ -767,7 +767,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.0.11" + "source": "https://github.com/symfony/process/tree/v6.0.19" }, "funding": [ { @@ -783,7 +783,7 @@ "type": "tidelift" } ], - "time": "2022-06-27T17:10:44+00:00" + "time": "2023-01-01T08:36:10+00:00" }, { "name": "symfony/service-contracts", @@ -869,16 +869,16 @@ }, { "name": "symfony/string", - "version": "v6.0.12", + "version": "v6.0.19", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "3a975ba1a1508ad97df45f4590f55b7cc4c1a0a0" + "reference": "d9e72497367c23e08bf94176d2be45b00a9d232a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/3a975ba1a1508ad97df45f4590f55b7cc4c1a0a0", - "reference": "3a975ba1a1508ad97df45f4590f55b7cc4c1a0a0", + "url": "https://api.github.com/repos/symfony/string/zipball/d9e72497367c23e08bf94176d2be45b00a9d232a", + "reference": "d9e72497367c23e08bf94176d2be45b00a9d232a", "shasum": "" }, "require": { @@ -934,7 +934,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.0.12" + "source": "https://github.com/symfony/string/tree/v6.0.19" }, "funding": [ { @@ -950,36 +950,36 @@ "type": "tidelift" } ], - "time": "2022-08-12T18:05:20+00:00" + "time": "2023-01-01T08:36:10+00:00" } ], "packages-dev": [ { "name": "doctrine/instantiator", - "version": "1.4.1", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^9", + "doctrine/coding-standard": "^9 || ^11", "ext-pdo": "*", "ext-phar": "*", "phpbench/phpbench": "^0.16 || ^1", "phpstan/phpstan": "^1.4", "phpstan/phpstan-phpunit": "^1", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.22" + "vimeo/psalm": "^4.30 || ^5.4" }, "type": "library", "autoload": { @@ -1006,7 +1006,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.1" + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" }, "funding": [ { @@ -1022,20 +1022,20 @@ "type": "tidelift" } ], - "time": "2022-03-03T08:28:38+00:00" + "time": "2022-12-30T00:15:36+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.11.0", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", "shasum": "" }, "require": { @@ -1073,7 +1073,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" }, "funding": [ { @@ -1081,20 +1081,20 @@ "type": "tidelift" } ], - "time": "2022-03-03T13:19:32+00:00" + "time": "2023-03-08T13:26:56+00:00" }, { "name": "nikic/php-parser", - "version": "v4.15.1", + "version": "v4.17.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900" + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", - "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", "shasum": "" }, "require": { @@ -1135,9 +1135,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" }, - "time": "2022-09-04T07:30:47+00:00" + "time": "2023-08-13T19:53:39+00:00" }, { "name": "phar-io/manifest", @@ -1252,16 +1252,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.6.3", + "version": "1.10.15", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "6128620b98292e0b69ea6d799871d77163681c8e" + "reference": "762c4dac4da6f8756eebb80e528c3a47855da9bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6128620b98292e0b69ea6d799871d77163681c8e", - "reference": "6128620b98292e0b69ea6d799871d77163681c8e", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/762c4dac4da6f8756eebb80e528c3a47855da9bd", + "reference": "762c4dac4da6f8756eebb80e528c3a47855da9bd", "shasum": "" }, "require": { @@ -1285,9 +1285,16 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.6.3" + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" }, "funding": [ { @@ -1298,34 +1305,30 @@ "url": "https://github.com/phpstan", "type": "github" }, - { - "url": "https://www.patreon.com/phpstan", - "type": "patreon" - }, { "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", "type": "tidelift" } ], - "time": "2022-04-28T11:27:53+00:00" + "time": "2023-05-09T15:28:01+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "1.1.1", + "version": "1.3.13", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "4a3c437c09075736285d1cabb5c75bf27ed0bc84" + "reference": "d8bdab0218c5eb0964338d24a8511b65e9c94fa5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/4a3c437c09075736285d1cabb5c75bf27ed0bc84", - "reference": "4a3c437c09075736285d1cabb5c75bf27ed0bc84", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/d8bdab0218c5eb0964338d24a8511b65e9c94fa5", + "reference": "d8bdab0218c5eb0964338d24a8511b65e9c94fa5", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.5.0" + "phpstan/phpstan": "^1.10" }, "conflict": { "phpunit/phpunit": "<7.0" @@ -1357,29 +1360,29 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.1.1" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.13" }, - "time": "2022-04-20T15:24:25+00:00" + "time": "2023-05-26T11:05:59+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.17", + "version": "9.2.27", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8" + "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa94dc41e8661fe90c7316849907cba3007b10d8", - "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b0a88255cb70d52653d80c890bd7f38740ea50d1", + "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.14", + "nikic/php-parser": "^4.15", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -1394,8 +1397,8 @@ "phpunit/phpunit": "^9.3" }, "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", "extra": { @@ -1428,7 +1431,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.17" + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.27" }, "funding": [ { @@ -1436,7 +1440,7 @@ "type": "github" } ], - "time": "2022-08-30T12:24:04+00:00" + "time": "2023-07-26T13:44:30+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1681,20 +1685,20 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.25", + "version": "9.6.11", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d" + "reference": "810500e92855eba8a7a5319ae913be2da6f957b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d", - "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/810500e92855eba8a7a5319ae913be2da6f957b0", + "reference": "810500e92855eba8a7a5319ae913be2da6f957b0", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1", + "doctrine/instantiator": "^1.3.1 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", @@ -1723,8 +1727,8 @@ "sebastian/version": "^3.0.2" }, "suggest": { - "ext-soap": "*", - "ext-xdebug": "*" + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "bin": [ "phpunit" @@ -1732,7 +1736,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.5-dev" + "dev-master": "9.6-dev" } }, "autoload": { @@ -1763,7 +1767,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.25" + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.11" }, "funding": [ { @@ -1779,7 +1784,7 @@ "type": "tidelift" } ], - "time": "2022-09-25T03:44:45+00:00" + "time": "2023-08-19T07:10:56+00:00" }, { "name": "sebastian/cli-parser", @@ -2081,16 +2086,16 @@ }, { "name": "sebastian/diff", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", "shasum": "" }, "require": { @@ -2135,7 +2140,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" }, "funding": [ { @@ -2143,20 +2148,20 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2023-05-07T05:35:17+00:00" }, { "name": "sebastian/environment", - "version": "5.1.4", + "version": "5.1.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", "shasum": "" }, "require": { @@ -2198,7 +2203,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" }, "funding": [ { @@ -2206,7 +2211,7 @@ "type": "github" } ], - "time": "2022-04-03T09:37:03+00:00" + "time": "2023-02-03T06:03:51+00:00" }, { "name": "sebastian/exporter", @@ -2287,16 +2292,16 @@ }, { "name": "sebastian/global-state", - "version": "5.0.5", + "version": "5.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + "reference": "bde739e7565280bda77be70044ac1047bc007e34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", + "reference": "bde739e7565280bda77be70044ac1047bc007e34", "shasum": "" }, "require": { @@ -2339,7 +2344,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" }, "funding": [ { @@ -2347,7 +2352,7 @@ "type": "github" } ], - "time": "2022-02-14T08:28:10+00:00" + "time": "2023-08-02T09:26:13+00:00" }, { "name": "sebastian/lines-of-code", @@ -2520,16 +2525,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", "shasum": "" }, "require": { @@ -2568,10 +2573,10 @@ } ], "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" }, "funding": [ { @@ -2579,7 +2584,7 @@ "type": "github" } ], - "time": "2020-10-26T13:17:30+00:00" + "time": "2023-02-03T06:07:39+00:00" }, { "name": "sebastian/resource-operations", @@ -2638,16 +2643,16 @@ }, { "name": "sebastian/type", - "version": "3.2.0", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e" + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", - "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", "shasum": "" }, "require": { @@ -2682,7 +2687,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.0" + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" }, "funding": [ { @@ -2690,7 +2695,7 @@ "type": "github" } ], - "time": "2022-09-12T14:47:03+00:00" + "time": "2023-02-03T06:13:03+00:00" }, { "name": "sebastian/version", diff --git a/compiler/src/Console/PrepareCommand.php b/compiler/src/Console/PrepareCommand.php index 0486b4951a..42cb964062 100644 --- a/compiler/src/Console/PrepareCommand.php +++ b/compiler/src/Console/PrepareCommand.php @@ -64,7 +64,7 @@ private function fixComposerJson(string $buildDir): void unset($json['replace']); $json['name'] = 'phpstan/phpstan'; - $json['require']['php'] = '^7.1'; + $json['require']['php'] = '^7.2'; // simplify autoload (remove not packed build directory] $json['autoload']['psr-4']['PHPStan\\'] = 'src/'; @@ -179,7 +179,7 @@ private function buildPreloadScript(): void $vendorDir . '/phpstan/phpdoc-parser/src', ])->exclude([ 'Testing', - ]) as $phpFile) { + ])->sortByName() as $phpFile) { $realPath = $phpFile->getRealPath(); if ($realPath === false) { return; @@ -205,7 +205,7 @@ private function deleteUnnecessaryVendorCode(): void private function transformSource(): void { chdir(__DIR__ . '/../../..'); - exec(escapeshellarg(__DIR__ . '/../../../build/transform-source') . ' 7.1', $outputLines, $exitCode); + exec(escapeshellarg(__DIR__ . '/../../../vendor/bin/simple-downgrade') . ' downgrade -c ' . escapeshellarg('build/downgrade.php') . ' 7.2', $outputLines, $exitCode); if ($exitCode === 0) { return; } diff --git a/composer.json b/composer.json index f8de94accb..f72eefc304 100644 --- a/composer.json +++ b/composer.json @@ -10,23 +10,26 @@ "clue/ndjson-react": "^1.0", "composer/ca-bundle": "^1.2", "composer/xdebug-handler": "^3.0.3", + "fidry/cpu-core-counter": "^0.5.0", "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", + "hoa/file": "1.17.07.11", "hoa/regex": "1.17.01.13", - "jetbrains/phpstorm-stubs": "dev-master#b30cf9fea18875fc6dad3f065ebd648c439b58fa", + "jetbrains/phpstorm-stubs": "dev-master#80e1e4810b9b72f2ce3871a344762e3a0ea87423", "nette/bootstrap": "^3.0", - "nette/di": "^3.0.11", - "nette/finder": "^2.5", + "nette/di": "^3.1.4", "nette/neon": "^3.3.1", "nette/schema": "^1.2.2", "nette/utils": "^3.2.5", - "nikic/php-parser": "^4.15.0", + "nikic/php-parser": "^4.17.1", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.3.0", - "phpstan/php-8-stubs": "0.3.38", - "phpstan/phpdoc-parser": "1.13.0", + "ondrejmirtes/better-reflection": "6.25.0.8", + "phpstan/php-8-stubs": "0.3.84", + "phpstan/phpdoc-parser": "1.28.0", + "psr/http-message": "^1.1", "react/async": "^3", "react/child-process": "^0.6.4", + "react/dns": "^1.10", "react/event-loop": "^1.2", "react/http": "^1.1", "react/promise": "^2.8", @@ -37,10 +40,10 @@ "symfony/polyfill-intl-grapheme": "^1.23", "symfony/polyfill-intl-normalizer": "^1.23", "symfony/polyfill-mbstring": "^1.23", - "symfony/polyfill-php72": "^1.23", "symfony/polyfill-php73": "^1.23", "symfony/polyfill-php74": "^1.23", "symfony/polyfill-php80": "^1.23", + "symfony/polyfill-php81": "^1.27", "symfony/process": "^5.4.3", "symfony/service-contracts": "^2.5.0", "symfony/string": "^5.4.3" @@ -50,16 +53,17 @@ }, "require-dev": { "brianium/paratest": "^6.5", - "loophp/phposinfo": "1.7.2", + "cweagans/composer-patches": "^1.7.3", + "nette/finder": "^2.5", + "ondrejmirtes/simple-downgrader": "^1.0", "php-parallel-lint/php-parallel-lint": "^1.2.0", - "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.2", "phpstan/phpstan-nette": "^1.0", - "phpstan/phpstan-php-parser": "^1.1", "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0", + "phpstan/phpstan-strict-rules": "^1.6", "phpunit/phpunit": "^9.5.4", - "rector/rector": "^0.14.3", - "vaimo/composer-patches": "^4.22" + "shipmonk/composer-dependency-analyser": "^1.5", + "shipmonk/name-collision-detector": "^2.0" }, "config": { "platform": { @@ -68,12 +72,36 @@ "platform-check": false, "sort-packages": true, "allow-plugins": { - "vaimo/composer-patches": true + "cweagans/composer-patches": true } }, "extra": { - "patcher": { - "search": "patches" + "composer-exit-on-patch-failure": true, + "patches": { + "hoa/iterator": [ + "patches/Buffer.patch", + "patches/Lookahead.patch" + ], + "hoa/compiler": [ + "patches/Rule.patch" + ], + "hoa/consistency": [ + "patches/Consistency.patch" + ], + "hoa/protocol": [ + "patches/Node.patch", + "patches/Wrapper.patch" + ], + "hoa/stream": [ + "patches/Stream.patch" + ], + "jetbrains/phpstorm-stubs": [ + "patches/PDO.patch", + "patches/ReflectionProperty.patch", + "patches/SessionHandler.patch", + "patches/xmlreader.patch", + "patches/dom_c.patch" + ] } }, "autoload": { diff --git a/composer.lock b/composer.lock index 017ad8355b..6b938a82bd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,29 +4,29 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "486d4f1331adb5d15c22a8e56df66497", + "content-hash": "79e2c3414616718a8ea0caa6a9d81e5d", "packages": [ { "name": "clue/ndjson-react", - "version": "v1.2.0", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/clue/reactphp-ndjson.git", - "reference": "708411c7e45ac85371a99d50f52284971494bede" + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/708411c7e45ac85371a99d50f52284971494bede", - "reference": "708411c7e45ac85371a99d50f52284971494bede", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", "shasum": "" }, "require": { "php": ">=5.3", - "react/stream": "^1.0 || ^0.7 || ^0.6" + "react/stream": "^1.2" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", - "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3" + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" }, "type": "library", "autoload": { @@ -56,7 +56,7 @@ ], "support": { "issues": "https://github.com/clue/reactphp-ndjson/issues", - "source": "https://github.com/clue/reactphp-ndjson/tree/v1.2.0" + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" }, "funding": [ { @@ -68,20 +68,20 @@ "type": "github" } ], - "time": "2020-12-09T13:09:07+00:00" + "time": "2022-12-23T10:58:28+00:00" }, { "name": "composer/ca-bundle", - "version": "1.3.3", + "version": "1.3.7", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "30897edbfb15e784fe55587b4f73ceefd3c4d98c" + "reference": "76e46335014860eec1aa5a724799a00a2e47cc85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/30897edbfb15e784fe55587b4f73ceefd3c4d98c", - "reference": "30897edbfb15e784fe55587b4f73ceefd3c4d98c", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/76e46335014860eec1aa5a724799a00a2e47cc85", + "reference": "76e46335014860eec1aa5a724799a00a2e47cc85", "shasum": "" }, "require": { @@ -128,7 +128,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.3.3" + "source": "https://github.com/composer/ca-bundle/tree/1.3.7" }, "funding": [ { @@ -144,20 +144,20 @@ "type": "tidelift" } ], - "time": "2022-07-20T07:14:26+00:00" + "time": "2023-08-30T09:31:38+00:00" }, { "name": "composer/pcre", - "version": "3.0.0", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd" + "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/e300eb6c535192decd27a85bc72a9290f0d6b3bd", - "reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd", + "url": "https://api.github.com/repos/composer/pcre/zipball/4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", + "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", "shasum": "" }, "require": { @@ -199,7 +199,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.0.0" + "source": "https://github.com/composer/pcre/tree/3.1.0" }, "funding": [ { @@ -215,7 +215,7 @@ "type": "tidelift" } ], - "time": "2022-02-25T20:21:48+00:00" + "time": "2022-11-17T09:50:14+00:00" }, { "name": "composer/xdebug-handler", @@ -330,6 +330,67 @@ }, "time": "2017-07-23T21:35:13+00:00" }, + { + "name": "fidry/cpu-core-counter", + "version": "0.5.1", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "b58e5a3933e541dc286cc91fc4f3898bbc6f1623" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/b58e5a3933e541dc286cc91fc4f3898bbc6f1623", + "reference": "b58e5a3933e541dc286cc91fc4f3898bbc6f1623", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^9.5.26 || ^8.5.31", + "theofidry/php-cs-fixer-config": "^1.0", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/0.5.1" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2022-12-24T12:35:10+00:00" + }, { "name": "fig/http-message-util", "version": "1.1.5", @@ -506,12 +567,12 @@ } }, "autoload": { - "psr-4": { - "Hoa\\Consistency\\": "." - }, "files": [ "Prelude.php" - ] + ], + "psr-4": { + "Hoa\\Consistency\\": "." + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -913,12 +974,12 @@ } }, "autoload": { - "psr-4": { - "Hoa\\Protocol\\": "." - }, "files": [ "Wrapper.php" - ] + ], + "psr-4": { + "Hoa\\Protocol\\": "." + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1292,20 +1353,19 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "b30cf9fea18875fc6dad3f065ebd648c439b58fa" + "reference": "80e1e4810b9b72f2ce3871a344762e3a0ea87423" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/b30cf9fea18875fc6dad3f065ebd648c439b58fa", - "reference": "b30cf9fea18875fc6dad3f065ebd648c439b58fa", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/80e1e4810b9b72f2ce3871a344762e3a0ea87423", + "reference": "80e1e4810b9b72f2ce3871a344762e3a0ea87423", "shasum": "" }, "require-dev": { - "friendsofphp/php-cs-fixer": "@stable", - "nikic/php-parser": "@stable", - "php": "^8.0", - "phpdocumentor/reflection-docblock": "@stable", - "phpunit/phpunit": "@stable" + "friendsofphp/php-cs-fixer": "v3.46.0", + "nikic/php-parser": "v5.0.0", + "phpdocumentor/reflection-docblock": "5.3.0", + "phpunit/phpunit": "10.5.5" }, "default-branch": true, "type": "library", @@ -1333,26 +1393,26 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2022-09-26T16:45:38+00:00" + "time": "2024-04-29T13:21:12+00:00" }, { "name": "nette/bootstrap", - "version": "v3.1.2", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/nette/bootstrap.git", - "reference": "3ab4912a08af0c16d541c3709935c3478b5ee090" + "reference": "1a7965b4ee401ad0e3f673b9c016d2481afdc280" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/bootstrap/zipball/3ab4912a08af0c16d541c3709935c3478b5ee090", - "reference": "3ab4912a08af0c16d541c3709935c3478b5ee090", + "url": "https://api.github.com/repos/nette/bootstrap/zipball/1a7965b4ee401ad0e3f673b9c016d2481afdc280", + "reference": "1a7965b4ee401ad0e3f673b9c016d2481afdc280", "shasum": "" }, "require": { "nette/di": "^3.0.5", - "nette/utils": "^3.2.1", - "php": ">=7.2 <8.2" + "nette/utils": "^3.2.1 || ^4.0", + "php": ">=7.2 <8.3" }, "conflict": { "tracy/tracy": "<2.6" @@ -1412,45 +1472,42 @@ ], "support": { "issues": "https://github.com/nette/bootstrap/issues", - "source": "https://github.com/nette/bootstrap/tree/v3.1.2" + "source": "https://github.com/nette/bootstrap/tree/v3.1.4" }, - "time": "2021-11-24T16:51:46+00:00" + "time": "2022-12-14T15:23:02+00:00" }, { "name": "nette/di", - "version": "v3.0.11", + "version": "v3.1.5", "source": { "type": "git", "url": "https://github.com/nette/di.git", - "reference": "942e406f63b88b57cb4e095ae0fd95c103d12c5b" + "reference": "00ea0afa643b3b4383a5cd1a322656c989ade498" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/di/zipball/942e406f63b88b57cb4e095ae0fd95c103d12c5b", - "reference": "942e406f63b88b57cb4e095ae0fd95c103d12c5b", + "url": "https://api.github.com/repos/nette/di/zipball/00ea0afa643b3b4383a5cd1a322656c989ade498", + "reference": "00ea0afa643b3b4383a5cd1a322656c989ade498", "shasum": "" }, "require": { "ext-tokenizer": "*", - "nette/neon": "^3.3", - "nette/php-generator": "^3.3.3", - "nette/robot-loader": "^3.2", - "nette/schema": "^1.1", - "nette/utils": "^3.1.6", - "php": ">=7.1 <8.2" - }, - "conflict": { - "nette/bootstrap": "<3.0" + "nette/neon": "^3.3 || ^4.0", + "nette/php-generator": "^3.5.4 || ^4.0", + "nette/robot-loader": "^3.2 || ~4.0.0", + "nette/schema": "^1.2", + "nette/utils": "^3.2.5 || ~4.0.0", + "php": "7.2 - 8.3" }, "require-dev": { - "nette/tester": "^2.2", - "phpstan/phpstan": "^0.12", - "tracy/tracy": "^2.3" + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -1487,22 +1544,22 @@ ], "support": { "issues": "https://github.com/nette/di/issues", - "source": "https://github.com/nette/di/tree/v3.0.11" + "source": "https://github.com/nette/di/tree/v3.1.5" }, - "time": "2021-10-26T11:44:44+00:00" + "time": "2023-10-02T19:58:38+00:00" }, { "name": "nette/finder", - "version": "v2.5.3", + "version": "v2.6.0", "source": { "type": "git", "url": "https://github.com/nette/finder.git", - "reference": "64dc25b7929b731e72a1bc84a9e57727f5d5d3e8" + "reference": "991aefb42860abeab8e003970c3809a9d83cb932" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/finder/zipball/64dc25b7929b731e72a1bc84a9e57727f5d5d3e8", - "reference": "64dc25b7929b731e72a1bc84a9e57727f5d5d3e8", + "url": "https://api.github.com/repos/nette/finder/zipball/991aefb42860abeab8e003970c3809a9d83cb932", + "reference": "991aefb42860abeab8e003970c3809a9d83cb932", "shasum": "" }, "require": { @@ -1520,7 +1577,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } }, "autoload": { @@ -1554,9 +1611,9 @@ ], "support": { "issues": "https://github.com/nette/finder/issues", - "source": "https://github.com/nette/finder/tree/v2.5.3" + "source": "https://github.com/nette/finder/tree/v2.6.0" }, - "time": "2021-12-12T17:43:24+00:00" + "time": "2022-10-13T01:31:15+00:00" }, { "name": "nette/neon", @@ -1763,25 +1820,25 @@ }, { "name": "nette/schema", - "version": "v1.2.2", + "version": "v1.2.5", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "9a39cef03a5b34c7de64f551538cbba05c2be5df" + "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/9a39cef03a5b34c7de64f551538cbba05c2be5df", - "reference": "9a39cef03a5b34c7de64f551538cbba05c2be5df", + "url": "https://api.github.com/repos/nette/schema/zipball/0462f0166e823aad657c9224d0f849ecac1ba10a", + "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a", "shasum": "" }, "require": { "nette/utils": "^2.5.7 || ^3.1.5 || ^4.0", - "php": ">=7.1 <8.2" + "php": "7.1 - 8.3" }, "require-dev": { "nette/tester": "^2.3 || ^2.4", - "phpstan/phpstan-nette": "^0.12", + "phpstan/phpstan-nette": "^1.0", "tracy/tracy": "^2.7" }, "type": "library", @@ -1819,9 +1876,9 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.2.2" + "source": "https://github.com/nette/schema/tree/v1.2.5" }, - "time": "2021-10-15T11:40:02+00:00" + "time": "2023-10-05T20:37:59+00:00" }, { "name": "nette/utils", @@ -1910,21 +1967,21 @@ }, { "name": "nikic/php-parser", - "version": "v4.15.1", + "version": "v4.19.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900" + "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", - "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4e1b88d21c69391150ace211e9eaf05810858d0b", + "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=7.1" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", @@ -1960,9 +2017,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.1" }, - "time": "2022-09-04T07:30:47+00:00" + "time": "2024-03-17T08:10:35+00:00" }, { "name": "ondram/ci-detector", @@ -2038,34 +2095,34 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.3.0", + "version": "6.25.0.8", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "afe870deee721fa850b0ae44c920028c2876f898" + "reference": "2e7c358186dc41cda44d6f4e4e11180d9de36d36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/afe870deee721fa850b0ae44c920028c2876f898", - "reference": "afe870deee721fa850b0ae44c920028c2876f898", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/2e7c358186dc41cda44d6f4e4e11180d9de36d36", + "reference": "2e7c358186dc41cda44d6f4e4e11180d9de36d36", "shasum": "" }, "require": { "ext-json": "*", - "jetbrains/phpstorm-stubs": "dev-master#9717ec39b211f2cef82a59c29c9eef0954647fac", - "nikic/php-parser": "^4.15.1", + "jetbrains/phpstorm-stubs": "dev-master#217ed9356d07ef89109d3cd7d8c5df10aab4b0d4", + "nikic/php-parser": "^4.18.0", "php": "^7.2 || ^8.0" }, "conflict": { "thecodingmachine/safe": "<1.1.3" }, "require-dev": { - "doctrine/coding-standard": "^10.0.0", - "phpstan/phpstan": "^1.8.9", - "phpstan/phpstan-phpunit": "^1.1.1", - "phpunit/phpunit": "^9.5.25", + "doctrine/coding-standard": "^12.0.0", + "phpstan/phpstan": "^1.10.60", + "phpstan/phpstan-phpunit": "^1.3.16", + "phpunit/phpunit": "^10.5.12", "rector/rector": "0.14.3", - "vimeo/psalm": "^4.29" + "vimeo/psalm": "5.23.0" }, "suggest": { "composer/composer": "Required to use the ComposerSourceLocator" @@ -2104,22 +2161,22 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.3.0" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.25.0.8" }, - "time": "2022-10-17T08:31:20+00:00" + "time": "2024-04-22T11:16:06+00:00" }, { "name": "phpstan/php-8-stubs", - "version": "0.3.38", + "version": "0.3.84", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "ee7501b720e2660f6eb6e9d40ab48311ee9288f7" + "reference": "d713e9c3f6f8223d323efe9558b477ae92e989df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/ee7501b720e2660f6eb6e9d40ab48311ee9288f7", - "reference": "ee7501b720e2660f6eb6e9d40ab48311ee9288f7", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/d713e9c3f6f8223d323efe9558b477ae92e989df", + "reference": "d713e9c3f6f8223d323efe9558b477ae92e989df", "shasum": "" }, "type": "library", @@ -2136,28 +2193,30 @@ "description": "PHP stubs extracted from php-src", "support": { "issues": "https://github.com/phpstan/php-8-stubs/issues", - "source": "https://github.com/phpstan/php-8-stubs/tree/0.3.38" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.3.84" }, - "time": "2022-09-24T00:21:31+00:00" + "time": "2023-12-30T11:29:15+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.13.0", + "version": "1.28.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "33aefcdab42900e36366d0feab6206e2dd68f947" + "reference": "cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/33aefcdab42900e36366d0feab6206e2dd68f947", - "reference": "33aefcdab42900e36366d0feab6206e2dd68f947", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb", + "reference": "cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^1.5", @@ -2181,9 +2240,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.13.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.28.0" }, - "time": "2022-10-21T09:57:39+00:00" + "time": "2024-04-03T18:51:33+00:00" }, { "name": "psr/container", @@ -2235,25 +2294,25 @@ }, { "name": "psr/http-message", - "version": "1.0.1", + "version": "1.1", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -2282,9 +2341,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/master" + "source": "https://github.com/php-fig/http-message/tree/1.1" }, - "time": "2016-08-06T14:39:51+00:00" + "time": "2023-04-04T09:50:52+00:00" }, { "name": "psr/log", @@ -2413,16 +2472,16 @@ }, { "name": "react/cache", - "version": "v1.1.1", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/reactphp/cache.git", - "reference": "4bf736a2cccec7298bdf745db77585966fc2ca7e" + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/cache/zipball/4bf736a2cccec7298bdf745db77585966fc2ca7e", - "reference": "4bf736a2cccec7298bdf745db77585966fc2ca7e", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", "shasum": "" }, "require": { @@ -2430,7 +2489,7 @@ "react/promise": "^3.0 || ^2.0 || ^1.1" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" }, "type": "library", "autoload": { @@ -2473,19 +2532,15 @@ ], "support": { "issues": "https://github.com/reactphp/cache/issues", - "source": "https://github.com/reactphp/cache/tree/v1.1.1" + "source": "https://github.com/reactphp/cache/tree/v1.2.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2021-02-02T06:47:52+00:00" + "time": "2022-11-30T15:59:55+00:00" }, { "name": "react/child-process", @@ -2648,33 +2703,31 @@ }, { "name": "react/event-loop", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/reactphp/event-loop.git", - "reference": "187fb56f46d424afb6ec4ad089269c72eec2e137" + "reference": "6e7e587714fff7a83dcc7025aee42ab3b265ae05" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/187fb56f46d424afb6ec4ad089269c72eec2e137", - "reference": "187fb56f46d424afb6ec4ad089269c72eec2e137", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/6e7e587714fff7a83dcc7025aee42ab3b265ae05", + "reference": "6e7e587714fff7a83dcc7025aee42ab3b265ae05", "shasum": "" }, "require": { "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, "suggest": { - "ext-event": "~1.0 for ExtEventLoop", - "ext-pcntl": "For signal handling support when using the StreamSelectLoop", - "ext-uv": "* for ExtUvLoop" + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" }, "type": "library", "autoload": { "psr-4": { - "React\\EventLoop\\": "src" + "React\\EventLoop\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2710,32 +2763,28 @@ ], "support": { "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.3.0" + "source": "https://github.com/reactphp/event-loop/tree/v1.4.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2022-03-17T11:10:22+00:00" + "time": "2023-05-05T10:11:24+00:00" }, { "name": "react/http", - "version": "v1.8.0", + "version": "v1.9.0", "source": { "type": "git", "url": "https://github.com/reactphp/http.git", - "reference": "aa7512ee17258c88466de30f9cb44ec5f9df3ff3" + "reference": "bb3154dbaf2dfe3f0467f956a05f614a69d5f1d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/http/zipball/aa7512ee17258c88466de30f9cb44ec5f9df3ff3", - "reference": "aa7512ee17258c88466de30f9cb44ec5f9df3ff3", + "url": "https://api.github.com/repos/reactphp/http/zipball/bb3154dbaf2dfe3f0467f956a05f614a69d5f1d0", + "reference": "bb3154dbaf2dfe3f0467f956a05f614a69d5f1d0", "shasum": "" }, "require": { @@ -2745,7 +2794,6 @@ "psr/http-message": "^1.0", "react/event-loop": "^1.2", "react/promise": "^3 || ^2.3 || ^1.2.1", - "react/promise-stream": "^1.4", "react/socket": "^1.12", "react/stream": "^1.2", "ringcentral/psr7": "^1.2" @@ -2754,14 +2802,15 @@ "clue/http-proxy-react": "^1.8", "clue/reactphp-ssh-proxy": "^1.4", "clue/socks-react": "^1.4", - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", "react/async": "^4 || ^3 || ^2", + "react/promise-stream": "^1.4", "react/promise-timer": "^1.9" }, "type": "library", "autoload": { "psr-4": { - "React\\Http\\": "src" + "React\\Http\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2806,39 +2855,35 @@ ], "support": { "issues": "https://github.com/reactphp/http/issues", - "source": "https://github.com/reactphp/http/tree/v1.8.0" + "source": "https://github.com/reactphp/http/tree/v1.9.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2022-09-29T12:55:52+00:00" + "time": "2023-04-26T10:29:24+00:00" }, { "name": "react/promise", - "version": "v2.9.0", + "version": "v2.10.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "234f8fd1023c9158e2314fa9d7d0e6a83db42910" + "reference": "f913fb8cceba1e6644b7b90c4bfb678ed8a3ef38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/234f8fd1023c9158e2314fa9d7d0e6a83db42910", - "reference": "234f8fd1023c9158e2314fa9d7d0e6a83db42910", + "url": "https://api.github.com/repos/reactphp/promise/zipball/f913fb8cceba1e6644b7b90c4bfb678ed8a3ef38", + "reference": "f913fb8cceba1e6644b7b90c4bfb678ed8a3ef38", "shasum": "" }, "require": { "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.36" + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.36" }, "type": "library", "autoload": { @@ -2882,102 +2927,15 @@ ], "support": { "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v2.9.0" - }, - "funding": [ - { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" - } - ], - "time": "2022-02-11T10:27:51+00:00" - }, - { - "name": "react/promise-stream", - "version": "v1.5.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/promise-stream.git", - "reference": "e6d2805e09ad50c4896f65f5e8705fe4ee7731a3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise-stream/zipball/e6d2805e09ad50c4896f65f5e8705fe4ee7731a3", - "reference": "e6d2805e09ad50c4896f65f5e8705fe4ee7731a3", - "shasum": "" - }, - "require": { - "php": ">=5.3", - "react/promise": "^3 || ^2.1 || ^1.2", - "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.6" - }, - "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "React\\Promise\\Stream\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "The missing link between Promise-land and Stream-land for ReactPHP", - "homepage": "https://github.com/reactphp/promise-stream", - "keywords": [ - "Buffer", - "async", - "promise", - "reactphp", - "stream", - "unwrap" - ], - "support": { - "issues": "https://github.com/reactphp/promise-stream/issues", - "source": "https://github.com/reactphp/promise-stream/tree/v1.5.0" + "source": "https://github.com/reactphp/promise/tree/v2.10.0" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2022-09-09T11:42:18+00:00" + "time": "2023-05-02T15:15:43+00:00" }, { "name": "react/promise-timer", @@ -3291,16 +3249,16 @@ }, { "name": "symfony/console", - "version": "v5.4.13", + "version": "v5.4.28", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "3f97f6c7b7e26848a90c0c0cfb91eeb2bb8618be" + "reference": "f4f71842f24c2023b91237c72a365306f3c58827" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/3f97f6c7b7e26848a90c0c0cfb91eeb2bb8618be", - "reference": "3f97f6c7b7e26848a90c0c0cfb91eeb2bb8618be", + "url": "https://api.github.com/repos/symfony/console/zipball/f4f71842f24c2023b91237c72a365306f3c58827", + "reference": "f4f71842f24c2023b91237c72a365306f3c58827", "shasum": "" }, "require": { @@ -3365,12 +3323,12 @@ "homepage": "https://symfony.com", "keywords": [ "cli", - "command line", + "command-line", "console", "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.13" + "source": "https://github.com/symfony/console/tree/v5.4.28" }, "funding": [ { @@ -3386,20 +3344,20 @@ "type": "tidelift" } ], - "time": "2022-08-26T13:50:20+00:00" + "time": "2023-08-07T06:12:30+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.1.1", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918" + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", - "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", "shasum": "" }, "require": { @@ -3408,7 +3366,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.1-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -3437,7 +3395,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0" }, "funding": [ { @@ -3453,20 +3411,20 @@ "type": "tidelift" } ], - "time": "2022-02-25T11:15:52+00:00" + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/finder", - "version": "v5.4.11", + "version": "v5.4.27", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c" + "reference": "ff4bce3c33451e7ec778070e45bd23f74214cd5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/7872a66f57caffa2916a584db1aa7f12adc76f8c", - "reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c", + "url": "https://api.github.com/repos/symfony/finder/zipball/ff4bce3c33451e7ec778070e45bd23f74214cd5d", + "reference": "ff4bce3c33451e7ec778070e45bd23f74214cd5d", "shasum": "" }, "require": { @@ -3500,7 +3458,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.11" + "source": "https://github.com/symfony/finder/tree/v5.4.27" }, "funding": [ { @@ -3516,20 +3474,20 @@ "type": "tidelift" } ], - "time": "2022-07-29T07:37:50+00:00" + "time": "2023-07-31T08:02:31+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.26.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", - "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", "shasum": "" }, "require": { @@ -3544,7 +3502,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3582,7 +3540,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" }, "funding": [ { @@ -3598,20 +3556,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.26.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "433d05519ce6990bf3530fba6957499d327395c2" + "reference": "875e90aeea2777b6f135677f618529449334a612" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2", - "reference": "433d05519ce6990bf3530fba6957499d327395c2", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", "shasum": "" }, "require": { @@ -3623,7 +3581,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3663,7 +3621,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" }, "funding": [ { @@ -3679,20 +3637,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.26.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "219aa369ceff116e673852dce47c3a41794c14bd" + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", - "reference": "219aa369ceff116e673852dce47c3a41794c14bd", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", "shasum": "" }, "require": { @@ -3704,7 +3662,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3747,7 +3705,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" }, "funding": [ { @@ -3763,20 +3721,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.26.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" + "reference": "42292d99c55abe617799667f454222c54c60e229" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", - "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", "shasum": "" }, "require": { @@ -3791,7 +3749,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3830,7 +3788,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" }, "funding": [ { @@ -3846,20 +3804,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2023-07-28T09:04:16+00:00" }, { - "name": "symfony/polyfill-php72", - "version": "v1.26.0", + "name": "symfony/polyfill-php73", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "bf44a9fd41feaac72b074de600314a93e2ae78e2" + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/bf44a9fd41feaac72b074de600314a93e2ae78e2", - "reference": "bf44a9fd41feaac72b074de600314a93e2ae78e2", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fe2f306d1d9d346a7fee353d0d5012e401e984b5", + "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5", "shasum": "" }, "require": { @@ -3868,7 +3826,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3880,8 +3838,11 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - } + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3897,7 +3858,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -3906,7 +3867,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.28.0" }, "funding": [ { @@ -3922,20 +3883,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { - "name": "symfony/polyfill-php73", - "version": "v1.26.0", + "name": "symfony/polyfill-php74", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85" + "url": "https://github.com/symfony/polyfill-php74.git", + "reference": "8b755b41a155c89f1af29cc33305538499fa05ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/e440d35fa0286f77fb45b79a03fedbeda9307e85", - "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85", + "url": "https://api.github.com/repos/symfony/polyfill-php74/zipball/8b755b41a155c89f1af29cc33305538499fa05ea", + "reference": "8b755b41a155c89f1af29cc33305538499fa05ea", "shasum": "" }, "require": { @@ -3944,7 +3905,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3956,17 +3917,18 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, - "classmap": [ - "Resources/stubs" - ] + "Symfony\\Polyfill\\Php74\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -3976,7 +3938,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 7.4+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -3985,7 +3947,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-php74/tree/v1.28.0" }, "funding": [ { @@ -4001,20 +3963,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { - "name": "symfony/polyfill-php74", - "version": "v1.26.0", + "name": "symfony/polyfill-php80", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php74.git", - "reference": "ad4f7d62a17b1187d9f381f0a662aab19ff3c033" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php74/zipball/ad4f7d62a17b1187d9f381f0a662aab19ff3c033", - "reference": "ad4f7d62a17b1187d9f381f0a662aab19ff3c033", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", "shasum": "" }, "require": { @@ -4023,7 +3985,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4035,8 +3997,11 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php74\\": "" - } + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4056,7 +4021,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.4+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -4065,7 +4030,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php74/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" }, "funding": [ { @@ -4081,20 +4046,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { - "name": "symfony/polyfill-php80", - "version": "v1.26.0", + "name": "symfony/polyfill-php81", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace" + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace", - "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/7581cd600fa9fd681b797d00b02f068e2f13263b", + "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b", "shasum": "" }, "require": { @@ -4103,7 +4068,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4115,7 +4080,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" + "Symfony\\Polyfill\\Php81\\": "" }, "classmap": [ "Resources/stubs" @@ -4126,10 +4091,6 @@ "MIT" ], "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -4139,7 +4100,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -4148,7 +4109,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.28.0" }, "funding": [ { @@ -4164,20 +4125,20 @@ "type": "tidelift" } ], - "time": "2022-05-10T07:21:04+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/process", - "version": "v5.4.11", + "version": "v5.4.28", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1" + "reference": "45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/6e75fe6874cbc7e4773d049616ab450eff537bf1", - "reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1", + "url": "https://api.github.com/repos/symfony/process/zipball/45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b", + "reference": "45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b", "shasum": "" }, "require": { @@ -4210,7 +4171,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.11" + "source": "https://github.com/symfony/process/tree/v5.4.28" }, "funding": [ { @@ -4226,7 +4187,7 @@ "type": "tidelift" } ], - "time": "2022-06-27T16:58:25+00:00" + "time": "2023-08-07T10:36:04+00:00" }, { "name": "symfony/service-contracts", @@ -4313,16 +4274,16 @@ }, { "name": "symfony/string", - "version": "v5.4.13", + "version": "v5.4.26", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "2900c668a32138a34118740de3e4d5a701801f53" + "reference": "1181fe9270e373537475e826873b5867b863883c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/2900c668a32138a34118740de3e4d5a701801f53", - "reference": "2900c668a32138a34118740de3e4d5a701801f53", + "url": "https://api.github.com/repos/symfony/string/zipball/1181fe9270e373537475e826873b5867b863883c", + "reference": "1181fe9270e373537475e826873b5867b863883c", "shasum": "" }, "require": { @@ -4379,7 +4340,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.13" + "source": "https://github.com/symfony/string/tree/v5.4.26" }, "funding": [ { @@ -4395,22 +4356,22 @@ "type": "tidelift" } ], - "time": "2022-09-01T01:52:16+00:00" + "time": "2023-06-28T12:46:07+00:00" } ], "packages-dev": [ { "name": "brianium/paratest", - "version": "v6.6.2", + "version": "v6.6.3", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "5249af4e25e79da66d1ec3b54b474047999c10b8" + "reference": "f2d781bb9136cda2f5e73ee778049e80ba681cf6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/5249af4e25e79da66d1ec3b54b474047999c10b8", - "reference": "5249af4e25e79da66d1ec3b54b474047999c10b8", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/f2d781bb9136cda2f5e73ee778049e80ba681cf6", + "reference": "f2d781bb9136cda2f5e73ee778049e80ba681cf6", "shasum": "" }, "require": { @@ -4420,10 +4381,10 @@ "ext-simplexml": "*", "jean85/pretty-package-versions": "^2.0.5", "php": "^7.3 || ^8.0", - "phpunit/php-code-coverage": "^9.2.15", + "phpunit/php-code-coverage": "^9.2.16", "phpunit/php-file-iterator": "^3.0.6", "phpunit/php-timer": "^5.0.3", - "phpunit/phpunit": "^9.5.21", + "phpunit/phpunit": "^9.5.23", "sebastian/environment": "^5.1.4", "symfony/console": "^5.4.9 || ^6.1.2", "symfony/polyfill-php80": "^v1.26.0", @@ -4478,7 +4439,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v6.6.2" + "source": "https://github.com/paratestphp/paratest/tree/v6.6.3" }, "funding": [ { @@ -4490,7 +4451,55 @@ "type": "paypal" } ], - "time": "2022-08-22T10:45:51+00:00" + "time": "2022-08-25T05:44:14+00:00" + }, + { + "name": "cweagans/composer-patches", + "version": "1.7.3", + "source": { + "type": "git", + "url": "https://github.com/cweagans/composer-patches.git", + "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/e190d4466fe2b103a55467dfa83fc2fecfcaf2db", + "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3.0" + }, + "require-dev": { + "composer/composer": "~1.0 || ~2.0", + "phpunit/phpunit": "~4.6" + }, + "type": "composer-plugin", + "extra": { + "class": "cweagans\\Composer\\Patches" + }, + "autoload": { + "psr-4": { + "cweagans\\Composer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Cameron Eagans", + "email": "me@cweagans.net" + } + ], + "description": "Provides a way to patch Composer packages.", + "support": { + "issues": "https://github.com/cweagans/composer-patches/issues", + "source": "https://github.com/cweagans/composer-patches/tree/1.7.3" + }, + "time": "2022-12-20T22:53:13+00:00" }, { "name": "doctrine/instantiator", @@ -4563,64 +4572,8 @@ "time": "2022-03-03T08:28:38+00:00" }, { - "name": "drupol/phposinfo", - "version": "1.6.5", - "source": { - "type": "git", - "url": "https://github.com/drupol/phposinfo.git", - "reference": "36b0250d38279c8a131a1898a31e359606024507" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/drupol/phposinfo/zipball/36b0250d38279c8a131a1898a31e359606024507", - "reference": "36b0250d38279c8a131a1898a31e359606024507", - "shasum": "" - }, - "require": { - "php": ">= 7.1.3" - }, - "require-dev": { - "drupol/php-conventions": "^1.7.1", - "friends-of-phpspec/phpspec-code-coverage": "^4.3.2", - "infection/infection": "^0.13.6 || ^0.15.0", - "phpspec/phpspec": "^5.1.2 || ^6.1.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "drupol\\phposinfo\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Pol Dellaiera", - "email": "pol.dellaiera@protonmail.com" - } - ], - "description": "Try to guess the host operating system.", - "keywords": [ - "operating system detection" - ], - "support": { - "issues": "https://github.com/drupol/phposinfo/issues", - "source": "https://github.com/drupol/phposinfo/tree/master" - }, - "funding": [ - { - "url": "https://github.com/drupol", - "type": "github" - } - ], - "abandoned": "loophp/phposinfo", - "time": "2020-05-19T14:14:28+00:00" - }, - { - "name": "jean85/pretty-package-versions", - "version": "2.0.5", + "name": "jean85/pretty-package-versions", + "version": "2.0.5", "source": { "type": "git", "url": "https://github.com/Jean85/pretty-package-versions.git", @@ -4677,68 +4630,6 @@ }, "time": "2021-10-08T21:21:46+00:00" }, - { - "name": "loophp/phposinfo", - "version": "1.7.2", - "source": { - "type": "git", - "url": "https://github.com/loophp/phposinfo.git", - "reference": "106e7b3f00849dce1787ebf38da493ba586b48f2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/loophp/phposinfo/zipball/106e7b3f00849dce1787ebf38da493ba586b48f2", - "reference": "106e7b3f00849dce1787ebf38da493ba586b48f2", - "shasum": "" - }, - "require": { - "php": ">= 7.3" - }, - "require-dev": { - "drupol/php-conventions": "^3.0.2", - "friends-of-phpspec/phpspec-code-coverage": "^5", - "infection/infection": "^0.18", - "infection/phpspec-adapter": "^0.1.1", - "phpspec/phpspec": "^6", - "vimeo/psalm": "^4.6" - }, - "type": "library", - "autoload": { - "psr-4": { - "loophp\\phposinfo\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Pol Dellaiera", - "email": "pol.dellaiera@protonmail.com" - } - ], - "description": "Try to guess the host operating system.", - "keywords": [ - "operating system detection" - ], - "support": { - "docs": "https://loophp-collection.rtfd.io", - "issues": "https://github.com/loophp/collection/issues", - "source": "https://github.com/loophp/collection" - }, - "funding": [ - { - "url": "https://github.com/drupol", - "type": "github" - }, - { - "url": "https://www.paypal.me/drupol", - "type": "paypal" - } - ], - "time": "2021-06-29T07:18:36+00:00" - }, { "name": "myclabs/deep-copy", "version": "1.11.0", @@ -4798,6 +4689,55 @@ ], "time": "2022-03-03T13:19:32+00:00" }, + { + "name": "ondrejmirtes/simple-downgrader", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/ondrejmirtes/simple-downgrader.git", + "reference": "832aaae53dcfe358f63180494de8734244773d46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ondrejmirtes/simple-downgrader/zipball/832aaae53dcfe358f63180494de8734244773d46", + "reference": "832aaae53dcfe358f63180494de8734244773d46", + "shasum": "" + }, + "require": { + "nette/utils": "^3.2.5", + "nikic/php-parser": "^4.18", + "php": "^7.2|^8.0", + "phpstan/phpdoc-parser": "^1.24.5", + "symfony/console": "^5.4", + "symfony/finder": "^5.4" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.36" + }, + "bin": [ + "bin/simple-downgrade" + ], + "type": "library", + "autoload": { + "psr-4": { + "SimpleDowngrader\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Simple Downgrader", + "support": { + "issues": "https://github.com/ondrejmirtes/simple-downgrader/issues", + "source": "https://github.com/ondrejmirtes/simple-downgrader/tree/1.0.2" + }, + "time": "2024-02-12T19:22:32+00:00" + }, { "name": "phar-io/manifest", "version": "2.0.3", @@ -4968,32 +4908,30 @@ }, { "name": "phpstan/phpstan-deprecation-rules", - "version": "1.0.0", + "version": "1.2.x-dev", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", - "reference": "e5ccafb0dd8d835dd65d8d7a1a0d2b1b75414682" + "reference": "788ea1bd84f7848abf27ba29b92c6c9d285dfc95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/e5ccafb0dd8d835dd65d8d7a1a0d2b1b75414682", - "reference": "e5ccafb0dd8d835dd65d8d7a1a0d2b1b75414682", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/788ea1bd84f7848abf27ba29b92c6c9d285dfc95", + "reference": "788ea1bd84f7848abf27ba29b92c6c9d285dfc95", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.0" + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.11" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^9.5" }, + "default-branch": true, "type": "phpstan-extension", "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - }, "phpstan": { "includes": [ "rules.neon" @@ -5012,27 +4950,27 @@ "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", "support": { "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", - "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.0.0" + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.2.x" }, - "time": "2021-09-23T11:02:21+00:00" + "time": "2023-09-19T08:17:29+00:00" }, { "name": "phpstan/phpstan-nette", - "version": "1.1.0", + "version": "1.2.9", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-nette.git", - "reference": "8dddb884521d282b85af7d4a8221e21827df426a" + "reference": "0e3a6805917811d685e59bb83c2286315f2f6d78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-nette/zipball/8dddb884521d282b85af7d4a8221e21827df426a", - "reference": "8dddb884521d282b85af7d4a8221e21827df426a", + "url": "https://api.github.com/repos/phpstan/phpstan-nette/zipball/0e3a6805917811d685e59bb83c2286315f2f6d78", + "reference": "0e3a6805917811d685e59bb83c2286315f2f6d78", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.7" + "phpstan/phpstan": "^1.10" }, "conflict": { "nette/application": "<2.3.0", @@ -5043,6 +4981,7 @@ "nette/utils": "<2.3.0" }, "require-dev": { + "nette/application": "^3.0", "nette/forms": "^3.0", "nette/utils": "^2.3.0 || ^3.0.0", "nikic/php-parser": "^4.13.2", @@ -5073,78 +5012,27 @@ "description": "Nette Framework class reflection extension for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-nette/issues", - "source": "https://github.com/phpstan/phpstan-nette/tree/1.1.0" + "source": "https://github.com/phpstan/phpstan-nette/tree/1.2.9" }, - "time": "2022-09-21T14:29:42+00:00" - }, - { - "name": "phpstan/phpstan-php-parser", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan-php-parser.git", - "reference": "1c7670dd92da864b5d019f22d9f512a6ae18b78e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-php-parser/zipball/1c7670dd92da864b5d019f22d9f512a6ae18b78e", - "reference": "1c7670dd92da864b5d019f22d9f512a6ae18b78e", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.3" - }, - "require-dev": { - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5" - }, - "type": "phpstan-extension", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - }, - "phpstan": { - "includes": [ - "extension.neon" - ] - } - }, - "autoload": { - "psr-4": { - "PHPStan\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHP-Parser extensions for PHPStan", - "support": { - "issues": "https://github.com/phpstan/phpstan-php-parser/issues", - "source": "https://github.com/phpstan/phpstan-php-parser/tree/1.1.0" - }, - "time": "2021-12-16T19:43:32+00:00" + "time": "2023-04-12T14:11:53+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "1.1.1", + "version": "1.3.15", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "4a3c437c09075736285d1cabb5c75bf27ed0bc84" + "reference": "70ecacc64fe8090d8d2a33db5a51fe8e88acd93a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/4a3c437c09075736285d1cabb5c75bf27ed0bc84", - "reference": "4a3c437c09075736285d1cabb5c75bf27ed0bc84", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/70ecacc64fe8090d8d2a33db5a51fe8e88acd93a", + "reference": "70ecacc64fe8090d8d2a33db5a51fe8e88acd93a", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.5.0" + "phpstan/phpstan": "^1.10" }, "conflict": { "phpunit/phpunit": "<7.0" @@ -5152,7 +5040,7 @@ "require-dev": { "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-strict-rules": "^1.0", + "phpstan/phpstan-strict-rules": "^1.5.1", "phpunit/phpunit": "^9.5" }, "type": "phpstan-extension", @@ -5176,34 +5064,36 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.1.1" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.15" }, - "time": "2022-04-20T15:24:25+00:00" + "time": "2023-10-09T18:58:39+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "1.4.4", + "version": "1.6.x-dev", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "23e5f377ee6395a1a04842d3d6ed4bd25e7b44a6" + "reference": "a3b0404c40197996b6ed32b2613e5a337fcbefd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/23e5f377ee6395a1a04842d3d6ed4bd25e7b44a6", - "reference": "23e5f377ee6395a1a04842d3d6ed4bd25e7b44a6", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/a3b0404c40197996b6ed32b2613e5a337fcbefd4", + "reference": "a3b0404c40197996b6ed32b2613e5a337fcbefd4", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.8.6" + "phpstan/phpstan": "^1.11" }, "require-dev": { "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^1.1", "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^9.5" }, + "default-branch": true, "type": "phpstan-extension", "extra": { "phpstan": { @@ -5224,29 +5114,29 @@ "description": "Extra strict and opinionated rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.4.4" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.6.x" }, - "time": "2022-09-21T11:38:17+00:00" + "time": "2023-10-30T14:35:14+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.15", + "version": "9.2.30", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f" + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.13.0", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -5261,8 +5151,8 @@ "phpunit/phpunit": "^9.3" }, "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", "extra": { @@ -5295,7 +5185,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15" + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30" }, "funding": [ { @@ -5303,7 +5194,7 @@ "type": "github" } ], - "time": "2022-03-07T09:28:20+00:00" + "time": "2023-12-22T06:47:57+00:00" }, { "name": "phpunit/php-file-iterator", @@ -5644,64 +5535,6 @@ ], "time": "2022-08-22T14:01:36+00:00" }, - { - "name": "rector/rector", - "version": "0.14.5", - "source": { - "type": "git", - "url": "https://github.com/rectorphp/rector.git", - "reference": "f7fd87b2435835f481e6a94ee28e09af412bd3cc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/f7fd87b2435835f481e6a94ee28e09af412bd3cc", - "reference": "f7fd87b2435835f481e6a94ee28e09af412bd3cc", - "shasum": "" - }, - "require": { - "php": "^7.2|^8.0", - "phpstan/phpstan": "^1.8.6" - }, - "conflict": { - "rector/rector-cakephp": "*", - "rector/rector-doctrine": "*", - "rector/rector-laravel": "*", - "rector/rector-php-parser": "*", - "rector/rector-phpoffice": "*", - "rector/rector-phpunit": "*", - "rector/rector-symfony": "*" - }, - "bin": [ - "bin/rector" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "0.14-dev" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Instant Upgrade and Automated Refactoring of any PHP code", - "support": { - "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/0.14.5" - }, - "funding": [ - { - "url": "https://github.com/tomasvotruba", - "type": "github" - } - ], - "time": "2022-09-29T11:05:42+00:00" - }, { "name": "sebastian/cli-parser", "version": "1.0.1", @@ -5945,20 +5778,20 @@ }, { "name": "sebastian/complexity", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -5990,7 +5823,7 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" }, "funding": [ { @@ -5998,7 +5831,7 @@ "type": "github" } ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2023-12-22T06:19:30+00:00" }, { "name": "sebastian/diff", @@ -6068,16 +5901,16 @@ }, { "name": "sebastian/environment", - "version": "5.1.4", + "version": "5.1.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", "shasum": "" }, "require": { @@ -6119,7 +5952,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" }, "funding": [ { @@ -6127,7 +5960,7 @@ "type": "github" } ], - "time": "2022-04-03T09:37:03+00:00" + "time": "2023-02-03T06:03:51+00:00" }, { "name": "sebastian/exporter", @@ -6272,20 +6105,20 @@ }, { "name": "sebastian/lines-of-code", - "version": "1.0.3", + "version": "1.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -6317,7 +6150,7 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" }, "funding": [ { @@ -6325,7 +6158,7 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-12-22T06:20:34+00:00" }, { "name": "sebastian/object-enumerator", @@ -6667,67 +6500,126 @@ "time": "2020-09-28T06:39:44+00:00" }, { - "name": "seld/jsonlint", - "version": "1.8.3", + "name": "shipmonk/composer-dependency-analyser", + "version": "1.5.3", "source": { "type": "git", - "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57" + "url": "https://github.com/shipmonk-rnd/composer-dependency-analyser.git", + "reference": "00b5023bcc0c9c4f34c9246b3faf5b780e144622" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9ad6ce79c342fbd44df10ea95511a1b24dee5b57", - "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57", + "url": "https://api.github.com/repos/shipmonk-rnd/composer-dependency-analyser/zipball/00b5023bcc0c9c4f34c9246b3faf5b780e144622", + "reference": "00b5023bcc0c9c4f34c9246b3faf5b780e144622", "shasum": "" }, "require": { - "php": "^5.3 || ^7.0 || ^8.0" + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^7.2 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "editorconfig-checker/editorconfig-checker": "^10.3.0", + "ergebnis/composer-normalize": "^2.19", + "ext-dom": "*", + "ext-libxml": "*", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.10.63", + "phpstan/phpstan-phpunit": "^1.1.1", + "phpstan/phpstan-strict-rules": "^1.2.3", + "phpunit/phpunit": "^8.5.28 || ^9.5.20", + "shipmonk/name-collision-detector": "^2.0.0", + "slevomat/coding-standard": "^8.0.1" }, "bin": [ - "bin/jsonlint" + "bin/composer-dependency-analyser" ], "type": "library", "autoload": { "psr-4": { - "Seld\\JsonLint\\": "src/Seld/JsonLint/" + "ShipMonk\\ComposerDependencyAnalyser\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "JSON Linter", + "description": "Fast detection of composer dependency issues (dead dependencies, shadow dependencies, misplaced dependencies)", "keywords": [ - "json", - "linter", - "parser", - "validator" + "analyser", + "composer", + "composer dependency", + "dead code", + "dead dependency", + "detector", + "dev", + "misplaced dependency", + "shadow dependency", + "static analysis", + "unused code", + "unused dependency" ], "support": { - "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/1.8.3" + "issues": "https://github.com/shipmonk-rnd/composer-dependency-analyser/issues", + "source": "https://github.com/shipmonk-rnd/composer-dependency-analyser/tree/1.5.3" }, - "funding": [ - { - "url": "https://github.com/Seldaek", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", - "type": "tidelift" + "time": "2024-04-22T13:28:23+00:00" + }, + { + "name": "shipmonk/name-collision-detector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/shipmonk-rnd/name-collision-detector.git", + "reference": "322993a0b057457ab363929c3ca37bce6eb4affb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shipmonk-rnd/name-collision-detector/zipball/322993a0b057457ab363929c3ca37bce6eb4affb", + "reference": "322993a0b057457ab363929c3ca37bce6eb4affb", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "nette/schema": "^1.1.0", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "editorconfig-checker/editorconfig-checker": "^10.3.0", + "ergebnis/composer-normalize": "^2.19", + "phpstan/phpstan": "^1.8.7", + "phpstan/phpstan-phpunit": "^1.1.1", + "phpstan/phpstan-strict-rules": "^1.2.3", + "phpunit/phpunit": "^8.5.28 || ^9.5.20", + "slevomat/coding-standard": "^8.0.1" + }, + "bin": [ + "bin/detect-collisions" + ], + "type": "library", + "autoload": { + "psr-4": { + "ShipMonk\\NameCollision\\": "src/" } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" ], - "time": "2020-11-11T09:19:24+00:00" + "description": "Simple tool to find ambiguous classes or any other name duplicates within your project.", + "keywords": [ + "ambiguous", + "autoload", + "autoloading", + "classname", + "collision", + "namespace" + ], + "support": { + "issues": "https://github.com/shipmonk-rnd/name-collision-detector/issues", + "source": "https://github.com/shipmonk-rnd/name-collision-detector/tree/2.1.0" + }, + "time": "2023-10-09T12:15:58+00:00" }, { "name": "theseer/tokenizer", @@ -6778,166 +6670,6 @@ } ], "time": "2021-07-28T10:34:58+00:00" - }, - { - "name": "vaimo/composer-patches", - "version": "4.22.4", - "source": { - "type": "git", - "url": "https://github.com/vaimo/composer-patches.git", - "reference": "3da4cdf03fb4dc8d92b3d435de183f6044d679d6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/vaimo/composer-patches/zipball/3da4cdf03fb4dc8d92b3d435de183f6044d679d6", - "reference": "3da4cdf03fb4dc8d92b3d435de183f6044d679d6", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.0 || ^2.0", - "drupol/phposinfo": "^1.6", - "ext-json": "*", - "php": ">=5.3.0", - "seld/jsonlint": "^1.7.1", - "vaimo/topological-sort": "^1.0" - }, - "require-dev": { - "composer/composer": "^1.0 || ^2.0", - "phpcompatibility/php-compatibility": ">=9.1.1", - "phpmd/phpmd": ">=2.6.0", - "sebastian/phpcpd": ">=1.4.3", - "squizlabs/php_codesniffer": ">=2.9.2", - "vaimo/composer-changelogs": "^0.17.0", - "vaimo/composer-patches-proxy": "1.0.0" - }, - "type": "composer-plugin", - "extra": { - "class": "Vaimo\\ComposerPatches\\Plugin", - "changelog": { - "source": "changelog.json", - "output": { - "md": "CHANGELOG.md" - } - } - }, - "autoload": { - "psr-4": { - "Vaimo\\ComposerPatches\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Allan Paiste", - "email": "allan.paiste@vaimo.com" - } - ], - "description": "Applies a patch from a local or remote file to any package that is part of a given composer project. Patches can be defined both on project and on package level. Optional support for patch versioning, sequencing, custom patch applier configuration and patch command for testing/troubleshooting added patches.", - "keywords": [ - "Fixes", - "back-ports", - "backports", - "bulk patches", - "bundled patches", - "composer command", - "composer plugin", - "configurable patch applier", - "development patches", - "downloaded patches", - "environment flags", - "hot-fixes", - "hotfixes", - "indirect restrictions", - "maintenance", - "maintenance tools", - "multi-version patches", - "multiple formats", - "os-specific config", - "package bug-fix", - "package patches", - "patch branching", - "patch command", - "patch description", - "patch exclusion", - "patch header", - "patch meta-data", - "patch resolve", - "patch search", - "patch skipping", - "patcher", - "patching", - "plugin", - "remote patch files", - "resolve patches", - "skipped packages", - "tools", - "utilities", - "utility", - "utils", - "version restriction" - ], - "support": { - "docs": "https://github.com/vaimo/composer-patches", - "issues": "https://github.com/vaimo/composer-patches/issues", - "source": "https://github.com/vaimo/composer-patches" - }, - "time": "2021-02-25T11:24:50+00:00" - }, - { - "name": "vaimo/topological-sort", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/vaimo/topological-sort.git", - "reference": "e19b93df2bac0e995ecd4b982ec4ea2fb1131e64" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/vaimo/topological-sort/zipball/e19b93df2bac0e995ecd4b982ec4ea2fb1131e64", - "reference": "e19b93df2bac0e995ecd4b982ec4ea2fb1131e64", - "shasum": "" - }, - "require": { - "php": ">=5.3" - }, - "require-dev": { - "codeclimate/php-test-reporter": "dev-master", - "phpcompatibility/php-compatibility": "^9.1.1", - "phpmd/phpmd": "^2.6.0", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "^2.9.2", - "symfony/console": "~2.5 || ~3.0 || ~4.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Vaimo\\TopSort\\": "src/", - "Vaimo\\TopSort\\Tests\\": "tests/Tests/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marc J. Schmidt", - "email": "marc@marcjschmidt.de" - } - ], - "description": "High-Performance TopSort/Dependency resolving algorithm (compatibility version to work with 5.3)", - "keywords": [ - "dependency resolving", - "topological sort", - "topsort" - ], - "support": { - "source": "https://github.com/vaimo/topological-sort/tree/1.0.0" - }, - "time": "2019-04-13T14:15:06+00:00" } ], "aliases": [], @@ -6955,5 +6687,5 @@ "platform-overrides": { "php": "8.1.99" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index e7abd2a774..f6cc07dab3 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -1,12 +1,13 @@ parameters: featureToggles: bleedingEdge: true - skipCheckGenericClasses: [] + skipCheckGenericClasses!: [] explicitMixedInUnknownGenericNew: true explicitMixedForGlobalVariables: true explicitMixedViaIsArray: true arrayFilter: true arrayUnpacking: true + arrayValues: true nodeConnectingVisitorCompatibility: false nodeConnectingVisitorRule: true disableCheckMissingIterableValueType: true @@ -16,6 +17,41 @@ parameters: checkUnresolvableParameterTypes: true readOnlyByPhpDoc: true phpDocParserRequireWhitespaceBeforeDescription: true + phpDocParserIncludeLines: true + enableIgnoreErrorsWithinPhpDocs: true runtimeReflectionRules: true notAnalysedTrait: true curlSetOptTypes: true + listType: true + abstractTraitMethod: true + missingMagicSerializationRule: true + nullContextForVoidReturningFunctions: true + unescapeStrings: true + alwaysCheckTooWideReturnTypeFinalMethods: true + duplicateStubs: true + logicalXor: true + betterNoop: true + invarianceComposition: true + alwaysTrueAlwaysReported: true + disableUnreachableBranchesRules: true + varTagType: true + closureDefaultParameterTypeRule: true + newRuleLevelHelper: true + instanceofType: true + paramOutVariance: true + allInvalidPhpDocs: true + strictStaticMethodTemplateTypeVariance: true + propertyVariance: true + genericPrototypeMessage: true + stricterFunctionMap: true + invalidPhpDocTagLine: true + detectDeadTypeInMultiCatch: true + zeroFiles: true + projectServicesNotInAnalysedPaths: true + callUserFunc: true + finalByPhpDoc: true + magicConstantOutOfContext: true + paramOutType: true + pure: true + stubFiles: + - ../stubs/bleedingEdge/Rule.stub diff --git a/conf/config.level0.neon b/conf/config.level0.neon index b53cd3127b..08a2153f0a 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -20,6 +20,10 @@ conditionalTags: phpstan.rules.rule: %featureToggles.runtimeReflectionRules% PHPStan\Rules\Api\RuntimeReflectionInstantiationRule: phpstan.rules.rule: %featureToggles.runtimeReflectionRules% + PHPStan\Rules\Methods\MissingMagicSerializationMethodsRule: + phpstan.rules.rule: %featureToggles.missingMagicSerializationRule% + PHPStan\Rules\Constants\MagicConstantContextRule: + phpstan.rules.rule: %featureToggles.magicConstantOutOfContext% rules: - PHPStan\Rules\Api\ApiInstantiationRule @@ -29,11 +33,13 @@ rules: - PHPStan\Rules\Api\ApiMethodCallRule - PHPStan\Rules\Api\ApiStaticCallRule - PHPStan\Rules\Api\ApiTraitUseRule + - PHPStan\Rules\Api\GetTemplateTypeRule - PHPStan\Rules\Api\PhpStanNamespaceIn3rdPartyPackageRule - PHPStan\Rules\Arrays\DuplicateKeysInLiteralArraysRule - PHPStan\Rules\Arrays\EmptyArrayItemRule - PHPStan\Rules\Arrays\OffsetAccessWithoutDimForReadingRule - PHPStan\Rules\Cast\UnsetCastRule + - PHPStan\Rules\Classes\AllowedSubTypesRule - PHPStan\Rules\Classes\ClassAttributesRule - PHPStan\Rules\Classes\ClassConstantAttributesRule - PHPStan\Rules\Classes\ClassConstantRule @@ -46,41 +52,61 @@ rules: - PHPStan\Rules\Classes\InstantiationRule - PHPStan\Rules\Classes\InstantiationCallableRule - PHPStan\Rules\Classes\InvalidPromotedPropertiesRule + - PHPStan\Rules\Classes\LocalTypeAliasesRule + - PHPStan\Rules\Classes\LocalTypeTraitAliasesRule - PHPStan\Rules\Classes\NewStaticRule - PHPStan\Rules\Classes\NonClassAttributeClassRule + - PHPStan\Rules\Classes\ReadOnlyClassRule - PHPStan\Rules\Classes\TraitAttributeClassRule + - PHPStan\Rules\Constants\DynamicClassConstantFetchRule - PHPStan\Rules\Constants\FinalConstantRule + - PHPStan\Rules\Constants\NativeTypedClassConstantRule - PHPStan\Rules\EnumCases\EnumCaseAttributesRule + - PHPStan\Rules\Exceptions\NoncapturingCatchRule - PHPStan\Rules\Exceptions\ThrowExpressionRule - PHPStan\Rules\Functions\ArrowFunctionAttributesRule - PHPStan\Rules\Functions\ArrowFunctionReturnNullsafeByRefRule - - PHPStan\Rules\Functions\CallToFunctionParametersRule - PHPStan\Rules\Functions\ClosureAttributesRule - PHPStan\Rules\Functions\DefineParametersRule - PHPStan\Rules\Functions\ExistingClassesInArrowFunctionTypehintsRule + - PHPStan\Rules\Functions\CallToFunctionParametersRule - PHPStan\Rules\Functions\ExistingClassesInClosureTypehintsRule - PHPStan\Rules\Functions\ExistingClassesInTypehintsRule - PHPStan\Rules\Functions\FunctionAttributesRule - PHPStan\Rules\Functions\InnerFunctionRule + - PHPStan\Rules\Functions\InvalidLexicalVariablesInClosureUseRule - PHPStan\Rules\Functions\ParamAttributesRule - PHPStan\Rules\Functions\PrintfParametersRule + - PHPStan\Rules\Functions\RedefinedParametersRule - PHPStan\Rules\Functions\ReturnNullsafeByRefRule + - PHPStan\Rules\Ignore\IgnoreParseErrorRule + - PHPStan\Rules\Functions\VariadicParametersDeclarationRule - PHPStan\Rules\Keywords\ContinueBreakInLoopRule + - PHPStan\Rules\Keywords\DeclareStrictTypesRule - PHPStan\Rules\Methods\AbstractMethodInNonAbstractClassRule + - PHPStan\Rules\Methods\AbstractPrivateMethodRule - PHPStan\Rules\Methods\CallMethodsRule - PHPStan\Rules\Methods\CallStaticMethodsRule + - PHPStan\Rules\Methods\ConstructorReturnTypeRule - PHPStan\Rules\Methods\ExistingClassesInTypehintsRule - PHPStan\Rules\Methods\FinalPrivateMethodRule - PHPStan\Rules\Methods\MethodCallableRule + - PHPStan\Rules\Methods\MethodVisibilityInInterfaceRule - PHPStan\Rules\Methods\MissingMethodImplementationRule - PHPStan\Rules\Methods\MethodAttributesRule - PHPStan\Rules\Methods\StaticMethodCallableRule + - PHPStan\Rules\Names\UsedNamesRule - PHPStan\Rules\Operators\InvalidAssignVarRule - PHPStan\Rules\Properties\AccessPropertiesInAssignRule - PHPStan\Rules\Properties\AccessStaticPropertiesInAssignRule + - PHPStan\Rules\Properties\InvalidCallablePropertyTypeRule - PHPStan\Rules\Properties\MissingReadOnlyPropertyAssignRule + - PHPStan\Rules\Properties\PropertiesInInterfaceRule - PHPStan\Rules\Properties\PropertyAttributesRule - PHPStan\Rules\Properties\ReadOnlyPropertyRule + - PHPStan\Rules\Traits\ConflictingTraitConstantsRule + - PHPStan\Rules\Traits\ConstantsInTraitsRule + - PHPStan\Rules\Types\InvalidTypesInUnionRule - PHPStan\Rules\Variables\UnsetRule - PHPStan\Rules\Whitespace\FileWhitespaceRule @@ -89,6 +115,13 @@ services: class: PHPStan\Rules\Api\ApiClassConstFetchRule - class: PHPStan\Rules\Api\ApiInstanceofRule + - + class: PHPStan\Rules\Api\ApiInstanceofTypeRule + arguments: + enabled: %featureToggles.instanceofType% + deprecationRulesInstalled: %deprecationRulesInstalled% + tags: + - phpstan.rules.rule - class: PHPStan\Rules\Api\NodeConnectingVisitorAttributesRule - @@ -132,6 +165,9 @@ services: class: PHPStan\Rules\Methods\OverridingMethodRule arguments: checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures% + genericPrototypeMessage: %featureToggles.genericPrototypeMessage% + finalByPhpDoc: %featureToggles.finalByPhpDoc% + checkMissingOverrideMethodAttribute: %checkMissingOverrideMethodAttribute% tags: - phpstan.rules.rule @@ -247,14 +283,13 @@ services: tags: - phpstan.rules.rule - - - class: PHPStan\Rules\Classes\LocalTypeAliasesRule - arguments: - globalTypeAliases: %typeAliases% - tags: - - phpstan.rules.rule - - class: PHPStan\Reflection\ConstructorsHelper arguments: additionalConstructors: %additionalConstructors% + + - + class: PHPStan\Rules\Methods\MissingMagicSerializationMethodsRule + + - + class: PHPStan\Rules\Constants\MagicConstantContextRule diff --git a/conf/config.level2.neon b/conf/config.level2.neon index a4db7e8199..ae718efb17 100644 --- a/conf/config.level2.neon +++ b/conf/config.level2.neon @@ -13,6 +13,7 @@ rules: - PHPStan\Rules\Cast\PrintRule - PHPStan\Rules\Classes\AccessPrivateConstantThroughStaticRule - PHPStan\Rules\Comparison\UsageOfVoidMatchExpressionRule + - PHPStan\Rules\Constants\ValueAssignedToClassConstantRule - PHPStan\Rules\Functions\IncompatibleDefaultParameterTypeRule - PHPStan\Rules\Generics\ClassAncestorsRule - PHPStan\Rules\Generics\ClassTemplateTypeRule @@ -23,6 +24,7 @@ rules: - PHPStan\Rules\Generics\InterfaceAncestorsRule - PHPStan\Rules\Generics\InterfaceTemplateTypeRule - PHPStan\Rules\Generics\MethodTemplateTypeRule + - PHPStan\Rules\Generics\MethodTagTemplateTypeRule - PHPStan\Rules\Generics\MethodSignatureVarianceRule - PHPStan\Rules\Generics\TraitTemplateTypeRule - PHPStan\Rules\Generics\UsedTraitsRule @@ -33,20 +35,37 @@ rules: - PHPStan\Rules\Operators\InvalidComparisonOperationRule - PHPStan\Rules\PhpDoc\FunctionConditionalReturnTypeRule - PHPStan\Rules\PhpDoc\MethodConditionalReturnTypeRule + - PHPStan\Rules\PhpDoc\FunctionAssertRule + - PHPStan\Rules\PhpDoc\MethodAssertRule + - PHPStan\Rules\PhpDoc\IncompatibleSelfOutTypeRule - PHPStan\Rules\PhpDoc\IncompatibleClassConstantPhpDocTypeRule - PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule - PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule - - PHPStan\Rules\PhpDoc\InvalidPhpDocTagValueRule - - PHPStan\Rules\PhpDoc\InvalidPHPStanDocTagRule - PHPStan\Rules\PhpDoc\InvalidThrowsPhpDocValueRule - - PHPStan\Rules\PhpDoc\WrongVariableNameInVarTagRule - PHPStan\Rules\Properties\AccessPrivatePropertyThroughStaticRule + - PHPStan\Rules\Classes\RequireImplementsRule + - PHPStan\Rules\Classes\RequireExtendsRule + - PHPStan\Rules\PhpDoc\RequireImplementsDefinitionClassRule + - PHPStan\Rules\PhpDoc\RequireExtendsDefinitionClassRule + - PHPStan\Rules\PhpDoc\RequireExtendsDefinitionTraitRule conditionalTags: + PHPStan\Rules\Functions\IncompatibleArrowFunctionDefaultParameterTypeRule: + phpstan.rules.rule: %featureToggles.closureDefaultParameterTypeRule% + PHPStan\Rules\Functions\IncompatibleClosureDefaultParameterTypeRule: + phpstan.rules.rule: %featureToggles.closureDefaultParameterTypeRule% PHPStan\Rules\Methods\IllegalConstructorMethodCallRule: phpstan.rules.rule: %featureToggles.illegalConstructorMethodCall% PHPStan\Rules\Methods\IllegalConstructorStaticCallRule: phpstan.rules.rule: %featureToggles.illegalConstructorMethodCall% + PHPStan\Rules\PhpDoc\VarTagChangedExpressionTypeRule: + phpstan.rules.rule: %featureToggles.varTagType% + PHPStan\Rules\Generics\PropertyVarianceRule: + phpstan.rules.rule: %featureToggles.propertyVariance% + PHPStan\Rules\Pure\PureFunctionRule: + phpstan.rules.rule: %featureToggles.pure% + PHPStan\Rules\Pure\PureMethodRule: + phpstan.rules.rule: %featureToggles.pure% services: - @@ -55,6 +74,23 @@ services: checkClassCaseSensitivity: %checkClassCaseSensitivity% tags: - phpstan.rules.rule + + - + class: PHPStan\Rules\PhpDoc\RequireExtendsCheck + arguments: + checkClassCaseSensitivity: %checkClassCaseSensitivity% + + - + class: PHPStan\Rules\PhpDoc\RequireImplementsDefinitionTraitRule + arguments: + checkClassCaseSensitivity: %checkClassCaseSensitivity% + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\Functions\IncompatibleArrowFunctionDefaultParameterTypeRule + - + class: PHPStan\Rules\Functions\IncompatibleClosureDefaultParameterTypeRule - class: PHPStan\Rules\Functions\CallCallablesRule arguments: @@ -65,6 +101,13 @@ services: class: PHPStan\Rules\Methods\IllegalConstructorMethodCallRule - class: PHPStan\Rules\Methods\IllegalConstructorStaticCallRule + - + class: PHPStan\Rules\PhpDoc\InvalidPhpDocTagValueRule + arguments: + checkAllInvalidPhpDocs: %featureToggles.allInvalidPhpDocs% + invalidPhpDocTagLine: %featureToggles.invalidPhpDocTagLine% + tags: + - phpstan.rules.rule - class: PHPStan\Rules\PhpDoc\InvalidPhpDocVarTagTypeRule arguments: @@ -72,3 +115,26 @@ services: checkMissingVarTagTypehint: %checkMissingVarTagTypehint% tags: - phpstan.rules.rule + - + class: PHPStan\Rules\PhpDoc\InvalidPHPStanDocTagRule + arguments: + checkAllInvalidPhpDocs: %featureToggles.allInvalidPhpDocs% + tags: + - phpstan.rules.rule + - + class: PHPStan\Rules\PhpDoc\VarTagChangedExpressionTypeRule + - + class: PHPStan\Rules\PhpDoc\WrongVariableNameInVarTagRule + arguments: + checkTypeAgainstNativeType: %featureToggles.varTagType% + tags: + - phpstan.rules.rule + - + class: PHPStan\Rules\Generics\PropertyVarianceRule + arguments: + readOnlyByPhpDoc: %featureToggles.readOnlyByPhpDoc% + - + class: PHPStan\Rules\Pure\PureFunctionRule + + - + class: PHPStan\Rules\Pure\PureMethodRule diff --git a/conf/config.level3.neon b/conf/config.level3.neon index d777a8b60f..5540500714 100644 --- a/conf/config.level3.neon +++ b/conf/config.level3.neon @@ -8,6 +8,10 @@ conditionalTags: phpstan.rules.rule: %featureToggles.readOnlyByPhpDoc% PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyAssignRule: phpstan.rules.rule: %featureToggles.readOnlyByPhpDoc% + PHPStan\Rules\Variables\ParameterOutAssignedTypeRule: + phpstan.rules.rule: %featureToggles.paramOutType% + PHPStan\Rules\Variables\ParameterOutExecutionEndTypeRule: + phpstan.rules.rule: %featureToggles.paramOutType% rules: - PHPStan\Rules\Arrays\ArrayDestructuringRule @@ -16,6 +20,7 @@ rules: - PHPStan\Rules\Arrays\OffsetAccessAssignOpRule - PHPStan\Rules\Arrays\OffsetAccessValueAssignmentRule - PHPStan\Rules\Arrays\UnpackIterableInArrayRule + - PHPStan\Rules\Exceptions\ThrowExprTypeRule - PHPStan\Rules\Functions\ArrowFunctionReturnTypeRule - PHPStan\Rules\Functions\ClosureReturnTypeRule - PHPStan\Rules\Functions\ReturnTypeRule @@ -91,3 +96,9 @@ services: - class: PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyAssignRule + + - + class: PHPStan\Rules\Variables\ParameterOutAssignedTypeRule + + - + class: PHPStan\Rules\Variables\ParameterOutExecutionEndTypeRule diff --git a/conf/config.level4.neon b/conf/config.level4.neon index e5445990a6..cb79c9cca5 100644 --- a/conf/config.level4.neon +++ b/conf/config.level4.neon @@ -3,15 +3,11 @@ includes: rules: - PHPStan\Rules\Arrays\DeadForeachRule - - PHPStan\Rules\Comparison\NumberComparisonOperatorsConstantConditionRule - - PHPStan\Rules\DeadCode\NoopRule - PHPStan\Rules\DeadCode\UnreachableStatementRule - PHPStan\Rules\DeadCode\UnusedPrivateConstantRule - PHPStan\Rules\DeadCode\UnusedPrivateMethodRule - - PHPStan\Rules\Exceptions\CatchWithUnthrownExceptionRule - PHPStan\Rules\Exceptions\OverwrittenExitPointByFinallyRule - PHPStan\Rules\Functions\CallToFunctionStatementWithoutSideEffectsRule - - PHPStan\Rules\Methods\CallToConstructorStatementWithoutSideEffectsRule - PHPStan\Rules\Methods\CallToMethodStatementWithoutSideEffectsRule - PHPStan\Rules\Methods\CallToStaticMethodStatementWithoutSideEffectsRule - PHPStan\Rules\Methods\NullsafeMethodCallRule @@ -28,6 +24,36 @@ conditionalTags: phpstan.collector: %featureToggles.notAnalysedTrait% PHPStan\Rules\Traits\NotAnalysedTraitRule: phpstan.rules.rule: %featureToggles.notAnalysedTrait% + PHPStan\Rules\Comparison\LogicalXorConstantConditionRule: + phpstan.rules.rule: %featureToggles.logicalXor% + PHPStan\Rules\DeadCode\BetterNoopRule: + phpstan.rules.rule: %featureToggles.betterNoop% + PHPStan\Rules\TooWideTypehints\TooWideFunctionParameterOutTypeRule: + phpstan.rules.rule: %featureToggles.paramOutType% + PHPStan\Rules\TooWideTypehints\TooWideMethodParameterOutTypeRule: + phpstan.rules.rule: %featureToggles.paramOutType% + PHPStan\Rules\DeadCode\CallToConstructorStatementWithoutImpurePointsRule: + phpstan.rules.rule: %featureToggles.pure% + PHPStan\Rules\DeadCode\PossiblyPureNewCollector: + phpstan.collector: %featureToggles.pure% + PHPStan\Rules\DeadCode\ConstructorWithoutImpurePointsCollector: + phpstan.collector: %featureToggles.pure% + PHPStan\Rules\DeadCode\CallToFunctionStatementWithoutImpurePointsRule: + phpstan.rules.rule: %featureToggles.pure% + PHPStan\Rules\DeadCode\PossiblyPureFuncCallCollector: + phpstan.collector: %featureToggles.pure% + PHPStan\Rules\DeadCode\FunctionWithoutImpurePointsCollector: + phpstan.collector: %featureToggles.pure% + PHPStan\Rules\DeadCode\CallToMethodStatementWithoutImpurePointsRule: + phpstan.rules.rule: %featureToggles.pure% + PHPStan\Rules\DeadCode\PossiblyPureMethodCallCollector: + phpstan.collector: %featureToggles.pure% + PHPStan\Rules\DeadCode\MethodWithoutImpurePointsCollector: + phpstan.collector: %featureToggles.pure% + PHPStan\Rules\DeadCode\CallToStaticMethodStatementWithoutImpurePointsRule: + phpstan.rules.rule: %featureToggles.pure% + PHPStan\Rules\DeadCode\PossiblyPureStaticCallCollector: + phpstan.collector: %featureToggles.pure% parameters: checkAdvancedIsset: true @@ -38,6 +64,7 @@ services: arguments: checkAlwaysTrueInstanceof: %checkAlwaysTrueInstanceof% treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% tags: - phpstan.rules.rule @@ -45,6 +72,8 @@ services: class: PHPStan\Rules\Comparison\BooleanAndConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + bleedingEdge: %featureToggles.bleedingEdge% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% tags: - phpstan.rules.rule @@ -52,6 +81,8 @@ services: class: PHPStan\Rules\Comparison\BooleanOrConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + bleedingEdge: %featureToggles.bleedingEdge% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% tags: - phpstan.rules.rule @@ -59,9 +90,50 @@ services: class: PHPStan\Rules\Comparison\BooleanNotConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% tags: - phpstan.rules.rule + - + class: PHPStan\Rules\DeadCode\NoopRule + arguments: + better: %featureToggles.betterNoop% + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\DeadCode\CallToConstructorStatementWithoutImpurePointsRule + + - + class: PHPStan\Rules\DeadCode\ConstructorWithoutImpurePointsCollector + + - + class: PHPStan\Rules\DeadCode\PossiblyPureNewCollector + + - + class: PHPStan\Rules\DeadCode\CallToFunctionStatementWithoutImpurePointsRule + + - + class: PHPStan\Rules\DeadCode\FunctionWithoutImpurePointsCollector + + - + class: PHPStan\Rules\DeadCode\PossiblyPureFuncCallCollector + + - + class: PHPStan\Rules\DeadCode\CallToMethodStatementWithoutImpurePointsRule + + - + class: PHPStan\Rules\DeadCode\MethodWithoutImpurePointsCollector + + - + class: PHPStan\Rules\DeadCode\PossiblyPureMethodCallCollector + + - + class: PHPStan\Rules\DeadCode\CallToStaticMethodStatementWithoutImpurePointsRule + + - + class: PHPStan\Rules\DeadCode\PossiblyPureStaticCallCollector + - class: PHPStan\Rules\DeadCode\UnusedPrivatePropertyRule arguments: @@ -82,6 +154,7 @@ services: class: PHPStan\Rules\Comparison\ElseIfConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% tags: - phpstan.rules.rule @@ -97,6 +170,7 @@ services: arguments: checkAlwaysTrueCheckTypeFunctionCall: %checkAlwaysTrueCheckTypeFunctionCall% treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% tags: - phpstan.rules.rule @@ -105,6 +179,7 @@ services: arguments: checkAlwaysTrueCheckTypeFunctionCall: %checkAlwaysTrueCheckTypeFunctionCall% treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% tags: - phpstan.rules.rule @@ -113,13 +188,33 @@ services: arguments: checkAlwaysTrueCheckTypeFunctionCall: %checkAlwaysTrueCheckTypeFunctionCall% treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% tags: - phpstan.rules.rule + - + class: PHPStan\Rules\Comparison\LogicalXorConstantConditionRule + arguments: + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + + - + class: PHPStan\Rules\DeadCode\BetterNoopRule + - class: PHPStan\Rules\Comparison\MatchExpressionRule arguments: checkAlwaysTrueStrictComparison: %checkAlwaysTrueStrictComparison% + disableUnreachable: %featureToggles.disableUnreachableBranchesRules% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\Comparison\NumberComparisonOperatorsConstantConditionRule + arguments: + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule @@ -127,6 +222,8 @@ services: class: PHPStan\Rules\Comparison\StrictComparisonOfDifferentTypesRule arguments: checkAlwaysTrueStrictComparison: %checkAlwaysTrueStrictComparison% + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% tags: - phpstan.rules.rule @@ -134,6 +231,8 @@ services: class: PHPStan\Rules\Comparison\ConstantLooseComparisonRule arguments: checkAlwaysTrueLooseComparison: %checkAlwaysTrueLooseComparison% + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% - class: PHPStan\Rules\Comparison\TernaryOperatorConstantConditionRule @@ -146,6 +245,7 @@ services: class: PHPStan\Rules\Comparison\UnreachableIfBranchesRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + disable: %featureToggles.disableUnreachableBranchesRules% tags: - phpstan.rules.rule @@ -153,6 +253,7 @@ services: class: PHPStan\Rules\Comparison\UnreachableTernaryElseBranchRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + disable: %featureToggles.disableUnreachableBranchesRules% tags: - phpstan.rules.rule @@ -170,10 +271,18 @@ services: tags: - phpstan.rules.rule + - + class: PHPStan\Rules\Methods\CallToConstructorStatementWithoutSideEffectsRule + arguments: + reportNoConstructor: %featureToggles.pure% + tags: + - phpstan.rules.rule + - class: PHPStan\Rules\TooWideTypehints\TooWideMethodReturnTypehintRule arguments: checkProtectedAndPublicMethods: %checkTooWideReturnTypesInProtectedAndPublicMethods% + alwaysCheckFinal: %featureToggles.alwaysCheckTooWideReturnTypeFinalMethods% tags: - phpstan.rules.rule @@ -190,3 +299,17 @@ services: - class: PHPStan\Rules\Traits\NotAnalysedTraitRule + + - + class: PHPStan\Rules\Exceptions\CatchWithUnthrownExceptionRule + arguments: + exceptionTypeResolver: @exceptionTypeResolver + reportUncheckedExceptionDeadCatch: %exceptions.reportUncheckedExceptionDeadCatch% + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\TooWideTypehints\TooWideFunctionParameterOutTypeRule + + - + class: PHPStan\Rules\TooWideTypehints\TooWideMethodParameterOutTypeRule diff --git a/conf/config.level5.neon b/conf/config.level5.neon index 3339b0a836..5a7516931d 100644 --- a/conf/config.level5.neon +++ b/conf/config.level5.neon @@ -8,6 +8,10 @@ parameters: conditionalTags: PHPStan\Rules\Functions\ArrayFilterRule: phpstan.rules.rule: %featureToggles.arrayFilter% + PHPStan\Rules\Functions\ArrayValuesRule: + phpstan.rules.rule: %featureToggles.arrayValues% + PHPStan\Rules\Functions\CallUserFuncRule: + phpstan.rules.rule: %featureToggles.callUserFunc% rules: - PHPStan\Rules\DateTimeInstantiationRule @@ -23,3 +27,13 @@ services: - class: PHPStan\Rules\Functions\ArrayFilterRule + arguments: + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + + - + class: PHPStan\Rules\Functions\ArrayValuesRule + arguments: + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + + - + class: PHPStan\Rules\Functions\CallUserFuncRule diff --git a/conf/config.level6.neon b/conf/config.level6.neon index 05f3616832..545fac6ad2 100644 --- a/conf/config.level6.neon +++ b/conf/config.level6.neon @@ -9,8 +9,21 @@ parameters: rules: - PHPStan\Rules\Constants\MissingClassConstantTypehintRule - - PHPStan\Rules\Functions\MissingFunctionParameterTypehintRule - PHPStan\Rules\Functions\MissingFunctionReturnTypehintRule - - PHPStan\Rules\Methods\MissingMethodParameterTypehintRule - PHPStan\Rules\Methods\MissingMethodReturnTypehintRule - PHPStan\Rules\Properties\MissingPropertyTypehintRule + +services: + - + class: PHPStan\Rules\Functions\MissingFunctionParameterTypehintRule + arguments: + paramOut: %featureToggles.paramOutType% + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\Methods\MissingMethodParameterTypehintRule + arguments: + paramOut: %featureToggles.paramOutType% + tags: + - phpstan.rules.rule diff --git a/conf/config.neon b/conf/config.neon index e4e9c85a5f..cab4e7356d 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1,3 +1,6 @@ +includes: + - parametersSchema.neon + parameters: bootstrapFiles: - ../stubs/runtime/ReflectionUnionType.php @@ -10,6 +13,7 @@ parameters: paths: [] exceptions: implicitThrows: true + reportUncheckedExceptionDeadCatch: true uncheckedExceptionRegexes: [] uncheckedExceptionClasses: [] checkedExceptionRegexes: [] @@ -31,11 +35,13 @@ parameters: - InfiniteIterator - CachingIterator - RegexIterator + - ReflectionEnum explicitMixedInUnknownGenericNew: false explicitMixedForGlobalVariables: false explicitMixedViaIsArray: false arrayFilter: false arrayUnpacking: false + arrayValues: false nodeConnectingVisitorCompatibility: true nodeConnectingVisitorRule: false illegalConstructorMethodCall: false @@ -46,16 +52,50 @@ parameters: checkUnresolvableParameterTypes: false readOnlyByPhpDoc: false phpDocParserRequireWhitespaceBeforeDescription: false + phpDocParserIncludeLines: false + enableIgnoreErrorsWithinPhpDocs: false runtimeReflectionRules: false notAnalysedTrait: false curlSetOptTypes: false + listType: false + abstractTraitMethod: false + missingMagicSerializationRule: false + nullContextForVoidReturningFunctions: false + unescapeStrings: false + alwaysCheckTooWideReturnTypeFinalMethods: false + duplicateStubs: false + logicalXor: false + betterNoop: false + invarianceComposition: false + alwaysTrueAlwaysReported: false + disableUnreachableBranchesRules: false + varTagType: false + closureDefaultParameterTypeRule: false + newRuleLevelHelper: false + instanceofType: false + paramOutVariance: false + allInvalidPhpDocs: false + strictStaticMethodTemplateTypeVariance: false + propertyVariance: false + genericPrototypeMessage: false + stricterFunctionMap: false + invalidPhpDocTagLine: false + detectDeadTypeInMultiCatch: false + zeroFiles: false + projectServicesNotInAnalysedPaths: false + callUserFunc: false + finalByPhpDoc: false + magicConstantOutOfContext: false + paramOutType: false + pure: false fileExtensions: - php checkAdvancedIsset: false - checkAlwaysTrueCheckTypeFunctionCall: false - checkAlwaysTrueInstanceof: false - checkAlwaysTrueStrictComparison: false - checkAlwaysTrueLooseComparison: false + checkAlwaysTrueCheckTypeFunctionCall: %featureToggles.alwaysTrueAlwaysReported% + checkAlwaysTrueInstanceof: %featureToggles.alwaysTrueAlwaysReported% + checkAlwaysTrueStrictComparison: %featureToggles.alwaysTrueAlwaysReported% + checkAlwaysTrueLooseComparison: %featureToggles.alwaysTrueAlwaysReported% + reportAlwaysTrueInLastCondition: false checkClassCaseSensitivity: false checkExplicitMixed: false checkImplicitMixed: false @@ -71,6 +111,7 @@ parameters: checkNullables: false checkThisOnly: true checkUnionTypes: false + checkBenevolentUnionTypes: false checkExplicitMixedMissingReturn: false checkPhpDocMissingReturn: false checkPhpDocMethodSignatures: false @@ -79,11 +120,17 @@ parameters: checkTooWideReturnTypesInProtectedAndPublicMethods: false checkUninitializedProperties: false checkDynamicProperties: false + deprecationRulesInstalled: false inferPrivatePropertyTypeFromConstructor: false reportMaybes: false reportMaybesInMethodSignatures: false reportMaybesInPropertyPhpDocTypes: false reportStaticMethodSignatures: false + reportWrongPhpDocTypeInVarTag: false + reportAnyTypeWideningInVarTag: false + reportPossiblyNonexistentGeneralArrayOffset: false + reportPossiblyNonexistentConstantArrayOffset: false + checkMissingOverrideMethodAttribute: false mixinExcludeClasses: [] scanFiles: [] scanDirectories: [] @@ -98,8 +145,10 @@ parameters: polluteScopeWithAlwaysIterableForeach: true propertyAlwaysWrittenTags: [] propertyAlwaysReadTags: [] + fixerTmpDir: %pro.tmpDir% #unused additionalConstructors: [] treatPhpDocTypesAsCertain: true + usePathConstantsAsConstantString: false rememberPossiblyImpureFunctionValues: true tipsOfTheDay: true reportMagicMethods: false @@ -126,8 +175,10 @@ parameters: - ../stubs/ArrayObject.stub - ../stubs/WeakReference.stub - ../stubs/ext-ds.stub + - ../stubs/ImagickPixel.stub - ../stubs/PDOStatement.stub - ../stubs/date.stub + - ../stubs/ibm_db2.stub - ../stubs/mysqli.stub - ../stubs/zip.stub - ../stubs/dom.stub @@ -136,6 +187,7 @@ parameters: - ../stubs/Exception.stub - ../stubs/arrayFunctions.stub - ../stubs/core.stub + - ../stubs/typeCheckingFunctions.stub earlyTerminatingMethodCalls: [] earlyTerminatingFunctionCalls: [] memoryLimitFile: %tmpDir%/.memory_limit @@ -197,7 +249,13 @@ parameters: - ZEND_THREAD_SAFE customRulesetUsed: null editorUrl: null + editorUrlTitle: null errorFormat: null + sysGetTempDir: ::sys_get_temp_dir() + pro: + dnsServers: + - '1.1.1.2' + tmpDir: %sysGetTempDir%/phpstan-fixer __validate: true extensions: @@ -206,190 +264,6 @@ extensions: parametersSchema: PHPStan\DependencyInjection\ParametersSchemaExtension validateIgnoredErrors: PHPStan\DependencyInjection\ValidateIgnoredErrorsExtension -parametersSchema: - bootstrapFiles: listOf(string()) - excludes_analyse: listOf(string()) - excludePaths: schema(anyOf( - structure([ - analyse: listOf(string()), - ]), - structure([ - analyseAndScan: listOf(string()), - ]) - structure([ - analyse: listOf(string()), - analyseAndScan: listOf(string()) - ]) - ), nullable()) - level: schema(anyOf(int(), string()), nullable()) - paths: listOf(string()) - exceptions: structure([ - implicitThrows: bool(), - uncheckedExceptionRegexes: listOf(string()), - uncheckedExceptionClasses: listOf(string()), - checkedExceptionRegexes: listOf(string()), - checkedExceptionClasses: listOf(string()), - check: structure([ - missingCheckedExceptionInThrows: bool(), - tooWideThrowType: bool() - ]) - ]) - featureToggles: structure([ - bleedingEdge: bool(), - disableRuntimeReflectionProvider: bool(), - skipCheckGenericClasses: listOf(string()), - explicitMixedInUnknownGenericNew: bool(), - explicitMixedForGlobalVariables: bool(), - explicitMixedViaIsArray: bool(), - arrayFilter: bool(), - arrayUnpacking: bool(), - nodeConnectingVisitorCompatibility: bool(), - nodeConnectingVisitorRule: bool(), - illegalConstructorMethodCall: bool(), - disableCheckMissingIterableValueType: bool(), - strictUnnecessaryNullsafePropertyFetch: bool(), - looseComparison: bool(), - consistentConstructor: bool() - checkUnresolvableParameterTypes: bool() - readOnlyByPhpDoc: bool() - phpDocParserRequireWhitespaceBeforeDescription: bool() - runtimeReflectionRules: bool() - notAnalysedTrait: bool() - curlSetOptTypes: bool() - ]) - fileExtensions: listOf(string()) - checkAdvancedIsset: bool() - checkAlwaysTrueCheckTypeFunctionCall: bool() - checkAlwaysTrueInstanceof: bool() - checkAlwaysTrueStrictComparison: bool() - checkAlwaysTrueLooseComparison: bool() - checkClassCaseSensitivity: bool() - checkExplicitMixed: bool() - checkImplicitMixed: bool() - checkFunctionArgumentTypes: bool() - checkFunctionNameCase: bool() - checkGenericClassInNonGenericObjectType: bool() - checkInternalClassCaseSensitivity: bool() - checkMissingIterableValueType: bool() - checkMissingCallableSignature: bool() - checkMissingVarTagTypehint: bool() - checkArgumentsPassedByReference: bool() - checkMaybeUndefinedVariables: bool() - checkNullables: bool() - checkThisOnly: bool() - checkUnionTypes: bool() - checkExplicitMixedMissingReturn: bool() - checkPhpDocMissingReturn: bool() - checkPhpDocMethodSignatures: bool() - checkExtraArguments: bool() - checkMissingTypehints: bool() - checkTooWideReturnTypesInProtectedAndPublicMethods: bool() - checkUninitializedProperties: bool() - checkDynamicProperties: bool() - inferPrivatePropertyTypeFromConstructor: bool() - - tipsOfTheDay: bool() - reportMaybes: bool() - reportMaybesInMethodSignatures: bool() - reportMaybesInPropertyPhpDocTypes: bool() - reportStaticMethodSignatures: bool() - parallel: structure([ - jobSize: int(), - processTimeout: float(), - maximumNumberOfProcesses: int(), - minimumNumberOfJobsPerProcess: int(), - buffer: int() - ]) - phpVersion: schema(anyOf(schema(int(), min(70100), max(80299))), nullable()) - polluteScopeWithLoopInitialAssignments: bool() - polluteScopeWithAlwaysIterableForeach: bool() - propertyAlwaysWrittenTags: listOf(string()) - propertyAlwaysReadTags: listOf(string()) - additionalConstructors: listOf(string()) - treatPhpDocTypesAsCertain: bool() - rememberPossiblyImpureFunctionValues: bool() - reportMagicMethods: bool() - reportMagicProperties: bool() - ignoreErrors: listOf( - anyOf( - string(), - structure([ - messages: listOf(string()) - ?path: string() - ?reportUnmatched: bool() - ]), - structure([ - message: string() - ?path: string() - ?reportUnmatched: bool() - ]), - structure([ - message: string() - count: int() - path: string() - ?reportUnmatched: bool() - ]), - structure([ - message: string() - paths: listOf(string()) - ?reportUnmatched: bool() - ]), - structure([ - messages: listOf(string()) - paths: listOf(string()) - ?reportUnmatched: bool() - ]) - ) - ) - internalErrorsCountLimit: int() - cache: structure([ - nodesByFileCountMax: int() - nodesByStringCountMax: int() - ]) - reportUnmatchedIgnoredErrors: bool() - scopeClass: string() - typeAliases: arrayOf(string()) - universalObjectCratesClasses: listOf(string()) - stubFiles: listOf(string()) - earlyTerminatingMethodCalls: arrayOf(listOf(string())) - earlyTerminatingFunctionCalls: listOf(string()) - memoryLimitFile: string() - tempResultCachePath: string() - resultCachePath: string() - resultCacheChecksProjectExtensionFilesDependencies: bool() - staticReflectionClassNamePatterns: listOf(string()) - dynamicConstantNames: listOf(string()) - customRulesetUsed: schema(bool(), nullable()) - rootDir: string() - tmpDir: string() - currentWorkingDirectory: string() - cliArgumentsVariablesRegistered: bool() - mixinExcludeClasses: listOf(string()) - scanFiles: listOf(string()) - scanDirectories: listOf(string()) - fixerTmpDir: string() - editorUrl: schema(string(), nullable()) - errorFormat: schema(string(), nullable()) - - # irrelevant Nette parameters - debugMode: bool() - productionMode: bool() - tempDir: string() - __validate: bool() - - # internal parameters only for DerivativeContainerFactory - additionalConfigFiles: arrayOf(string()) - generateBaselineFile: schema(string(), nullable()) - analysedPaths: listOf(string()) - composerAutoloaderProjectPaths: listOf(string()) - analysedPathsFromConfig: listOf(string()) - usedLevel: string() - cliAutoloadFile: schema(string(), nullable()) - - # internal - static reflection - singleReflectionFile: schema(string(), nullable()) - singleReflectionInsteadOfFile: schema(string(), nullable()) - rules: - PHPStan\Rules\Debug\DumpTypeRule - PHPStan\Rules\Debug\FileAssertRule @@ -407,6 +281,8 @@ conditionalTags: phpstan.parser.richParserNodeVisitor: %featureToggles.nodeConnectingVisitorCompatibility% PHPStan\Parser\CurlSetOptArgVisitor: phpstan.parser.richParserNodeVisitor: %featureToggles.curlSetOptTypes% + PHPStan\Parser\TypeTraverserInstanceofVisitor: + phpstan.parser.richParserNodeVisitor: %featureToggles.instanceofType% services: - @@ -441,14 +317,32 @@ services: tags: - phpstan.parser.richParserNodeVisitor + - + class: PHPStan\Parser\ClosureBindToVarVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\ClosureBindArgVisitor + tags: + - phpstan.parser.richParserNodeVisitor + - class: PHPStan\Parser\CurlSetOptArgVisitor + - + class: PHPStan\Parser\TypeTraverserInstanceofVisitor + - class: PHPStan\Parser\ArrowFunctionArgVisitor tags: - phpstan.parser.richParserNodeVisitor + - + class: PHPStan\Parser\MagicConstantParamDefaultVisitor + tags: + - phpstan.parser.richParserNodeVisitor + - class: PHPStan\Parser\NewAssignedToPropertyVisitor tags: @@ -464,6 +358,11 @@ services: tags: - phpstan.parser.richParserNodeVisitor + - + class: PHPStan\Parser\LastConditionVisitor + tags: + - phpstan.parser.richParserNodeVisitor + - class: PhpParser\NodeVisitor\NodeConnectingVisitor @@ -497,14 +396,25 @@ services: - class: PHPStan\PhpDocParser\Parser\TypeParser + arguments: + quoteAwareConstExprString: %featureToggles.unescapeStrings% - class: PHPStan\PhpDocParser\Parser\ConstExprParser + factory: @PHPStan\PhpDoc\ConstExprParserFactory::create() - class: PHPStan\PhpDocParser\Parser\PhpDocParser arguments: requireWhitespaceBeforeDescription: %featureToggles.phpDocParserRequireWhitespaceBeforeDescription% + preserveTypeAliasesWithInvalidTypes: true + usedAttributes: + lines: %featureToggles.phpDocParserIncludeLines% + + - + class: PHPStan\PhpDoc\ConstExprParserFactory + arguments: + unescapeStrings: %featureToggles.unescapeStrings% - class: PHPStan\PhpDoc\PhpDocInheritanceResolver @@ -530,6 +440,8 @@ services: - class: PHPStan\PhpDoc\StubValidator + arguments: + duplicateStubs: %featureToggles.duplicateStubs% - class: PHPStan\PhpDoc\CountableStubFilesExtension @@ -538,6 +450,11 @@ services: tags: - phpstan.stubFilesExtension + - + class: PHPStan\PhpDoc\SocketSelectStubFilesExtension + tags: + - phpstan.stubFilesExtension + - class: PHPStan\PhpDoc\DefaultStubFilesProvider arguments: @@ -546,32 +463,55 @@ services: autowired: - PHPStan\PhpDoc\StubFilesProvider + - + class: PHPStan\PhpDoc\JsonValidateStubFilesExtension + tags: + - phpstan.stubFilesExtension + + - + class: PHPStan\PhpDoc\ReflectionEnumStubFilesExtension + tags: + - phpstan.stubFilesExtension + - class: PHPStan\Analyser\Analyser arguments: internalErrorsCountLimit: %internalErrorsCountLimit% + - + class: PHPStan\Analyser\AnalyserResultFinalizer + arguments: + reportUnmatchedIgnoredErrors: %reportUnmatchedIgnoredErrors% + - class: PHPStan\Analyser\FileAnalyser arguments: parser: @defaultAnalysisParser - reportUnmatchedIgnoredErrors: %reportUnmatchedIgnoredErrors% + + - + class: PHPStan\Analyser\LocalIgnoresProcessor - class: PHPStan\Analyser\RuleErrorTransformer - - class: PHPStan\Analyser\IgnoredErrorHelper + class: PHPStan\Analyser\Ignore\IgnoredErrorHelper arguments: ignoreErrors: %ignoreErrors% reportUnmatchedIgnoredErrors: %reportUnmatchedIgnoredErrors% - - class: PHPStan\Analyser\LazyScopeFactory + class: PHPStan\Analyser\Ignore\IgnoreLexer + + - + class: PHPStan\Analyser\LazyInternalScopeFactory arguments: scopeClass: %scopeClass% autowired: - - PHPStan\Analyser\ScopeFactory + - PHPStan\Analyser\InternalScopeFactory + + - + class: PHPStan\Analyser\ScopeFactory - class: PHPStan\Analyser\NodeScopeResolver @@ -583,6 +523,10 @@ services: earlyTerminatingMethodCalls: %earlyTerminatingMethodCalls% earlyTerminatingFunctionCalls: %earlyTerminatingFunctionCalls% implicitThrows: %exceptions.implicitThrows% + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + detectDeadTypeInMultiCatch: %featureToggles.detectDeadTypeInMultiCatch% + universalObjectCratesClasses: %universalObjectCratesClasses% + paramOutType: %featureToggles.paramOutType% - class: PHPStan\Analyser\ConstantResolver @@ -596,7 +540,6 @@ services: arguments: scanFileFinder: @fileFinderScan cacheFilePath: %resultCachePath% - tempResultCachePath: %tempResultCachePath% analysedPaths: %analysedPaths% composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% usedLevel: %usedLevel% @@ -610,7 +553,6 @@ services: class: PHPStan\Analyser\ResultCache\ResultCacheClearer arguments: cacheFilePath: %resultCachePath% - tempResultCachePath: %tempResultCachePath% - class: PHPStan\Cache\Cache @@ -637,8 +579,13 @@ services: arguments: analysedPaths: %analysedPaths% currentWorkingDirectory: %currentWorkingDirectory% - fixerTmpDir: %fixerTmpDir% - maximumNumberOfProcesses: %parallel.maximumNumberOfProcesses% + proTmpDir: %pro.tmpDir% + dnsServers: %pro.dnsServers% + composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% + allConfigFiles: %allConfigFiles% + cliAutoloadFile: %cliAutoloadFile% + bootstrapFiles: %bootstrapFiles% + editorUrl: %editorUrl% - class: PHPStan\Dependency\DependencyResolver @@ -677,8 +624,6 @@ services: usedLevel: %usedLevel% generateBaselineFile: %generateBaselineFile% cliAutoloadFile: %cliAutoloadFile% - singleReflectionFile: %singleReflectionFile% - singleReflectionInsteadOfFile: %singleReflectionInsteadOfFile% - class: PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider @@ -688,6 +633,10 @@ services: class: PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider factory: PHPStan\DependencyInjection\Type\LazyDynamicReturnTypeExtensionRegistryProvider + - + class: PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider + factory: PHPStan\DependencyInjection\Type\LazyExpressionTypeResolverExtensionRegistryProvider + - class: PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider factory: PHPStan\DependencyInjection\Type\LazyOperatorTypeSpecifyingExtensionRegistryProvider @@ -740,7 +689,7 @@ services: fileFinder: @fileFinderAnalyse - - class: PHPStan\NodeVisitor\StatementOrderVisitor + class: PHPStan\Parser\DeclarePositionVisitor tags: - phpstan.parser.richParserNodeVisitor @@ -771,6 +720,8 @@ services: - class: PHPStan\Reflection\InitializerExprTypeResolver + arguments: + usePathConstantsAsConstantString: %usePathConstantsAsConstantString% - class: PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension @@ -806,6 +757,12 @@ services: - class: PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository + - + class: PHPStan\Reflection\RequireExtension\RequireExtendsMethodsClassReflectionExtension + + - + class: PHPStan\Reflection\RequireExtension\RequireExtendsPropertiesClassReflectionExtension + - class: PHPStan\Reflection\Mixin\MixinMethodsClassReflectionExtension tags: @@ -825,7 +782,6 @@ services: arguments: parser: @defaultAnalysisParser inferPrivatePropertyTypeFromConstructor: %inferPrivatePropertyTypeFromConstructor% - universalObjectCratesClasses: %universalObjectCratesClasses% - implement: PHPStan\Reflection\Php\PhpMethodReflectionFactory @@ -837,6 +793,11 @@ services: tags: - phpstan.broker.methodsClassReflectionExtension + - + class: PHPStan\Reflection\Php\EnumAllowedSubTypesClassReflectionExtension + tags: + - phpstan.broker.allowedSubTypesClassReflectionExtension + - class: PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension tags: @@ -844,6 +805,22 @@ services: arguments: classes: %universalObjectCratesClasses% + - + class: PHPStan\Reflection\PHPStan\NativeReflectionEnumReturnDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + arguments: + className: PHPStan\Reflection\ClassReflection + methodName: getNativeReflection + + - + class: PHPStan\Reflection\PHPStan\NativeReflectionEnumReturnDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + arguments: + className: PHPStan\Reflection\Php\BuiltinMethodReflection + methodName: getDeclaringClass + - class: PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider factory: PHPStan\Reflection\ReflectionProvider\LazyReflectionProviderProvider @@ -858,6 +835,8 @@ services: - class: PHPStan\Reflection\SignatureMap\FunctionSignatureMapProvider + arguments: + stricterFunctionMap: %featureToggles.stricterFunctionMap% autowired: - PHPStan\Reflection\SignatureMap\FunctionSignatureMapProvider @@ -878,27 +857,45 @@ services: - class: PHPStan\Rules\AttributesCheck + arguments: + deprecationRulesInstalled: %deprecationRulesInstalled% - class: PHPStan\Rules\Arrays\NonexistentOffsetInArrayDimFetchCheck arguments: reportMaybes: %reportMaybes% + bleedingEdge: %featureToggles.bleedingEdge% + reportPossiblyNonexistentGeneralArrayOffset: %reportPossiblyNonexistentGeneralArrayOffset% + reportPossiblyNonexistentConstantArrayOffset: %reportPossiblyNonexistentConstantArrayOffset% + + - + class: PHPStan\Rules\ClassNameCheck - class: PHPStan\Rules\ClassCaseSensitivityCheck arguments: checkInternalClassCaseSensitivity: %checkInternalClassCaseSensitivity% + - + class: PHPStan\Rules\ClassForbiddenNameCheck + + - + class: PHPStan\Rules\Classes\LocalTypeAliasesCheck + arguments: + globalTypeAliases: %typeAliases% + - class: PHPStan\Rules\Comparison\ConstantConditionRuleHelper arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + looseComparisonRuleEnabled: %featureToggles.looseComparison% - class: PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper arguments: universalObjectCratesClasses: %universalObjectCratesClasses% treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + nullContextForVoidReturningFunctions: %featureToggles.nullContextForVoidReturningFunctions% - class: PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver @@ -967,6 +964,9 @@ services: - class: PHPStan\Rules\Generics\VarianceCheck + arguments: + checkParamOutVariance: %featureToggles.paramOutVariance% + strictStaticVariance: %featureToggles.strictStaticMethodTemplateTypeVariance% - class: PHPStan\Rules\IssetCheck @@ -993,9 +993,12 @@ services: arguments: reportMaybes: %reportMaybesInMethodSignatures% reportStatic: %reportStaticMethodSignatures% + abstractTraitMethod: %featureToggles.abstractTraitMethod% - class: PHPStan\Rules\Methods\MethodParameterComparisonHelper + arguments: + genericPrototypeMessage: %featureToggles.genericPrototypeMessage% - class: PHPStan\Rules\MissingTypehintCheck @@ -1012,12 +1015,30 @@ services: - class: PHPStan\Rules\Constants\LazyAlwaysUsedClassConstantsExtensionProvider + - + class: PHPStan\Rules\Methods\LazyAlwaysUsedMethodExtensionProvider + - class: PHPStan\Rules\PhpDoc\ConditionalReturnTypeRuleHelper + - + class: PHPStan\Rules\PhpDoc\AssertRuleHelper + - class: PHPStan\Rules\PhpDoc\UnresolvableTypeHelper + - + class: PHPStan\Rules\PhpDoc\GenericCallableRuleHelper + + - + class: PHPStan\Rules\PhpDoc\VarTagTypeRuleHelper + arguments: + checkTypeAgainstPhpDocType: %reportWrongPhpDocTypeInVarTag% + strictWideningCheck: %reportAnyTypeWideningInVarTag% + + - + class: PHPStan\Rules\Playground\NeverRuleHelper + - class: PHPStan\Rules\Properties\LazyReadWritePropertiesExtensionProvider @@ -1027,6 +1048,9 @@ services: - class: PHPStan\Rules\Properties\PropertyReflectionFinder + - + class: PHPStan\Rules\Pure\FunctionPurityCheck + - class: PHPStan\Rules\RuleLevelHelper arguments: @@ -1035,10 +1059,15 @@ services: checkUnionTypes: %checkUnionTypes% checkExplicitMixed: %checkExplicitMixed% checkImplicitMixed: %checkImplicitMixed% + newRuleLevelHelper: %featureToggles.newRuleLevelHelper% + checkBenevolentUnionTypes: %checkBenevolentUnionTypes% - class: PHPStan\Rules\UnusedFunctionParametersCheck + - + class: PHPStan\Rules\TooWideTypehints\TooWideParameterOutTypeCheck + - class: PHPStan\Type\FileTypeMapper arguments: @@ -1207,6 +1236,16 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\AssertThrowTypeExtension + tags: + - phpstan.dynamicFunctionThrowTypeExtension + + - + class: PHPStan\Type\Php\BackedEnumFromMethodDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - class: PHPStan\Type\Php\Base64DecodeDynamicFunctionReturnTypeExtension tags: @@ -1239,6 +1278,14 @@ services: arguments: checkMaybeUndefinedVariables: %checkMaybeUndefinedVariables% + - + class: PHPStan\Type\Php\ConstantFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ConstantHelper + - class: PHPStan\Type\Php\CountFunctionReturnTypeExtension tags: @@ -1259,6 +1306,9 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\DateFunctionReturnTypeHelper + - class: PHPStan\Type\Php\DateFormatFunctionReturnTypeExtension tags: @@ -1279,6 +1329,11 @@ services: tags: - phpstan.dynamicStaticMethodThrowTypeExtension + - + class: PHPStan\Type\Php\DateIntervalDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - class: PHPStan\Type\Php\DateTimeCreateDynamicReturnTypeExtension tags: @@ -1308,11 +1363,21 @@ services: tags: - phpstan.dynamicStaticMethodThrowTypeExtension + - + class: PHPStan\Type\Php\DateTimeZoneConstructorThrowTypeExtension + tags: + - phpstan.dynamicStaticMethodThrowTypeExtension + - class: PHPStan\Type\Php\DsMapDynamicReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Type\Php\DsMapDynamicMethodThrowTypeExtension + tags: + - phpstan.dynamicMethodThrowTypeExtension + - class: PHPStan\Type\Php\DioStatDynamicFunctionReturnTypeExtension tags: @@ -1323,11 +1388,24 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\FilterFunctionReturnTypeHelper + + - + class: PHPStan\Type\Php\FilterInputDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\FilterVarDynamicReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\FilterVarArrayDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\GetCalledClassDynamicReturnTypeExtension tags: @@ -1343,6 +1421,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\GettypeFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\GettimeofdayDynamicFunctionReturnTypeExtension tags: @@ -1357,6 +1440,11 @@ services: tags: - phpstan.dynamicFunctionThrowTypeExtension + - + class: PHPStan\Type\Php\IniGetReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\JsonThrowTypeExtension tags: @@ -1498,6 +1586,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\SetTypeFunctionTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - class: PHPStan\Type\Php\StrCaseFunctionsReturnTypeExtension tags: @@ -1508,6 +1601,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\StrIncrementDecrementFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\StrPadFunctionReturnTypeExtension tags: @@ -1578,6 +1676,11 @@ services: tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - + class: PHPStan\Type\Php\ClassImplementsFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\DefineConstantTypeSpecifyingExtension tags: @@ -1598,21 +1701,6 @@ services: tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - class: PHPStan\Type\Php\IsIntFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsFloatFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsNullFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\IsArrayFunctionTypeSpecifyingExtension tags: @@ -1620,36 +1708,16 @@ services: arguments: explicitMixed: %featureToggles.explicitMixedViaIsArray% - - - class: PHPStan\Type\Php\IsBoolFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\IsCallableFunctionTypeSpecifyingExtension tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - class: PHPStan\Type\Php\IsCountableFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsResourceFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\IsIterableFunctionTypeSpecifyingExtension tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - class: PHPStan\Type\Php\IsStringFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\IsSubclassOfFunctionTypeSpecifyingExtension tags: @@ -1660,21 +1728,6 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - - class: PHPStan\Type\Php\IsObjectFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsNumericFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - - - class: PHPStan\Type\Php\IsScalarFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\IsAFunctionTypeSpecifyingExtension tags: @@ -1683,11 +1736,6 @@ services: - class: PHPStan\Type\Php\IsAFunctionTypeSpecifyingHelper - - - class: PHPStan\Type\Php\ArrayIsListFunctionTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.functionTypeSpecifyingExtension - - class: PHPStan\Type\Php\CtypeDigitFunctionTypeSpecifyingExtension tags: @@ -1703,6 +1751,7 @@ services: arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% universalObjectCratesClasses: %universalObjectCratesClasses% + nullContextForVoidReturningFunctions: %featureToggles.nullContextForVoidReturningFunctions% tags: - phpstan.broker.dynamicFunctionReturnTypeExtension @@ -1797,6 +1846,9 @@ services: arguments: parser: @currentPhpVersionPhpParser + - + class: PHPStan\Type\Constant\OversizedArrayBuilder + exceptionTypeResolver: class: PHPStan\Rules\Exceptions\ExceptionTypeResolver factory: @PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver @@ -1849,6 +1901,7 @@ services: arguments: parser: @currentPhpVersionPhpParser lexer: @currentPhpVersionLexer + enableIgnoreErrorsWithinPhpDocs: %featureToggles.enableIgnoreErrorsWithinPhpDocs% autowired: no currentPhpVersionSimpleParser: @@ -1954,6 +2007,7 @@ services: class: PHPStan\Reflection\BetterReflection\BetterReflectionProvider arguments: reflector: @betterReflectionReflector + universalObjectCratesClasses: %universalObjectCratesClasses% autowired: false - @@ -1966,10 +2020,11 @@ services: analysedPaths: %analysedPaths% composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% analysedPathsFromConfig: %analysedPathsFromConfig% - singleReflectionFile: %singleReflectionFile% - implement: PHPStan\Reflection\BetterReflection\BetterReflectionProviderFactory + arguments: + universalObjectCratesClasses: %universalObjectCratesClasses% - class: PHPStan\Reflection\BetterReflection\SourceStubber\PhpStormStubsSourceStubberFactory @@ -2009,7 +2064,6 @@ services: currentPhpVersionRichParser: @currentPhpVersionRichParser currentPhpVersionSimpleParser: @currentPhpVersionSimpleParser php8Parser: @php8Parser - singleReflectionFile: %singleReflectionFile% autowired: false # Error formatters @@ -2028,6 +2082,7 @@ services: simpleRelativePathHelper: @simpleRelativePathHelper showTipsOfTheDay: %tipsOfTheDay% editorUrl: %editorUrl% + editorUrlTitle: %editorUrlTitle% errorFormatter.checkstyle: class: PHPStan\Command\ErrorFormatter\CheckstyleErrorFormatter diff --git a/conf/config.stubValidator.neon b/conf/config.stubValidator.neon index 6c4c8bde25..1645698a92 100644 --- a/conf/config.stubValidator.neon +++ b/conf/config.stubValidator.neon @@ -20,6 +20,7 @@ services: class: PHPStan\Reflection\BetterReflection\BetterReflectionProvider arguments: reflector: @stubReflector + universalObjectCratesClasses: %universalObjectCratesClasses% autowired: false stubReflector: diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon new file mode 100644 index 0000000000..1c621ce153 --- /dev/null +++ b/conf/parametersSchema.neon @@ -0,0 +1,236 @@ +parametersSchema: + bootstrapFiles: listOf(string()) + excludes_analyse: listOf(string()) + excludePaths: schema(anyOf( + structure([ + analyse: listOf(string()), + ]), + structure([ + analyseAndScan: listOf(string()), + ]) + structure([ + analyse: listOf(string()), + analyseAndScan: listOf(string()) + ]) + ), nullable()) + level: schema(anyOf(int(), string()), nullable()) + paths: listOf(string()) + exceptions: structure([ + implicitThrows: bool(), + reportUncheckedExceptionDeadCatch: bool(), + uncheckedExceptionRegexes: listOf(string()), + uncheckedExceptionClasses: listOf(string()), + checkedExceptionRegexes: listOf(string()), + checkedExceptionClasses: listOf(string()), + check: structure([ + missingCheckedExceptionInThrows: bool(), + tooWideThrowType: bool() + ]) + ]) + featureToggles: structure([ + bleedingEdge: bool(), + disableRuntimeReflectionProvider: bool(), + skipCheckGenericClasses: listOf(string()), + explicitMixedInUnknownGenericNew: bool(), + explicitMixedForGlobalVariables: bool(), + explicitMixedViaIsArray: bool(), + arrayFilter: bool(), + arrayUnpacking: bool(), + arrayValues: bool(), + nodeConnectingVisitorCompatibility: bool(), + nodeConnectingVisitorRule: bool(), + illegalConstructorMethodCall: bool(), + disableCheckMissingIterableValueType: bool(), + strictUnnecessaryNullsafePropertyFetch: bool(), + looseComparison: bool(), + consistentConstructor: bool() + checkUnresolvableParameterTypes: bool() + readOnlyByPhpDoc: bool() + phpDocParserRequireWhitespaceBeforeDescription: bool() + phpDocParserIncludeLines: bool() + enableIgnoreErrorsWithinPhpDocs: bool() + runtimeReflectionRules: bool() + notAnalysedTrait: bool() + curlSetOptTypes: bool() + listType: bool() + abstractTraitMethod: bool() + missingMagicSerializationRule: bool() + nullContextForVoidReturningFunctions: bool() + unescapeStrings: bool() + alwaysCheckTooWideReturnTypeFinalMethods: bool() + duplicateStubs: bool() + logicalXor: bool() + betterNoop: bool() + invarianceComposition: bool() + alwaysTrueAlwaysReported: bool() + disableUnreachableBranchesRules: bool() + varTagType: bool() + closureDefaultParameterTypeRule: bool() + newRuleLevelHelper: bool() + instanceofType: bool() + paramOutVariance: bool() + allInvalidPhpDocs: bool() + strictStaticMethodTemplateTypeVariance: bool() + propertyVariance: bool() + genericPrototypeMessage: bool() + stricterFunctionMap: bool() + invalidPhpDocTagLine: bool() + detectDeadTypeInMultiCatch: bool() + zeroFiles: bool() + projectServicesNotInAnalysedPaths: bool() + callUserFunc: bool() + finalByPhpDoc: bool() + magicConstantOutOfContext: bool() + paramOutType: bool() + pure: bool() + ]) + fileExtensions: listOf(string()) + checkAdvancedIsset: bool() + checkAlwaysTrueCheckTypeFunctionCall: bool() + checkAlwaysTrueInstanceof: bool() + checkAlwaysTrueStrictComparison: bool() + checkAlwaysTrueLooseComparison: bool() + reportAlwaysTrueInLastCondition: bool() + checkClassCaseSensitivity: bool() + checkExplicitMixed: bool() + checkImplicitMixed: bool() + checkFunctionArgumentTypes: bool() + checkFunctionNameCase: bool() + checkGenericClassInNonGenericObjectType: bool() + checkInternalClassCaseSensitivity: bool() + checkMissingIterableValueType: bool() + checkMissingCallableSignature: bool() + checkMissingVarTagTypehint: bool() + checkArgumentsPassedByReference: bool() + checkMaybeUndefinedVariables: bool() + checkNullables: bool() + checkThisOnly: bool() + checkUnionTypes: bool() + checkBenevolentUnionTypes: bool() + checkExplicitMixedMissingReturn: bool() + checkPhpDocMissingReturn: bool() + checkPhpDocMethodSignatures: bool() + checkExtraArguments: bool() + checkMissingTypehints: bool() + checkTooWideReturnTypesInProtectedAndPublicMethods: bool() + checkUninitializedProperties: bool() + checkDynamicProperties: bool() + deprecationRulesInstalled: bool() + inferPrivatePropertyTypeFromConstructor: bool() + + tipsOfTheDay: bool() + reportMaybes: bool() + reportMaybesInMethodSignatures: bool() + reportMaybesInPropertyPhpDocTypes: bool() + reportStaticMethodSignatures: bool() + reportWrongPhpDocTypeInVarTag: bool() + reportAnyTypeWideningInVarTag: bool() + reportPossiblyNonexistentGeneralArrayOffset: bool() + reportPossiblyNonexistentConstantArrayOffset: bool() + checkMissingOverrideMethodAttribute: bool() + parallel: structure([ + jobSize: int(), + processTimeout: float(), + maximumNumberOfProcesses: int(), + minimumNumberOfJobsPerProcess: int(), + buffer: int() + ]) + phpVersion: schema(anyOf(schema(int(), min(70100), max(80399))), nullable()) + polluteScopeWithLoopInitialAssignments: bool() + polluteScopeWithAlwaysIterableForeach: bool() + propertyAlwaysWrittenTags: listOf(string()) + propertyAlwaysReadTags: listOf(string()) + additionalConstructors: listOf(string()) + treatPhpDocTypesAsCertain: bool() + usePathConstantsAsConstantString: bool() + rememberPossiblyImpureFunctionValues: bool() + reportMagicMethods: bool() + reportMagicProperties: bool() + ignoreErrors: listOf( + anyOf( + string(), + structure([ + ?messages: listOf(string()) + ?identifier: string() + ?path: string() + ?reportUnmatched: bool() + ]), + structure([ + ?message: string() + ?identifier: string() + ?path: string() + ?reportUnmatched: bool() + ]), + structure([ + ?message: string() + count: int() + path: string() + ?identifier: string() + ?reportUnmatched: bool() + ]), + structure([ + ?message: string() + paths: listOf(string()) + ?identifier: string() + ?reportUnmatched: bool() + ]), + structure([ + ?messages: listOf(string()) + paths: listOf(string()) + ?identifier: string() + ?reportUnmatched: bool() + ]) + ) + ) + internalErrorsCountLimit: int() + cache: structure([ + nodesByFileCountMax: int() + nodesByStringCountMax: int() + ]) + reportUnmatchedIgnoredErrors: bool() + scopeClass: string() + typeAliases: arrayOf(string()) + universalObjectCratesClasses: listOf(string()) + stubFiles: listOf(string()) + earlyTerminatingMethodCalls: arrayOf(listOf(string())) + earlyTerminatingFunctionCalls: listOf(string()) + memoryLimitFile: string() + tempResultCachePath: string() + resultCachePath: string() + resultCacheChecksProjectExtensionFilesDependencies: bool() + staticReflectionClassNamePatterns: listOf(string()) + dynamicConstantNames: listOf(string()) + customRulesetUsed: schema(bool(), nullable()) + rootDir: string() + tmpDir: string() + currentWorkingDirectory: string() + cliArgumentsVariablesRegistered: bool() + mixinExcludeClasses: listOf(string()) + scanFiles: listOf(string()) + scanDirectories: listOf(string()) + fixerTmpDir: string() #unused + editorUrl: schema(string(), nullable()) + editorUrlTitle: schema(string(), nullable()) + errorFormat: schema(string(), nullable()) + pro: structure([ + dnsServers: schema(listOf(string()), min(1)), + tmpDir: string() + ]) + env: arrayOf(string(), anyOf(int(), string())) + sysGetTempDir: string() + + # irrelevant Nette parameters + debugMode: bool() + productionMode: bool() + tempDir: string() + __validate: bool() + + # internal parameters only for DerivativeContainerFactory + additionalConfigFiles: arrayOf(string()) + generateBaselineFile: schema(string(), nullable()) + analysedPaths: listOf(string()) + allConfigFiles: listOf(string()) + composerAutoloaderProjectPaths: listOf(string()) + analysedPathsFromConfig: listOf(string()) + usedLevel: string() + cliAutoloadFile: schema(string(), nullable()) diff --git a/e2e/baseline-uninit-prop-trait/src/Foo.php b/e2e/baseline-uninit-prop-trait/src/Foo.php new file mode 100644 index 0000000000..485fef5e1f --- /dev/null +++ b/e2e/baseline-uninit-prop-trait/src/Foo.php @@ -0,0 +1,18 @@ +x; + } + + public function init(): void + { + $this->x = rand(); + } +} diff --git a/e2e/baseline-uninit-prop-trait/src/HelloWorld.php b/e2e/baseline-uninit-prop-trait/src/HelloWorld.php new file mode 100644 index 0000000000..f7ad689a59 --- /dev/null +++ b/e2e/baseline-uninit-prop-trait/src/HelloWorld.php @@ -0,0 +1,14 @@ +init(); + $this->foo(); + } +} diff --git a/e2e/baseline-uninit-prop-trait/test-no-baseline.neon b/e2e/baseline-uninit-prop-trait/test-no-baseline.neon new file mode 100644 index 0000000000..3e639cd0d2 --- /dev/null +++ b/e2e/baseline-uninit-prop-trait/test-no-baseline.neon @@ -0,0 +1,4 @@ +parameters: + level: 9 + paths: + - src diff --git a/e2e/baseline-uninit-prop-trait/test.neon b/e2e/baseline-uninit-prop-trait/test.neon new file mode 100644 index 0000000000..9b21d7b642 --- /dev/null +++ b/e2e/baseline-uninit-prop-trait/test.neon @@ -0,0 +1,3 @@ +includes: + - test-baseline.neon + - test-no-baseline.neon diff --git a/e2e/bug-9622-trait/baseline-1.neon b/e2e/bug-9622-trait/baseline-1.neon new file mode 100644 index 0000000000..1548dbca10 --- /dev/null +++ b/e2e/bug-9622-trait/baseline-1.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: "#^Offset 'foo' does not exist on array\\{foo\\?\\: int\\}\\.$#" + count: 1 + path: src/UsesBar.php diff --git a/e2e/bug-9622-trait/patch-1.patch b/e2e/bug-9622-trait/patch-1.patch new file mode 100644 index 0000000000..cc2a6a622d --- /dev/null +++ b/e2e/bug-9622-trait/patch-1.patch @@ -0,0 +1,11 @@ +--- src/Foo.php 2023-07-13 09:01:37 ++++ src/Foo.php 2023-07-13 09:02:20 +@@ -3,7 +3,7 @@ + namespace Bug9622Trait; + + /** +- * @phpstan-type AnArray array{foo: int} ++ * @phpstan-type AnArray array{foo?: int} + */ + class Foo + { diff --git a/e2e/bug-9622-trait/phpstan-baseline.neon b/e2e/bug-9622-trait/phpstan-baseline.neon new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/bug-9622-trait/phpstan.neon b/e2e/bug-9622-trait/phpstan.neon new file mode 100644 index 0000000000..ddbf4c2114 --- /dev/null +++ b/e2e/bug-9622-trait/phpstan.neon @@ -0,0 +1,7 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 8 + paths: + - src diff --git a/e2e/bug-9622-trait/src/Bar.php b/e2e/bug-9622-trait/src/Bar.php new file mode 100644 index 0000000000..082a948bd5 --- /dev/null +++ b/e2e/bug-9622-trait/src/Bar.php @@ -0,0 +1,19 @@ + $query + * + * @return T + */ + public function handle(QueryInterface $query); +} \ No newline at end of file diff --git a/e2e/bug10449/src/Bus/QueryHandlerInterface.php b/e2e/bug10449/src/Bus/QueryHandlerInterface.php new file mode 100644 index 0000000000..88faa0164a --- /dev/null +++ b/e2e/bug10449/src/Bus/QueryHandlerInterface.php @@ -0,0 +1,9 @@ +queryBus->handle(new Query\ExampleQuery()); + $this->needsString($value); + return $value; + } + + private function needsString(string $s):void {} +} \ No newline at end of file diff --git a/e2e/bug10449/src/Query/ExampleQuery.php b/e2e/bug10449/src/Query/ExampleQuery.php new file mode 100644 index 0000000000..a5c7637793 --- /dev/null +++ b/e2e/bug10449/src/Query/ExampleQuery.php @@ -0,0 +1,15 @@ + + */ +final class ExampleQuery implements QueryInterface +{ +} \ No newline at end of file diff --git a/e2e/bug10449/src/Query/ExampleQueryHandler.php b/e2e/bug10449/src/Query/ExampleQueryHandler.php new file mode 100644 index 0000000000..e9bc1d59f0 --- /dev/null +++ b/e2e/bug10449/src/Query/ExampleQueryHandler.php @@ -0,0 +1,19 @@ + $query + * + * @return T + */ + public function handle(QueryInterface $query); +} \ No newline at end of file diff --git a/e2e/bug10449b/src/Bus/QueryHandlerInterface.php b/e2e/bug10449b/src/Bus/QueryHandlerInterface.php new file mode 100644 index 0000000000..88faa0164a --- /dev/null +++ b/e2e/bug10449b/src/Bus/QueryHandlerInterface.php @@ -0,0 +1,9 @@ +queryBus->handle($x); + $this->needsString($value); + return $value; + } + + return 'hello'; + } + + private function needsString(string $s):void {} +} \ No newline at end of file diff --git a/e2e/bug10449b/src/Query/ExampleQuery.php b/e2e/bug10449b/src/Query/ExampleQuery.php new file mode 100644 index 0000000000..a5c7637793 --- /dev/null +++ b/e2e/bug10449b/src/Query/ExampleQuery.php @@ -0,0 +1,15 @@ + + */ +final class ExampleQuery implements QueryInterface +{ +} \ No newline at end of file diff --git a/e2e/bug10449b/src/Query/ExampleQueryHandler.php b/e2e/bug10449b/src/Query/ExampleQueryHandler.php new file mode 100644 index 0000000000..e9bc1d59f0 --- /dev/null +++ b/e2e/bug10449b/src/Query/ExampleQueryHandler.php @@ -0,0 +1,19 @@ += 8.1 + +namespace PhpstanPhpUnit190; + +class FoobarTest +{ + public function testBaz(): int + { + $matcher = new self(); + $this->acceptCallback(static function (string $test) use ($matcher): string { + match ($matcher->testBaz()) { + 1 => 1, + 2 => 2, + default => new \LogicException() + }; + + return $test; + }); + + return 1; + } + + public function acceptCallback(callable $cb): void + { + + } +} diff --git a/e2e/result-cache-5/baseline-1.neon b/e2e/result-cache-5/baseline-1.neon new file mode 100644 index 0000000000..f0db0c2a00 --- /dev/null +++ b/e2e/result-cache-5/baseline-1.neon @@ -0,0 +1,11 @@ +parameters: + ignoreErrors: + - + message: "#^Expected type true, actual\\: false$#" + count: 1 + path: src/Foo.php + + - + message: "#^Instanceof between TestResultCache5\\\\Baz and Exception will always evaluate to false\\.$#" + count: 1 + path: src/Foo.php diff --git a/e2e/result-cache-5/patch-1.patch b/e2e/result-cache-5/patch-1.patch new file mode 100644 index 0000000000..69791cca29 --- /dev/null +++ b/e2e/result-cache-5/patch-1.patch @@ -0,0 +1,11 @@ +--- src/Baz.php 2022-10-24 14:28:45.000000000 +0200 ++++ src/Baz.php 2022-10-24 14:30:02.000000000 +0200 +@@ -2,7 +2,7 @@ + + namespace TestResultCache5; + +-class Baz extends \Exception ++class Baz extends \stdClass + { + + } diff --git a/e2e/result-cache-5/phpstan-baseline.neon b/e2e/result-cache-5/phpstan-baseline.neon new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/result-cache-5/phpstan.neon b/e2e/result-cache-5/phpstan.neon new file mode 100644 index 0000000000..ddbf4c2114 --- /dev/null +++ b/e2e/result-cache-5/phpstan.neon @@ -0,0 +1,7 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 8 + paths: + - src diff --git a/e2e/result-cache-5/src/Bar.php b/e2e/result-cache-5/src/Bar.php new file mode 100644 index 0000000000..6df1b7ee81 --- /dev/null +++ b/e2e/result-cache-5/src/Bar.php @@ -0,0 +1,16 @@ +doBar($var); + assertType('true', $var instanceof \Exception); + } + +} diff --git a/e2e/result-cache-6/baseline-1.neon b/e2e/result-cache-6/baseline-1.neon new file mode 100644 index 0000000000..a9ebd9fb61 --- /dev/null +++ b/e2e/result-cache-6/baseline-1.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: "#^Access to an undefined property TestResultCache6\\\\Bar\\:\\:\\$s\\.$#" + count: 1 + path: src/Foo.php diff --git a/e2e/result-cache-6/patch-1.patch b/e2e/result-cache-6/patch-1.patch new file mode 100644 index 0000000000..e2d7e84db0 --- /dev/null +++ b/e2e/result-cache-6/patch-1.patch @@ -0,0 +1,10 @@ +diff --git b/e2e/result-cache-6/src/Baz.php a/e2e/result-cache-6/src/Baz.php +index 4a94eb3ae..6fed0b9ec 100644 +--- b/e2e/result-cache-6/src/Baz.php ++++ a/e2e/result-cache-6/src/Baz.php +@@ -4,5 +4,4 @@ namespace TestResultCache6; + + class Baz + { +- public string $s; + } diff --git a/e2e/result-cache-6/phpstan-baseline.neon b/e2e/result-cache-6/phpstan-baseline.neon new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/result-cache-6/phpstan.neon b/e2e/result-cache-6/phpstan.neon new file mode 100644 index 0000000000..ddbf4c2114 --- /dev/null +++ b/e2e/result-cache-6/phpstan.neon @@ -0,0 +1,7 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 8 + paths: + - src diff --git a/e2e/result-cache-6/src/Bar.php b/e2e/result-cache-6/src/Bar.php new file mode 100644 index 0000000000..a9d6848bfe --- /dev/null +++ b/e2e/result-cache-6/src/Bar.php @@ -0,0 +1,10 @@ +s; + } + +} diff --git a/e2e/result-cache-7/baseline-1.neon b/e2e/result-cache-7/baseline-1.neon new file mode 100644 index 0000000000..6f1062520d --- /dev/null +++ b/e2e/result-cache-7/baseline-1.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: "#^PHPDoc tag @phpstan\\-require\\-implements cannot contain non\\-interface type TestResultCache7\\\\Bar\\.$#" + count: 1 + path: src/Foo.php diff --git a/e2e/result-cache-7/patch-1.patch b/e2e/result-cache-7/patch-1.patch new file mode 100644 index 0000000000..a381f8b428 --- /dev/null +++ b/e2e/result-cache-7/patch-1.patch @@ -0,0 +1,12 @@ +diff --git a/e2e/result-cache-7/src/Bar.php b/e2e/result-cache-7/src/Bar.php +index b698e695d..0bbcc3093 100644 +--- a/e2e/result-cache-7/src/Bar.php ++++ b/e2e/result-cache-7/src/Bar.php +@@ -2,6 +2,6 @@ + + namespace TestResultCache7; + +-interface Bar ++class Bar + { + } diff --git a/e2e/result-cache-7/phpstan-baseline.neon b/e2e/result-cache-7/phpstan-baseline.neon new file mode 100644 index 0000000000..e69de29bb2 diff --git a/e2e/result-cache-7/phpstan.neon b/e2e/result-cache-7/phpstan.neon new file mode 100644 index 0000000000..ddbf4c2114 --- /dev/null +++ b/e2e/result-cache-7/phpstan.neon @@ -0,0 +1,7 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 8 + paths: + - src diff --git a/e2e/result-cache-7/src/Bar.php b/e2e/result-cache-7/src/Bar.php new file mode 100644 index 0000000000..b698e695dd --- /dev/null +++ b/e2e/result-cache-7/src/Bar.php @@ -0,0 +1,7 @@ + + */ +class CustomRule implements Rule +{ + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return []; + } + +} diff --git a/e2e/result-cache-8/build/CustomRule2.php b/e2e/result-cache-8/build/CustomRule2.php new file mode 100644 index 0000000000..413d37b54d --- /dev/null +++ b/e2e/result-cache-8/build/CustomRule2.php @@ -0,0 +1,24 @@ + + */ +class CustomRule2 implements Rule +{ + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return []; + } + +} diff --git a/e2e/result-cache-8/composer.json b/e2e/result-cache-8/composer.json new file mode 100644 index 0000000000..d29ab86e75 --- /dev/null +++ b/e2e/result-cache-8/composer.json @@ -0,0 +1,14 @@ +{ + "require-dev": { + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-webmozart-assert": "^1.2" + }, + "autoload": { + "classmap": ["src"] + }, + "autoload-dev": { + "classmap": [ + "build" + ] + } +} diff --git a/e2e/result-cache-8/composer.lock b/e2e/result-cache-8/composer.lock new file mode 100644 index 0000000000..23a7f311ea --- /dev/null +++ b/e2e/result-cache-8/composer.lock @@ -0,0 +1,132 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "964b13a11680dbf7fa5291f0baa6d10c", + "packages": [], + "packages-dev": [ + { + "name": "phpstan/phpstan", + "version": "1.10.63", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "ad12836d9ca227301f5fb9960979574ed8628339" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ad12836d9ca227301f5fb9960979574ed8628339", + "reference": "ad12836d9ca227301f5fb9960979574ed8628339", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2024-03-18T16:53:53+00:00" + }, + { + "name": "phpstan/phpstan-webmozart-assert", + "version": "1.2.4", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-webmozart-assert.git", + "reference": "d1ff28697bd4e1c9ef5d3f871367ce9092871fec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-webmozart-assert/zipball/d1ff28697bd4e1c9ef5d3f871367ce9092871fec", + "reference": "d1ff28697bd4e1c9ef5d3f871367ce9092871fec", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.10" + }, + "require-dev": { + "nikic/php-parser": "^4.13.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "webmozart/assert": "^1.11.0" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan webmozart/assert extension", + "support": { + "issues": "https://github.com/phpstan/phpstan-webmozart-assert/issues", + "source": "https://github.com/phpstan/phpstan-webmozart-assert/tree/1.2.4" + }, + "time": "2023-02-21T20:34:19+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/e2e/result-cache-8/phpstan.neon b/e2e/result-cache-8/phpstan.neon new file mode 100644 index 0000000000..c7fd83c756 --- /dev/null +++ b/e2e/result-cache-8/phpstan.neon @@ -0,0 +1,17 @@ +includes: + - vendor/phpstan/phpstan-webmozart-assert/extension.neon + +parameters: + paths: + - src + level: 8 + +rules: + - ResultCache8E2E\CustomRule + - ResultCache8E2E\CustomRule3 + +services: + - + class: ResultCache8E2E\CustomRule2 + tags: + - phpstan.rules.rule diff --git a/e2e/result-cache-8/src/CustomRule3.php b/e2e/result-cache-8/src/CustomRule3.php new file mode 100644 index 0000000000..1f0ca326e1 --- /dev/null +++ b/e2e/result-cache-8/src/CustomRule3.php @@ -0,0 +1,24 @@ + + */ +class CustomRule3 implements Rule +{ + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return []; + } + +} diff --git a/e2e/result-cache-8/src/Foo.php b/e2e/result-cache-8/src/Foo.php new file mode 100644 index 0000000000..0082675fde --- /dev/null +++ b/e2e/result-cache-8/src/Foo.php @@ -0,0 +1,8 @@ +=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "Clue\\StreamFilter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "A simple and modern approach to stream filtering in PHP", + "homepage": "https://github.com/clue/stream-filter", + "keywords": [ + "bucket brigade", + "callback", + "filter", + "php_user_filter", + "stream", + "stream_filter_append", + "stream_filter_register" + ], + "support": { + "issues": "https://github.com/clue/stream-filter/issues", + "source": "https://github.com/clue/stream-filter/tree/v1.7.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2023-12-20T15:40:13+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "f41715465d65213d644d3141a6a93081be5d3549" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/f41715465d65213d644d3141a6a93081be5d3549", + "reference": "f41715465d65213d644d3141a6a93081be5d3549", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.2" + }, + "time": "2022-10-27T11:44:00+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:35:24+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:19:20+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.6.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:05:35+00:00" + }, + { + "name": "knplabs/github-api", + "version": "v3.14.1", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/php-github-api.git", + "reference": "71fec50e228737ec23c0b69801b85bf596fbdaca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/php-github-api/zipball/71fec50e228737ec23c0b69801b85bf596fbdaca", + "reference": "71fec50e228737ec23c0b69801b85bf596fbdaca", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.2.5 || ^8.0", + "php-http/cache-plugin": "^1.7.1|^2.0", + "php-http/client-common": "^2.3", + "php-http/discovery": "^1.12", + "php-http/httplug": "^2.2", + "php-http/multipart-stream-builder": "^1.1.2", + "psr/cache": "^1.0|^2.0|^3.0", + "psr/http-client-implementation": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/http-message": "^1.0|^2.0", + "symfony/deprecation-contracts": "^2.2|^3.0", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.2", + "guzzlehttp/psr7": "^1.7", + "http-interop/http-factory-guzzle": "^1.0", + "php-http/mock-client": "^1.4.1", + "phpstan/extension-installer": "^1.0.5", + "phpstan/phpstan": "^0.12.57", + "phpstan/phpstan-deprecation-rules": "^0.12.5", + "phpunit/phpunit": "^8.5 || ^9.4", + "symfony/cache": "^5.1.8", + "symfony/phpunit-bridge": "^5.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.20.x-dev", + "dev-master": "3.14-dev" + } + }, + "autoload": { + "psr-4": { + "Github\\": "lib/Github/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KnpLabs Team", + "homepage": "http://knplabs.com" + }, + { + "name": "Thibault Duplessis", + "email": "thibault.duplessis@gmail.com", + "homepage": "http://ornicar.github.com" + } + ], + "description": "GitHub API v3 client", + "homepage": "https://github.com/KnpLabs/php-github-api", + "keywords": [ + "api", + "gh", + "gist", + "github" + ], + "support": { + "issues": "https://github.com/KnpLabs/php-github-api/issues", + "source": "https://github.com/KnpLabs/php-github-api/tree/v3.14.1" + }, + "funding": [ + { + "url": "https://github.com/acrobat", + "type": "github" + } + ], + "time": "2024-03-24T18:21:15+00:00" + }, + { + "name": "league/commonmark", + "version": "2.4.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/91c24291965bd6d7c46c46a12ba7492f83b1cadf", + "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.30.3", + "commonmark/commonmark.js": "0.30.0", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 || ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 || ^7.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2024-02-02T11:59:32+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "nette/neon", + "version": "v3.4.1", + "source": { + "type": "git", + "url": "https://github.com/nette/neon.git", + "reference": "457bfbf0560f600b30d9df4233af382a478bb44d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/neon/zipball/457bfbf0560f600b30d9df4233af382a478bb44d", + "reference": "457bfbf0560f600b30d9df4233af382a478bb44d", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "8.0 - 8.3" + }, + "require-dev": { + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.7" + }, + "bin": [ + "bin/neon-lint" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🍸 Nette NEON: encodes and decodes NEON file format.", + "homepage": "https://ne-on.org", + "keywords": [ + "export", + "import", + "neon", + "nette", + "yaml" + ], + "support": { + "issues": "https://github.com/nette/neon/issues", + "source": "https://github.com/nette/neon/tree/v3.4.1" + }, + "time": "2023-09-27T08:59:11+00:00" + }, + { + "name": "nette/schema", + "version": "v1.2.5", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/0462f0166e823aad657c9224d0f849ecac1ba10a", + "reference": "0462f0166e823aad657c9224d0f849ecac1ba10a", + "shasum": "" + }, + "require": { + "nette/utils": "^2.5.7 || ^3.1.5 || ^4.0", + "php": "7.1 - 8.3" + }, + "require-dev": { + "nette/tester": "^2.3 || ^2.4", + "phpstan/phpstan-nette": "^1.0", + "tracy/tracy": "^2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.2.5" + }, + "time": "2023-10-05T20:37:59+00:00" + }, + { + "name": "nette/utils", + "version": "v3.2.10", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "a4175c62652f2300c8017fb7e640f9ccb11648d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/a4175c62652f2300c8017fb7e640f9ccb11648d2", + "reference": "a4175c62652f2300c8017fb7e640f9ccb11648d2", + "shasum": "" + }, + "require": { + "php": ">=7.2 <8.4" + }, + "conflict": { + "nette/di": "<3.0.6" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "~2.0", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.3" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()", + "ext-xml": "to use Strings::length() etc. when mbstring is not available" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v3.2.10" + }, + "time": "2023-07-30T15:38:18+00:00" + }, + { + "name": "php-http/cache-plugin", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/cache-plugin.git", + "reference": "539b2d1ea0dc1c2f141c8155f888197d4ac5635b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/cache-plugin/zipball/539b2d1ea0dc1c2f141c8155f888197d4ac5635b", + "reference": "539b2d1ea0dc1c2f141c8155f888197d4ac5635b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/client-common": "^1.9 || ^2.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "psr/http-factory-implementation": "^1.0", + "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "require-dev": { + "nyholm/psr7": "^1.6.1", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\Common\\Plugin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "PSR-6 Cache plugin for HTTPlug", + "homepage": "http://httplug.io", + "keywords": [ + "cache", + "http", + "httplug", + "plugin" + ], + "support": { + "issues": "https://github.com/php-http/cache-plugin/issues", + "source": "https://github.com/php-http/cache-plugin/tree/2.0.0" + }, + "time": "2024-02-19T17:02:14+00:00" + }, + { + "name": "php-http/client-common", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/client-common.git", + "reference": "1e19c059b0e4d5f717bf5d524d616165aeab0612" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/client-common/zipball/1e19c059b0e4d5f717bf5d524d616165aeab0612", + "reference": "1e19c059b0e4d5f717bf5d524d616165aeab0612", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/httplug": "^2.0", + "php-http/message": "^1.6", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0", + "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "doctrine/instantiator": "^1.1", + "guzzlehttp/psr7": "^1.4", + "nyholm/psr7": "^1.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "phpspec/prophecy": "^1.10.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" + }, + "suggest": { + "ext-json": "To detect JSON responses with the ContentTypePlugin", + "ext-libxml": "To detect XML responses with the ContentTypePlugin", + "php-http/cache-plugin": "PSR-6 Cache plugin", + "php-http/logger-plugin": "PSR-3 Logger plugin", + "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\Common\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Common HTTP Client implementations and tools for HTTPlug", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "common", + "http", + "httplug" + ], + "support": { + "issues": "https://github.com/php-http/client-common/issues", + "source": "https://github.com/php-http/client-common/tree/2.7.1" + }, + "time": "2023-11-30T10:31:25+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.19.4", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "0700efda8d7526335132360167315fdab3aeb599" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/0700efda8d7526335132360167315fdab3aeb599", + "reference": "0700efda8d7526335132360167315fdab3aeb599", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.19.4" + }, + "time": "2024-03-29T13:00:05+00:00" + }, + { + "name": "php-http/httplug", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "625ad742c360c8ac580fcc647a1541d29e257f67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/625ad742c360c8ac580fcc647a1541d29e257f67", + "reference": "625ad742c360c8ac580fcc647a1541d29e257f67", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http" + ], + "support": { + "issues": "https://github.com/php-http/httplug/issues", + "source": "https://github.com/php-http/httplug/tree/2.4.0" + }, + "time": "2023-04-14T15:10:03+00:00" + }, + { + "name": "php-http/message", + "version": "1.16.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/message.git", + "reference": "5997f3289332c699fa2545c427826272498a2088" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message/zipball/5997f3289332c699fa2545c427826272498a2088", + "reference": "5997f3289332c699fa2545c427826272498a2088", + "shasum": "" + }, + "require": { + "clue/stream-filter": "^1.5", + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.6", + "ext-zlib": "*", + "guzzlehttp/psr7": "^1.0 || ^2.0", + "laminas/laminas-diactoros": "^2.0 || ^3.0", + "php-http/message-factory": "^1.0.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "slim/slim": "^3.0" + }, + "suggest": { + "ext-zlib": "Used with compressor/decompressor streams", + "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", + "laminas/laminas-diactoros": "Used with Diactoros Factories", + "slim/slim": "Used with Slim Framework PSR-7 implementation" + }, + "type": "library", + "autoload": { + "files": [ + "src/filters.php" + ], + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "HTTP Message related tools", + "homepage": "http://php-http.org", + "keywords": [ + "http", + "message", + "psr-7" + ], + "support": { + "issues": "https://github.com/php-http/message/issues", + "source": "https://github.com/php-http/message/tree/1.16.1" + }, + "time": "2024-03-07T13:22:09+00:00" + }, + { + "name": "php-http/multipart-stream-builder", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/multipart-stream-builder.git", + "reference": "f5938fd135d9fa442cc297dc98481805acfe2b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/f5938fd135d9fa442cc297dc98481805acfe2b6a", + "reference": "f5938fd135d9fa442cc297dc98481805acfe2b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/discovery": "^1.15", + "psr/http-factory-implementation": "^1.0" + }, + "require-dev": { + "nyholm/psr7": "^1.0", + "php-http/message": "^1.5", + "php-http/message-factory": "^1.0.2", + "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Message\\MultipartStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "description": "A builder class that help you create a multipart stream", + "homepage": "http://php-http.org", + "keywords": [ + "factory", + "http", + "message", + "multipart stream", + "stream" + ], + "support": { + "issues": "https://github.com/php-http/multipart-stream-builder/issues", + "source": "https://github.com/php-http/multipart-stream-builder/tree/1.3.0" + }, + "time": "2023-04-28T14:10:22+00:00" + }, + { + "name": "php-http/promise", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3", + "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/php-http/promise/issues", + "source": "https://github.com/php-http/promise/tree/1.3.1" + }, + "time": "2024-03-15T13:55:21+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.11.x-dev", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "7a2e524c7bdc18295d62b0ed598cec1166da80ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7a2e524c7bdc18295d62b0ed598cec1166da80ab", + "reference": "7a2e524c7bdc18295d62b0ed598cec1166da80ab", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "default-branch": true, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2024-04-19T14:55:18+00:00" + }, + { + "name": "phpstan/phpstan-strict-rules", + "version": "1.6.x-dev", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "c7b4d283fbffd23b9405c01d1f68124739d698f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/c7b4d283fbffd23b9405c01d1f68124739d698f6", + "reference": "c7b4d283fbffd23b9405c01d1f68124739d698f6", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.11" + }, + "require-dev": { + "nikic/php-parser": "^4.13.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5" + }, + "default-branch": true, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extra strict and opinionated rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.6.x" + }, + "time": "2024-04-19T14:52:46+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "e616d01114759c4c489f93b099585439f795fe35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", + "reference": "e616d01114759c4c489f93b099585439f795fe35", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + }, + "time": "2023-04-10T20:10:41+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "a2708a5da5c87d1d0d52937bdeac625df659e11f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/a2708a5da5c87d1d0d52937bdeac625df659e11f", + "reference": "a2708a5da5c87d1d0d52937bdeac625df659e11f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-03-29T19:07:53+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "11d736e97f116ac375a81f96e662911a34cd50ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/11d736e97f116ac375a81f96e662911a34cd50ce", + "reference": "11d736e97f116ac375a81f96e662911a34cd50ce", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-10-31T17:30:12+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "700ff4096e346f54cb628ea650767c8130f1001f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/700ff4096e346f54cb628ea650767c8130f1001f", + "reference": "700ff4096e346f54cb628ea650767c8130f1001f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-08-08T10:20:21+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "11bbf19a0fb7b36345861e85c5768844c552906e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/11bbf19a0fb7b36345861e85c5768844c552906e", + "reference": "11bbf19a0fb7b36345861e85c5768844c552906e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.4.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-12-19T21:51:00+00:00" + }, + { + "name": "symfony/string", + "version": "v7.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/f5832521b998b0bec40bee688ad5de98d4cf111b", + "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-02-01T13:17:36+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2023-03-08T13:26:56+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.0.2", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" + }, + "time": "2024-03-05T20:51:40+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.31", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:37:42+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.19", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8", + "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.28", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.5", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.2", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2024-04-05T04:35:58+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:33:00+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:35:11+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": { + "phpstan/phpstan-strict-rules": 20 + }, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.3" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/issue-bot/console.php b/issue-bot/console.php new file mode 100755 index 0000000000..fd1dfa73a8 --- /dev/null +++ b/issue-bot/console.php @@ -0,0 +1,73 @@ +#!/usr/bin/env php +addPlugin($rateLimitPlugin); + $httpBuilder->addPlugin($requestCounter); + + $client = new Client($httpBuilder); + $client->authenticate($token, AuthMethod::ACCESS_TOKEN); + $rateLimitPlugin->setClient($client); + + $markdownEnvironment = new Environment(); + $markdownEnvironment->addExtension(new CommonMarkCoreExtension()); + $markdownEnvironment->addExtension(new GithubFlavoredMarkdownExtension()); + $botCommentParser = new BotCommentParser(new MarkdownParser($markdownEnvironment)); + $issueCommentDownloader = new IssueCommentDownloader($client, $botCommentParser); + + $issueCachePath = __DIR__ . '/tmp/issueCache.tmp'; + $playgroundCachePath = __DIR__ . '/tmp/playgroundCache.tmp'; + $tmpDir = __DIR__ . '/tmp'; + + exec('git branch --show-current', $gitBranchLines, $exitCode); + if ($exitCode === 0) { + $gitBranch = implode("\n", $gitBranchLines); + } else { + $gitBranch = 'dev-master'; + } + + $postGenerator = new PostGenerator(new Differ(new UnifiedDiffOutputBuilder(''))); + + $application = new Application(); + $application->add(new DownloadCommand($client, new PlaygroundClient(new \GuzzleHttp\Client()), $issueCommentDownloader, $issueCachePath, $playgroundCachePath)); + $application->add(new RunCommand($playgroundCachePath, $tmpDir)); + $application->add(new EvaluateCommand(new TabCreator(), $postGenerator, $client, $issueCommentDownloader, $issueCachePath, $playgroundCachePath, $tmpDir, $gitBranch, $phpstanSrcCommitBefore, $phpstanSrcCommitAfter)); + + $application->setCatchExceptions(false); + $application->run(); +})(); diff --git a/issue-bot/phpstan.neon b/issue-bot/phpstan.neon new file mode 100644 index 0000000000..7ff756707c --- /dev/null +++ b/issue-bot/phpstan.neon @@ -0,0 +1,13 @@ +includes: + - ../vendor/phpstan/phpstan-deprecation-rules/rules.neon + - ../vendor/phpstan/phpstan-phpunit/extension.neon + - ../vendor/phpstan/phpstan-phpunit/rules.neon + - ../vendor/phpstan/phpstan-strict-rules/rules.neon + - ../conf/bleedingEdge.neon + +parameters: + level: 8 + paths: + - src + - tests + - console.php diff --git a/issue-bot/phpunit.xml b/issue-bot/phpunit.xml new file mode 100644 index 0000000000..0946e9b886 --- /dev/null +++ b/issue-bot/phpunit.xml @@ -0,0 +1,28 @@ + + + + + src + + + + + + + + + tests + + + + diff --git a/issue-bot/playground.neon b/issue-bot/playground.neon new file mode 100644 index 0000000000..2f9743d575 --- /dev/null +++ b/issue-bot/playground.neon @@ -0,0 +1,5 @@ +rules: + - PHPStan\Rules\Playground\FunctionNeverRule + - PHPStan\Rules\Playground\MethodNeverRule + - PHPStan\Rules\Playground\NotAnalysedTraitRule + - PHPStan\Rules\Playground\NoPhpCodeRule diff --git a/issue-bot/src/.gitkeep b/issue-bot/src/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/issue-bot/src/Comment/BotComment.php b/issue-bot/src/Comment/BotComment.php new file mode 100644 index 0000000000..3fbc351a6e --- /dev/null +++ b/issue-bot/src/Comment/BotComment.php @@ -0,0 +1,32 @@ +resultHash = $playgroundExample->getHash(); + } + + public function getResultHash(): string + { + return $this->resultHash; + } + + public function getDiff(): string + { + return $this->diff; + } + +} diff --git a/issue-bot/src/Comment/BotCommentParser.php b/issue-bot/src/Comment/BotCommentParser.php new file mode 100644 index 0000000000..c09d09263d --- /dev/null +++ b/issue-bot/src/Comment/BotCommentParser.php @@ -0,0 +1,63 @@ +docParser->parse($text); + $walker = $document->walker(); + $hashes = []; + $diffs = []; + while ($event = $walker->next()) { + if (!$event->isEntering()) { + continue; + } + + $node = $event->getNode(); + if ($node instanceof Link) { + $url = $node->getUrl(); + $match = Strings::match($url, '/^https:\/\/phpstan\.org\/r\/([0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12})$/i'); + if ($match === null) { + continue; + } + + $hashes[] = $match[1]; + continue; + } + + if (!($node instanceof FencedCode)) { + continue; + } + + if ($node->getInfo() !== 'diff') { + continue; + } + + $diffs[] = $node->getLiteral(); + } + + if (count($hashes) !== 1) { + throw new BotCommentParserException(); + } + + if (count($diffs) !== 1) { + throw new BotCommentParserException(); + } + + return new BotCommentParserResult($hashes[0], $diffs[0]); + } + +} diff --git a/issue-bot/src/Comment/BotCommentParserException.php b/issue-bot/src/Comment/BotCommentParserException.php new file mode 100644 index 0000000000..8b4a7d7794 --- /dev/null +++ b/issue-bot/src/Comment/BotCommentParserException.php @@ -0,0 +1,10 @@ +hash; + } + + public function getDiff(): string + { + return $this->diff; + } + +} diff --git a/issue-bot/src/Comment/Comment.php b/issue-bot/src/Comment/Comment.php new file mode 100644 index 0000000000..e29701b08d --- /dev/null +++ b/issue-bot/src/Comment/Comment.php @@ -0,0 +1,39 @@ + $playgroundExamples + */ + public function __construct( + private string $author, + private string $text, + private array $playgroundExamples, + ) + { + } + + public function getAuthor(): string + { + return $this->author; + } + + public function getText(): string + { + return $this->text; + } + + /** + * @return non-empty-list + */ + public function getPlaygroundExamples(): array + { + return $this->playgroundExamples; + } + +} diff --git a/issue-bot/src/Comment/IssueCommentDownloader.php b/issue-bot/src/Comment/IssueCommentDownloader.php new file mode 100644 index 0000000000..f676390289 --- /dev/null +++ b/issue-bot/src/Comment/IssueCommentDownloader.php @@ -0,0 +1,92 @@ + + */ + public function getComments(int $issueNumber): array + { + $comments = []; + foreach ($this->downloadComments($issueNumber) as $issueComment) { + $commentExamples = $this->searchBody($issueComment['body']); + if (count($commentExamples) === 0) { + continue; + } + + if ($issueComment['user']['login'] === 'phpstan-bot') { + $parserResult = $this->botCommentParser->parse($issueComment['body']); + if (count($commentExamples) !== 1 || $commentExamples[0]->getHash() !== $parserResult->getHash()) { + throw new BotCommentParserException(); + } + + $comments[] = new BotComment($issueComment['body'], $commentExamples[0], $parserResult->getDiff()); + continue; + } + + $comments[] = new Comment($issueComment['user']['login'], $issueComment['body'], $commentExamples); + } + + return $comments; + } + + /** + * @return mixed[] + */ + private function downloadComments(int $issueNumber): array + { + $page = 1; + + /** @var Issue $api */ + $api = $this->githubClient->api('issue'); + + $comments = []; + while (true) { + $newComments = $api->comments()->all('phpstan', 'phpstan', $issueNumber, [ + 'page' => $page, + 'per_page' => 100, + ]); + $comments = array_merge($comments, $newComments); + if (count($newComments) < 100) { + break; + } + $page++; + } + + return $comments; + } + + /** + * @return list + */ + public function searchBody(string $text): array + { + $matches = Strings::matchAll($text, '/https:\/\/phpstan\.org\/r\/([0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12})/i'); + + $examples = []; + + foreach ($matches as [$url, $hash]) { + $examples[] = new PlaygroundExample($url, $hash); + } + + return $examples; + } + +} diff --git a/issue-bot/src/Console/DownloadCommand.php b/issue-bot/src/Console/DownloadCommand.php new file mode 100644 index 0000000000..2b702bd9a2 --- /dev/null +++ b/issue-bot/src/Console/DownloadCommand.php @@ -0,0 +1,255 @@ +setName('download'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $issues = $this->getIssues(); + + $playgroundCache = $this->loadPlaygroundCache(); + if ($playgroundCache === null) { + $cachedResults = []; + } else { + $cachedResults = $playgroundCache->getResults(); + } + + $unusedCachedResults = $cachedResults; + + $deduplicatedExamples = []; + foreach ($issues as $issue) { + foreach ($issue->getComments() as $comment) { + if ($comment instanceof BotComment) { + continue; + } + foreach ($comment->getPlaygroundExamples() as $example) { + $deduplicatedExamples[$example->getHash()] = $example; + } + } + } + + $hashes = array_keys($deduplicatedExamples); + foreach ($hashes as $hash) { + if (array_key_exists($hash, $cachedResults)) { + unset($unusedCachedResults[$hash]); + continue; + } + + $cachedResults[$hash] = $this->playgroundClient->getResult($hash); + } + + foreach (array_keys($unusedCachedResults) as $hash) { + unset($cachedResults[$hash]); + } + + $this->savePlaygroundCache(new PlaygroundCache($cachedResults)); + + $chunkSize = (int) ceil(count($hashes) / 20); + if ($chunkSize < 1) { + throw new Exception('Chunk size less than 1'); + } + + $matrix = []; + foreach ([70200, 70300, 70400, 80000, 80100, 80200, 80300] as $phpVersion) { + $phpVersionHashes = []; + foreach ($cachedResults as $hash => $result) { + $resultPhpVersions = array_keys($result->getVersionedErrors()); + if ($resultPhpVersions === [70400]) { + $resultPhpVersions = [70200, 70300, 70400, 80000]; + } + + if (!in_array(80100, $resultPhpVersions, true)) { + $resultPhpVersions[] = 80100; + } + if (!in_array(80200, $resultPhpVersions, true)) { + $resultPhpVersions[] = 80200; + } + if (!in_array(80300, $resultPhpVersions, true)) { + $resultPhpVersions[] = 80300; + } + + if (!in_array($phpVersion, $resultPhpVersions, true)) { + continue; + } + $phpVersionHashes[] = $hash; + } + $chunkSize = (int) ceil(count($phpVersionHashes) / 18); + if ($chunkSize < 1) { + throw new Exception('Chunk size less than 1'); + } + $chunks = array_chunk($phpVersionHashes, $chunkSize); + $i = 1; + foreach ($chunks as $chunk) { + $matrix[] = [ + 'phpVersion' => $phpVersion, + 'chunkNumber' => $i, + 'playgroundExamples' => implode(',', $chunk), + ]; + $i++; + } + } + + $output->writeln(Json::encode(['include' => $matrix])); + + return 0; + } + + /** + * @return Issue[] + */ + private function getIssues(): array + { + /** @var \Github\Api\Issue $api */ + $api = $this->githubClient->api('issue'); + + $cache = $this->loadIssueCache(); + $newDate = new DateTimeImmutable(); + + $issues = []; + foreach (['feature-request', 'bug'] as $label) { + $page = 1; + while (true) { + $parameters = [ + 'labels' => $label, + 'page' => $page, + 'per_page' => 100, + 'sort' => 'created', + 'direction' => 'desc', + ]; + if ($cache !== null) { + $parameters['state'] = 'all'; + $parameters['since'] = $cache->getDate()->format(DateTimeImmutable::ATOM); + } else { + $parameters['state'] = 'open'; + } + $newIssues = $api->all('phpstan', 'phpstan', $parameters); + $issues = array_merge($issues, $newIssues); + if (count($newIssues) < 100) { + break; + } + + $page++; + } + } + + $issueObjects = []; + if ($cache !== null) { + $issueObjects = $cache->getIssues(); + } + foreach ($issues as $issue) { + if ($issue['state'] === 'closed') { + unset($issueObjects[$issue['number']]); + continue; + } + $comments = []; + $issueExamples = $this->issueCommentDownloader->searchBody($issue['body']); + if (count($issueExamples) > 0) { + $comments[] = new Comment($issue['user']['login'], $issue['body'], $issueExamples); + } + + foreach ($this->issueCommentDownloader->getComments($issue['number']) as $issueComment) { + $comments[] = $issueComment; + } + + $issueObjects[(int) $issue['number']] = new Issue( + $issue['number'], + $comments, + ); + } + + $this->saveIssueCache(new IssueCache($newDate, $issueObjects)); + + return $issueObjects; + } + + private function loadIssueCache(): ?IssueCache + { + if (!is_file($this->issueCachePath)) { + return null; + } + + $contents = file_get_contents($this->issueCachePath); + if ($contents === false) { + throw new Exception('Read unsuccessful'); + } + + return unserialize($contents); + } + + private function saveIssueCache(IssueCache $cache): void + { + $result = file_put_contents($this->issueCachePath, serialize($cache)); + if ($result === false) { + throw new Exception('Write unsuccessful'); + } + } + + private function loadPlaygroundCache(): ?PlaygroundCache + { + if (!is_file($this->playgroundCachePath)) { + return null; + } + + $contents = file_get_contents($this->playgroundCachePath); + if ($contents === false) { + throw new Exception('Read unsuccessful'); + } + + return unserialize($contents); + } + + private function savePlaygroundCache(PlaygroundCache $cache): void + { + $result = file_put_contents($this->playgroundCachePath, serialize($cache)); + if ($result === false) { + throw new Exception('Write unsuccessful'); + } + } + +} diff --git a/issue-bot/src/Console/EvaluateCommand.php b/issue-bot/src/Console/EvaluateCommand.php new file mode 100644 index 0000000000..0f8d05a8d8 --- /dev/null +++ b/issue-bot/src/Console/EvaluateCommand.php @@ -0,0 +1,316 @@ +setName('evaluate'); + $this->addOption('post-comments', null, InputOption::VALUE_NONE); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $issueCache = $this->loadIssueCache(); + $originalResults = $this->loadPlaygroundCache()->getResults(); + $newResults = $this->loadResults(); + $toPost = []; + $totalCodeSnippets = 0; + + foreach ($issueCache->getIssues() as $issue) { + $botComments = []; + $deduplicatedExamples = []; + foreach ($issue->getComments() as $comment) { + if ($comment instanceof BotComment) { + $botComments[] = $comment; + continue; + } + foreach ($comment->getPlaygroundExamples() as $example) { + if (isset($deduplicatedExamples[$example->getHash()])) { + $deduplicatedExamples[$example->getHash()]['users'][] = $comment->getAuthor(); + $deduplicatedExamples[$example->getHash()]['users'] = array_values(array_unique($deduplicatedExamples[$example->getHash()]['users'])); + continue; + } + $deduplicatedExamples[$example->getHash()] = [ + 'example' => $example, + 'users' => [$comment->getAuthor()], + ]; + } + } + + $totalCodeSnippets += count($deduplicatedExamples); + foreach ($deduplicatedExamples as ['example' => $example, 'users' => $users]) { + $hash = $example->getHash(); + if (!array_key_exists($hash, $originalResults)) { + throw new Exception(sprintf('Hash %s does not exist in original results.', $hash)); + } + + $originalErrors = $originalResults[$hash]->getVersionedErrors(); + $originalTabs = $this->tabCreator->create($originalErrors); + + if (!array_key_exists($hash, $newResults)) { + throw new Exception(sprintf('Hash %s does not exist in new results.', $hash)); + } + + $originalPhpVersions = array_keys($originalErrors); + $newResult = $newResults[$hash]; + if (array_key_exists(70100, $originalErrors) || $originalPhpVersions === [70400]) { + $newResult[70100] = $newResult[70200]; + } + + $newTabs = $this->tabCreator->create($this->filterErrors($originalErrors, $newResult)); + $text = $this->postGenerator->createText($hash, $originalTabs, $newTabs, $botComments); + if ($text === null) { + continue; + } + + if ($this->isIssueClosed($issue->getNumber())) { + continue; + } + + $freshBotComments = $this->getFreshBotComments($issue->getNumber()); + $textAgain = $this->postGenerator->createText($hash, $originalTabs, $newTabs, $freshBotComments); + if ($textAgain === null) { + continue; + } + + $toPost[] = [ + 'issue' => $issue->getNumber(), + 'hash' => $hash, + 'users' => $users, + 'diff' => $text['diff'], + 'details' => $text['details'], + ]; + } + } + + if (count($toPost) === 0) { + $output->writeln(sprintf('No changes in results in %d code snippets from %d GitHub issues. :tada:', $totalCodeSnippets, count($issueCache->getIssues()))); + } + + foreach ($toPost as ['issue' => $issue, 'hash' => $hash, 'users' => $users, 'diff' => $diff, 'details' => $details]) { + $text = sprintf( + "Result of the [code snippet](https://phpstan.org/r/%s) from %s in [#%d](https://github.com/phpstan/phpstan/issues/%d) changed:\n\n```diff\n%s```", + $hash, + implode(' ', array_map(static fn (string $user): string => sprintf('@%s', $user), $users)), + $issue, + $issue, + $diff, + ); + if ($details !== null) { + $text .= "\n\n" . sprintf('
+ Full report + +%s +
', $details); + } + + $text .= "\n\n---\n"; + + $output->writeln($text); + } + + $postComments = (bool) $input->getOption('post-comments'); + if ($postComments) { + if (count($toPost) > 20) { + $output->writeln('Too many comments to post, something is probably wrong.'); + return 1; + } + foreach ($toPost as ['issue' => $issue, 'hash' => $hash, 'users' => $users, 'diff' => $diff, 'details' => $details]) { + $text = sprintf( + "%s After [the latest push in %s](https://github.com/phpstan/phpstan-src/compare/%s...%s), PHPStan now reports different result with your [code snippet](https://phpstan.org/r/%s):\n\n```diff\n%s```", + implode(' ', array_map(static fn (string $user): string => sprintf('@%s', $user), $users)), + $this->gitBranch, + $this->phpstanSrcCommitBefore, + $this->phpstanSrcCommitAfter, + $hash, + $diff, + ); + if ($details !== null) { + $text .= "\n\n" . sprintf('
+ Full report + +%s +
', $details); + } + + /** @var GitHubIssueApi $issueApi */ + $issueApi = $this->githubClient->api('issue'); + $issueApi->comments()->create('phpstan', 'phpstan', $issue, [ + 'body' => $text, + ]); + } + } + + return 0; + } + + /** + * @return list + */ + private function getFreshBotComments(int $issueNumber): array + { + $comments = []; + foreach ($this->issueCommentDownloader->getComments($issueNumber) as $issueComment) { + if (!$issueComment instanceof BotComment) { + continue; + } + + $comments[] = $issueComment; + } + + return $comments; + } + + private function isIssueClosed(int $issueNumber): bool + { + /** @var GitHubIssueApi $issueApi */ + $issueApi = $this->githubClient->api('issue'); + $issue = $issueApi->show('phpstan', 'phpstan', $issueNumber); + + return $issue['state'] === 'closed'; + } + + private function loadIssueCache(): IssueCache + { + if (!is_file($this->issueCachePath)) { + throw new Exception('Issue cache must exist'); + } + + $contents = file_get_contents($this->issueCachePath); + if ($contents === false) { + throw new Exception('Read unsuccessful'); + } + + return unserialize($contents); + } + + private function loadPlaygroundCache(): PlaygroundCache + { + if (!is_file($this->playgroundCachePath)) { + throw new Exception('Playground cache must exist'); + } + + $contents = file_get_contents($this->playgroundCachePath); + if ($contents === false) { + throw new Exception('Read unsuccessful'); + } + + return unserialize($contents); + } + + /** + * @return array>> + */ + private function loadResults(): array + { + $finder = new Finder(); + $tmpResults = []; + foreach ($finder->files()->name('results-*.tmp')->in($this->tmpDir) as $resultFile) { + $contents = file_get_contents($resultFile->getPathname()); + if ($contents === false) { + throw new Exception('Result read unsuccessful'); + } + $result = unserialize($contents); + $phpVersion = (int) $result['phpVersion']; + foreach ($result['errors'] as $hash => $errors) { + $tmpResults[(string) $hash][$phpVersion] = array_values($errors); + } + } + + return $tmpResults; + } + + /** + * @param array> $originalErrors + * @param array> $newErrors + * @return array> + */ + private function filterErrors( + array $originalErrors, + array $newErrors, + ): array + { + $originalPhpVersions = array_keys($originalErrors); + $filteredNewErrors = []; + foreach ($newErrors as $phpVersion => $errors) { + if (!in_array($phpVersion, $originalPhpVersions, true)) { + continue; + } + + $filteredNewErrors[$phpVersion] = $errors; + } + + $newTabs = $this->tabCreator->create($newErrors); + $filteredNewTabs = $this->tabCreator->create($filteredNewErrors); + if (count($newTabs) !== count($filteredNewTabs)) { + return $newErrors; + } + + $firstFilteredNewTab = $filteredNewTabs[0]; + $firstNewTab = $newTabs[0]; + + if (count($firstFilteredNewTab->getErrors()) !== count($firstNewTab->getErrors())) { + return $newErrors; + } + + foreach ($firstFilteredNewTab->getErrors() as $i => $error) { + $otherError = $firstNewTab->getErrors()[$i]; + if ($error->getLine() !== $otherError->getLine()) { + return $newErrors; + } + if ($error->getMessage() !== $otherError->getMessage()) { + return $newErrors; + } + } + + return $filteredNewErrors; + } + +} diff --git a/issue-bot/src/Console/RunCommand.php b/issue-bot/src/Console/RunCommand.php new file mode 100644 index 0000000000..763db57cd0 --- /dev/null +++ b/issue-bot/src/Console/RunCommand.php @@ -0,0 +1,150 @@ +setName('run'); + $this->addArgument('phpVersion', InputArgument::REQUIRED); + $this->addArgument('playgroundHashes', InputArgument::REQUIRED); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $phpVersion = (int) $input->getArgument('phpVersion'); + $commaSeparatedPlaygroundHashes = $input->getArgument('playgroundHashes'); + $playgroundHashes = explode(',', $commaSeparatedPlaygroundHashes); + $playgroundCache = $this->loadPlaygroundCache(); + $errors = []; + foreach ($playgroundHashes as $hash) { + if (!array_key_exists($hash, $playgroundCache->getResults())) { + throw new Exception(sprintf('Hash %s must exist', $hash)); + } + $errors[$hash] = $this->analyseHash($output, $phpVersion, $playgroundCache->getResults()[$hash]); + } + + $data = ['phpVersion' => $phpVersion, 'errors' => $errors]; + + $writeSuccess = file_put_contents(sprintf($this->tmpDir . '/results-%d-%s.tmp', $phpVersion, sha1($commaSeparatedPlaygroundHashes)), serialize($data)); + if ($writeSuccess === false) { + throw new Exception('Result write unsuccessful'); + } + + return 0; + } + + /** + * @return list + */ + private function analyseHash(OutputInterface $output, int $phpVersion, PlaygroundResult $result): array + { + $configFiles = [ + __DIR__ . '/../../playground.neon', + ]; + if ($result->isBleedingEdge()) { + $configFiles[] = __DIR__ . '/../../../conf/bleedingEdge.neon'; + } + if ($result->isStrictRules()) { + $configFiles[] = __DIR__ . '/../../vendor/phpstan/phpstan-strict-rules/rules.neon'; + } + $neon = Neon::encode([ + 'includes' => $configFiles, + 'parameters' => [ + 'level' => $result->getLevel(), + 'inferPrivatePropertyTypeFromConstructor' => true, + 'treatPhpDocTypesAsCertain' => $result->isTreatPhpDocTypesAsCertain(), + 'phpVersion' => $phpVersion, + ], + ]); + + $hash = $result->getHash(); + $neonPath = sprintf($this->tmpDir . '/%s.neon', $hash); + $codePath = sprintf($this->tmpDir . '/%s.php', $hash); + file_put_contents($neonPath, $neon); + file_put_contents($codePath, $result->getCode()); + + $commandArray = [ + __DIR__ . '/../../../bin/phpstan', + 'analyse', + '--error-format', + 'json', + '--no-progress', + '-c', + $neonPath, + $codePath, + ]; + + $output->writeln(sprintf('Starting analysis of %s', $hash)); + + $startTime = microtime(true); + exec(implode(' ', $commandArray), $outputLines, $exitCode); + $elapsedTime = microtime(true) - $startTime; + $output->writeln(sprintf('Analysis of %s took %.2f s', $hash, $elapsedTime)); + + if ($exitCode !== 0 && $exitCode !== 1) { + throw new Exception(sprintf('PHPStan exited with code %d during analysis of %s', $exitCode, $hash)); + } + + $json = Json::decode(implode("\n", $outputLines), Json::FORCE_ARRAY); + $errors = []; + foreach ($json['files'] as ['messages' => $messages]) { + foreach ($messages as $message) { + $messageText = str_replace(sprintf('/%s.php', $hash), '/tmp.php', $message['message']); + if (strpos($messageText, 'Internal error') !== false) { + throw new Exception(sprintf('While analysing %s: %s', $hash, $messageText)); + } + $errors[] = new PlaygroundError($message['line'] ?? -1, $messageText, $message['identifier'] ?? null); + } + } + + return $errors; + } + + private function loadPlaygroundCache(): PlaygroundCache + { + if (!is_file($this->playgroundCachePath)) { + throw new Exception('Playground cache must exist'); + } + + $contents = file_get_contents($this->playgroundCachePath); + if ($contents === false) { + throw new Exception('Read unsuccessful'); + } + + return unserialize($contents); + } + +} diff --git a/issue-bot/src/GitHub/RateLimitPlugin.php b/issue-bot/src/GitHub/RateLimitPlugin.php new file mode 100644 index 0000000000..ce0bbf8caa --- /dev/null +++ b/issue-bot/src/GitHub/RateLimitPlugin.php @@ -0,0 +1,47 @@ +client = $client; + } + + public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise + { + $path = $request->getUri()->getPath(); + if ($path === '/rate_limit') { + return $next($request); + } + + /** @var RateLimit $api */ + $api = $this->client->api('rate_limit'); + + /** @var RateLimitResource $resource */ + $resource = $api->getResource('core'); + if ($resource->getRemaining() < 10) { + $reset = $resource->getReset(); + $sleepFor = $reset - time(); + if ($sleepFor > 0) { + sleep($sleepFor); + } + } + + return $next($request); + } + +} diff --git a/issue-bot/src/GitHub/RequestCounterPlugin.php b/issue-bot/src/GitHub/RequestCounterPlugin.php new file mode 100644 index 0000000000..042cb53202 --- /dev/null +++ b/issue-bot/src/GitHub/RequestCounterPlugin.php @@ -0,0 +1,30 @@ +getUri()->getPath(); + if ($path === '/rate_limit') { + return $next($request); + } + + $this->totalCount++; + return $next($request); + } + + public function getTotalCount(): int + { + return $this->totalCount; + } + +} diff --git a/issue-bot/src/Issue/Issue.php b/issue-bot/src/Issue/Issue.php new file mode 100644 index 0000000000..a9feaf01d8 --- /dev/null +++ b/issue-bot/src/Issue/Issue.php @@ -0,0 +1,30 @@ +number; + } + + /** + * @return Comment[] + */ + public function getComments(): array + { + return $this->comments; + } + +} diff --git a/issue-bot/src/Issue/IssueCache.php b/issue-bot/src/Issue/IssueCache.php new file mode 100644 index 0000000000..6b5ed53fec --- /dev/null +++ b/issue-bot/src/Issue/IssueCache.php @@ -0,0 +1,30 @@ + $issues + */ + public function __construct(private DateTimeImmutable $date, private array $issues) + { + } + + public function getDate(): DateTimeImmutable + { + return $this->date; + } + + /** + * @return array + */ + public function getIssues(): array + { + return $this->issues; + } + +} diff --git a/issue-bot/src/Playground/PlaygroundCache.php b/issue-bot/src/Playground/PlaygroundCache.php new file mode 100644 index 0000000000..cda495a831 --- /dev/null +++ b/issue-bot/src/Playground/PlaygroundCache.php @@ -0,0 +1,23 @@ + $results + */ + public function __construct(private array $results) + { + } + + /** + * @return array + */ + public function getResults(): array + { + return $this->results; + } + +} diff --git a/issue-bot/src/Playground/PlaygroundClient.php b/issue-bot/src/Playground/PlaygroundClient.php new file mode 100644 index 0000000000..43cd6ea9d3 --- /dev/null +++ b/issue-bot/src/Playground/PlaygroundClient.php @@ -0,0 +1,42 @@ +client->get(sprintf('https://api.phpstan.org/sample?id=%s', $hash)); + + $body = (string) $response->getBody(); + $json = Json::decode($body, Json::FORCE_ARRAY); + + $versionedErrors = []; + foreach ($json['versionedErrors'] as ['phpVersion' => $phpVersion, 'errors' => $errors]) { + $versionedErrors[(int) $phpVersion] = array_map(static fn (array $error) => new PlaygroundError($error['line'] ?? -1, $error['message'], $error['identifier'] ?? null), array_values($errors)); + } + + return new PlaygroundResult( + sprintf('https://phpstan.org/r/%s', $hash), + $hash, + $json['code'], + $json['level'], + $json['config']['strictRules'], + $json['config']['bleedingEdge'], + $json['config']['treatPhpDocTypesAsCertain'], + $versionedErrors, + ); + } + +} diff --git a/issue-bot/src/Playground/PlaygroundError.php b/issue-bot/src/Playground/PlaygroundError.php new file mode 100644 index 0000000000..1e55ac88b3 --- /dev/null +++ b/issue-bot/src/Playground/PlaygroundError.php @@ -0,0 +1,27 @@ +line; + } + + public function getMessage(): string + { + return $this->message; + } + + public function getIdentifier(): ?string + { + return $this->identifier; + } + +} diff --git a/issue-bot/src/Playground/PlaygroundExample.php b/issue-bot/src/Playground/PlaygroundExample.php new file mode 100644 index 0000000000..0d25f7bfbb --- /dev/null +++ b/issue-bot/src/Playground/PlaygroundExample.php @@ -0,0 +1,25 @@ +url; + } + + public function getHash(): string + { + return $this->hash; + } + +} diff --git a/issue-bot/src/Playground/PlaygroundResult.php b/issue-bot/src/Playground/PlaygroundResult.php new file mode 100644 index 0000000000..faddc6c078 --- /dev/null +++ b/issue-bot/src/Playground/PlaygroundResult.php @@ -0,0 +1,67 @@ +> $versionedErrors + */ + public function __construct( + private string $url, + private string $hash, + private string $code, + private string $level, + private bool $strictRules, + private bool $bleedingEdge, + private bool $treatPhpDocTypesAsCertain, + private array $versionedErrors, + ) + { + } + + public function getUrl(): string + { + return $this->url; + } + + public function getHash(): string + { + return $this->hash; + } + + public function getCode(): string + { + return $this->code; + } + + public function getLevel(): string + { + return $this->level; + } + + public function isStrictRules(): bool + { + return $this->strictRules; + } + + public function isBleedingEdge(): bool + { + return $this->bleedingEdge; + } + + public function isTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; + } + + /** + * @return array> + */ + public function getVersionedErrors(): array + { + return $this->versionedErrors; + } + +} diff --git a/issue-bot/src/Playground/PlaygroundResultTab.php b/issue-bot/src/Playground/PlaygroundResultTab.php new file mode 100644 index 0000000000..03d33ac7d4 --- /dev/null +++ b/issue-bot/src/Playground/PlaygroundResultTab.php @@ -0,0 +1,28 @@ + $errors + */ + public function __construct(private string $title, private array $errors) + { + } + + public function getTitle(): string + { + return $this->title; + } + + /** + * @return list + */ + public function getErrors(): array + { + return $this->errors; + } + +} diff --git a/issue-bot/src/Playground/TabCreator.php b/issue-bot/src/Playground/TabCreator.php new file mode 100644 index 0000000000..544fcb1461 --- /dev/null +++ b/issue-bot/src/Playground/TabCreator.php @@ -0,0 +1,127 @@ +> $versionedErrors + * @return list + */ + public function create(array $versionedErrors): array + { + ksort($versionedErrors, SORT_NUMERIC); + + $versions = []; + $last = null; + + foreach ($versionedErrors as $phpVersion => $errors) { + $errors = array_map(static function (PlaygroundError $error): PlaygroundError { + if ($error->getIdentifier() === null) { + return $error; + } + + if (!str_starts_with($error->getIdentifier(), 'phpstanPlayground.')) { + return $error; + } + + return new PlaygroundError( + $error->getLine(), + sprintf('Tip: %s', $error->getMessage()), + $error->getIdentifier(), + ); + }, $errors); + $current = [ + 'versions' => [$phpVersion], + 'errors' => $errors, + ]; + if ($last === null) { + $last = $current; + continue; + } + + if (count($errors) !== count($last['errors'])) { + $versions[] = $last; + $last = $current; + continue; + } + + $merge = true; + foreach ($errors as $i => $error) { + $lastError = $last['errors'][$i]; + if ($error->getLine() !== $lastError->getLine()) { + $versions[] = $last; + $last = $current; + $merge = false; + break; + } + if ($error->getMessage() !== $lastError->getMessage()) { + $versions[] = $last; + $last = $current; + $merge = false; + break; + } + } + + if (!$merge) { + continue; + } + + $last['versions'][] = $phpVersion; + } + + if ($last !== null) { + $versions[] = $last; + } + + usort($versions, static function ($a, $b): int { + $aVersion = $a['versions'][count($a['versions']) - 1]; + $bVersion = $b['versions'][count($b['versions']) - 1]; + + return $bVersion - $aVersion; + }); + + $tabs = []; + + foreach ($versions as $version) { + $title = 'PHP '; + if (count($version['versions']) > 1) { + $title .= $this->versionNumberToString($version['versions'][0]); + $title .= ' – '; + $title .= $this->versionNumberToString($version['versions'][count($version['versions']) - 1]); + } else { + $title .= $this->versionNumberToString($version['versions'][0]); + } + + if (count($version['errors']) === 1) { + $title .= ' (1 error)'; + } elseif (count($version['errors']) > 0) { + $title .= ' (' . count($version['errors']) . ' errors)'; + } + + $tabs[] = new PlaygroundResultTab($title, $version['errors']); + } + + return $tabs; + } + + private function versionNumberToString(int $versionId): string + { + $first = (int) floor($versionId / 10000); + $second = (int) floor(($versionId % 10000) / 100); + $third = (int) floor($versionId % 100); + + return $first . '.' . $second . ($third !== 0 ? '.' . $third : ''); + } + +} diff --git a/issue-bot/src/PostGenerator.php b/issue-bot/src/PostGenerator.php new file mode 100644 index 0000000000..db62d448a8 --- /dev/null +++ b/issue-bot/src/PostGenerator.php @@ -0,0 +1,138 @@ + $originalTabs + * @param list $currentTabs + * @param BotComment[] $botComments + * @return array{diff: string, details: string|null}|null + */ + public function createText( + string $hash, + array $originalTabs, + array $currentTabs, + array $botComments, + ): ?array + { + foreach ($currentTabs as $tab) { + foreach ($tab->getErrors() as $error) { + if (strpos($error->getMessage(), 'Internal error') === false) { + continue; + } + + return null; + } + } + + $maxDigit = 1; + foreach (array_merge($originalTabs, $currentTabs) as $tab) { + foreach ($tab->getErrors() as $error) { + $length = strlen((string) $error->getLine()); + if ($length <= $maxDigit) { + continue; + } + + $maxDigit = $length; + } + } + $originalErrorsText = $this->generateTextFromTabs($originalTabs, $maxDigit); + $currentErrorsText = $this->generateTextFromTabs($currentTabs, $maxDigit); + if ($originalErrorsText === $currentErrorsText) { + return null; + } + + $diff = $this->differ->diff($originalErrorsText, $currentErrorsText); + foreach ($botComments as $botComment) { + if ($botComment->getResultHash() !== $hash) { + continue; + } + + if ($botComment->getDiff() === $diff) { + return null; + } + } + + if (count($currentTabs) === 1 && count($currentTabs[0]->getErrors()) === 0) { + return ['diff' => $diff, 'details' => null]; + } + + $details = []; + foreach ($currentTabs as $tab) { + $detail = ''; + if (count($currentTabs) > 1) { + $detail .= sprintf("%s\n-----------\n\n", $tab->getTitle()); + } + + if (count($tab->getErrors()) === 0) { + $detail .= "No errors\n"; + $details[] = $detail; + continue; + } + + $detail .= "| Line | Error |\n"; + $detail .= "|---|---|\n"; + + foreach ($tab->getErrors() as $error) { + $errorText = Strings::replace($error->getMessage(), "/\r|\n/", ''); + $detail .= sprintf("| %d | `%s` |\n", $error->getLine(), $errorText); + } + + $details[] = $detail; + } + + return ['diff' => $diff, 'details' => implode("\n", $details)]; + } + + /** + * @param PlaygroundResultTab[] $tabs + */ + private function generateTextFromTabs(array $tabs, int $maxDigit): string + { + $parts = []; + foreach ($tabs as $tab) { + $text = ''; + if (count($tabs) > 1) { + $text .= sprintf("%s\n==========\n\n", $tab->getTitle()); + } + + if (count($tab->getErrors()) === 0) { + $text .= 'No errors'; + $parts[] = $text; + continue; + } + + $errorLines = []; + foreach ($tab->getErrors() as $error) { + $errorLines[] = sprintf('%s: %s', str_pad((string) $error->getLine(), $maxDigit, ' ', STR_PAD_LEFT), $error->getMessage()); + } + + $text .= implode("\n", $errorLines); + + $parts[] = $text; + } + + return implode("\n\n", $parts); + } + +} diff --git a/issue-bot/tests/Comment/BotCommentParserResultTest.php b/issue-bot/tests/Comment/BotCommentParserResultTest.php new file mode 100644 index 0000000000..123c8034f1 --- /dev/null +++ b/issue-bot/tests/Comment/BotCommentParserResultTest.php @@ -0,0 +1,49 @@ + + */ + public function dataParse(): iterable + { + yield [ + '@foobar After [the latest commit to dev-master](https://github.com/phpstan/phpstan-src/commit/abc123), PHPStan now reports different result with your [code snippet](https://phpstan.org/r/74c3b0af-5a87-47e7-907a-9ea6fbb1c396): + +```diff +@@ @@ +-1: abc ++1: def +```', + '74c3b0af-5a87-47e7-907a-9ea6fbb1c396', + '@@ @@ +-1: abc ++1: def +', + ]; + } + + /** + * @dataProvider dataParse + */ + public function testParse(string $text, string $expectedHash, string $expectedDiff): void + { + $markdownEnvironment = new Environment(); + $markdownEnvironment->addExtension(new CommonMarkCoreExtension()); + $markdownEnvironment->addExtension(new GithubFlavoredMarkdownExtension()); + $parser = new BotCommentParser(new MarkdownParser($markdownEnvironment)); + $result = $parser->parse($text); + self::assertSame($expectedHash, $result->getHash()); + self::assertSame($expectedDiff, $result->getDiff()); + } + +} diff --git a/issue-bot/tests/Playground/TabCreatorTest.php b/issue-bot/tests/Playground/TabCreatorTest.php new file mode 100644 index 0000000000..47bf147308 --- /dev/null +++ b/issue-bot/tests/Playground/TabCreatorTest.php @@ -0,0 +1,134 @@ +>, list}> + */ + public function dataCreate(): array + { + return [ + [ + [ + 70100 => [ + + ], + 70200 => [ + + ], + ], + [ + new PlaygroundResultTab('PHP 7.1 – 7.2', []), + ], + ], + [ + [ + 70100 => [ + new PlaygroundError(2, 'Foo', null), + ], + 70200 => [ + new PlaygroundError(2, 'Foo', null), + ], + ], + [ + new PlaygroundResultTab('PHP 7.1 – 7.2 (1 error)', [ + new PlaygroundError(2, 'Foo', null), + ]), + ], + ], + [ + [ + 70100 => [ + new PlaygroundError(2, 'Foo', null), + new PlaygroundError(3, 'Foo', null), + ], + 70200 => [ + new PlaygroundError(2, 'Foo', null), + new PlaygroundError(3, 'Foo', null), + ], + ], + [ + new PlaygroundResultTab('PHP 7.1 – 7.2 (2 errors)', [ + new PlaygroundError(2, 'Foo', null), + new PlaygroundError(3, 'Foo', null), + ]), + ], + ], + [ + [ + 70100 => [ + new PlaygroundError(2, 'Foo', null), + new PlaygroundError(3, 'Foo', null), + ], + 70200 => [ + new PlaygroundError(3, 'Foo', null), + ], + ], + [ + new PlaygroundResultTab('PHP 7.2 (1 error)', [ + new PlaygroundError(3, 'Foo', null), + ]), + new PlaygroundResultTab('PHP 7.1 (2 errors)', [ + new PlaygroundError(2, 'Foo', null), + new PlaygroundError(3, 'Foo', null), + ]), + ], + ], + [ + [ + 70100 => [ + new PlaygroundError(2, 'Foo', 'attribute.notFound'), + ], + ], + [ + new PlaygroundResultTab('PHP 7.1 (1 error)', [ + new PlaygroundError(2, 'Foo', null), + ]), + ], + ], + [ + [ + 70100 => [ + new PlaygroundError(2, 'Foo', 'phpstanPlayground.never'), + ], + ], + [ + new PlaygroundResultTab('PHP 7.1 (1 error)', [ + new PlaygroundError(2, 'Tip: Foo', null), + ]), + ], + ], + ]; + } + + /** + * @dataProvider dataCreate + * @param array> $versionedErrors + * @param list $expectedTabs + * @return void + */ + public function testCreate(array $versionedErrors, array $expectedTabs): void + { + $tabCreator = new TabCreator(); + $tabs = $tabCreator->create($versionedErrors); + self::assertCount(count($expectedTabs), $tabs); + + foreach ($tabs as $i => $tab) { + $expectedTab = $expectedTabs[$i]; + self::assertSame($expectedTab->getTitle(), $tab->getTitle()); + self::assertCount(count($expectedTab->getErrors()), $tab->getErrors()); + foreach ($tab->getErrors() as $j => $error) { + $expectedError = $expectedTab->getErrors()[$j]; + self::assertSame($expectedError->getMessage(), $error->getMessage()); + self::assertSame($expectedError->getLine(), $error->getLine()); + } + } + } + +} diff --git a/issue-bot/tests/PostGeneratorTest.php b/issue-bot/tests/PostGeneratorTest.php new file mode 100644 index 0000000000..06c30811c9 --- /dev/null +++ b/issue-bot/tests/PostGeneratorTest.php @@ -0,0 +1,133 @@ +, list, BotComment[], string|null}> + */ + public function dataGeneratePosts(): iterable + { + $diff = '@@ @@ +-1: abc ++1: def +'; + + $details = "| Line | Error | +|---|---| +| 1 | `def` | +"; + + yield [ + 'abc-def', + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'abc', null), + ])], + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'abc', null), + ])], + [], + null, + null, + ]; + + yield [ + 'abc-def', + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'abc', null), + ])], + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'def', null), + ])], + [], + $diff, + $details, + ]; + + yield [ + 'abc-def', + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'abc', null), + ])], + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'def', null), + ])], + [ + new BotComment('', new PlaygroundExample('', 'abc-def'), 'some diff'), + ], + $diff, + $details, + ]; + + yield [ + 'abc-def', + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'abc', null), + ])], + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'def', null), + ])], + [ + new BotComment('', new PlaygroundExample('', 'abc-def'), $diff), + ], + null, + null, + ]; + + yield [ + 'abc-def', + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'abc', null), + ])], + [new PlaygroundResultTab('PHP 7.1', [ + new PlaygroundError(1, 'Internal error', null), + ])], + [], + null, + null, + ]; + } + + /** + * @dataProvider dataGeneratePosts + * @param list $originalTabs + * @param list $currentTabs + * @param BotComment[] $botComments + */ + public function testGeneratePosts( + string $hash, + array $originalTabs, + array $currentTabs, + array $botComments, + ?string $expectedDiff, + ?string $expectedDetails + ): void + { + $generator = new PostGenerator(new Differ(new UnifiedDiffOutputBuilder(''))); + $text = $generator->createText( + $hash, + $originalTabs, + $currentTabs, + $botComments, + ); + if ($text === null) { + self::assertNull($expectedDiff); + self::assertNull($expectedDetails); + return; + } + + self::assertSame($expectedDiff, $text['diff']); + self::assertSame($expectedDetails, $text['details']); + } + +} diff --git a/issue-bot/tmp/.gitignore b/issue-bot/tmp/.gitignore new file mode 100644 index 0000000000..125e34294b --- /dev/null +++ b/issue-bot/tmp/.gitignore @@ -0,0 +1,2 @@ +* +!.* diff --git a/patches/BooleanTypeMapper.patch b/patches/BooleanTypeMapper.patch deleted file mode 100644 index 1a85eecaf2..0000000000 --- a/patches/BooleanTypeMapper.patch +++ /dev/null @@ -1,13 +0,0 @@ -@package rector/rector - ---- packages/PHPStanStaticTypeMapper/TypeMapper/BooleanTypeMapper.php 2022-09-23 11:46:53.000000000 +0200 -+++ packages/PHPStanStaticTypeMapper/TypeMapper/BooleanTypeMapper.php 2022-09-27 11:04:44.000000000 +0200 -@@ -44,7 +44,7 @@ - } - if ($type instanceof ConstantBooleanType) { - // cannot be parent of union -- return new IdentifierTypeNode('true'); -+ return new IdentifierTypeNode('false'); - } - return new IdentifierTypeNode('bool'); - } diff --git a/patches/Buffer.patch b/patches/Buffer.patch index 9b2e2c7f86..1e50ecf112 100644 --- a/patches/Buffer.patch +++ b/patches/Buffer.patch @@ -1,5 +1,3 @@ -@package hoa/iterator - --- Buffer.php 2017-01-10 11:34:47.000000000 +0100 +++ Buffer.php 2021-10-30 16:36:22.000000000 +0200 @@ -103,7 +103,7 @@ diff --git a/patches/Consistency.patch b/patches/Consistency.patch index 73926b901a..4409109b36 100644 --- a/patches/Consistency.patch +++ b/patches/Consistency.patch @@ -1,5 +1,3 @@ -@package hoa/consistency - --- Consistency.php 2017-05-02 14:18:12.000000000 +0200 +++ Consistency.php 2020-05-05 08:28:35.000000000 +0200 @@ -319,42 +319,6 @@ diff --git a/patches/Lookahead.patch b/patches/Lookahead.patch index b6c283492c..d17a378444 100644 --- a/patches/Lookahead.patch +++ b/patches/Lookahead.patch @@ -1,5 +1,3 @@ -@package hoa/iterator - --- Lookahead.php 2017-01-10 11:34:47.000000000 +0100 +++ Lookahead.php 2021-10-30 16:35:30.000000000 +0200 @@ -93,7 +93,7 @@ diff --git a/patches/NameNodeMapper.patch b/patches/NameNodeMapper.patch deleted file mode 100644 index 030d2479a8..0000000000 --- a/patches/NameNodeMapper.patch +++ /dev/null @@ -1,21 +0,0 @@ -@package rector/rector - ---- packages/StaticTypeMapper/PhpParser/NameNodeMapper.php 2022-09-23 11:46:53.000000000 +0200 -+++ packages/StaticTypeMapper/PhpParser/NameNodeMapper.php 2022-09-27 11:05:58.000000000 +0200 -@@ -14,6 +14,7 @@ - use PHPStan\Type\FloatType; - use PHPStan\Type\IntegerType; - use PHPStan\Type\MixedType; -+use PHPStan\Type\ObjectType; - use PHPStan\Type\ObjectWithoutClassType; - use PHPStan\Type\StaticType; - use PHPStan\Type\StringType; -@@ -107,7 +108,7 @@ - } - return new ParentObjectWithoutClassType(); - } -- return new ThisType($classReflection); -+ return new ObjectType($classReflection->getName()); - } - /** - * @return \PHPStan\Type\ArrayType|\PHPStan\Type\IntegerType|\PHPStan\Type\FloatType|\PHPStan\Type\StringType|\PHPStan\Type\Constant\ConstantBooleanType|\PHPStan\Type\BooleanType|\PHPStan\Type\MixedType diff --git a/patches/Node.patch b/patches/Node.patch index 303ab36e75..d289251ebd 100644 --- a/patches/Node.patch +++ b/patches/Node.patch @@ -1,5 +1,3 @@ -@package hoa/protocol - --- Node/Node.php 2017-01-14 13:26:10.000000000 +0100 +++ Node/Node.php 2021-10-30 16:32:43.000000000 +0200 @@ -108,7 +108,7 @@ diff --git a/patches/PDO.patch b/patches/PDO.patch index 51d125c9d0..17aff2c148 100644 --- a/patches/PDO.patch +++ b/patches/PDO.patch @@ -1,6 +1,3 @@ -@package jetbrains/phpstorm-stubs -@version dev-master - --- PDO/PDO.php 2021-12-26 15:44:39.000000000 +0100 +++ PDO/PDO.php 2022-01-03 22:54:21.000000000 +0100 @@ -1415,7 +1415,7 @@ diff --git a/patches/ReflectionProperty.patch b/patches/ReflectionProperty.patch new file mode 100644 index 0000000000..38d60b1bd4 --- /dev/null +++ b/patches/ReflectionProperty.patch @@ -0,0 +1,11 @@ +--- Reflection/ReflectionProperty.php 2023-09-07 12:59:56.000000000 +0200 ++++ Reflection/ReflectionProperty.php 2023-09-15 13:24:07.900736741 +0200 +@@ -248,7 +248,7 @@ + * Gets property type + * + * @link https://php.net/manual/en/reflectionproperty.gettype.php +- * @return ReflectionNamedType|ReflectionUnionType|null Returns a {@see ReflectionType} if the ++ * @return ReflectionType|null Returns a {@see ReflectionType} if the + * property has a type, and {@see null} otherwise. + * @since 7.4 + */ diff --git a/patches/Rule.patch b/patches/Rule.patch index 2efb475fe8..faf698a5ff 100644 --- a/patches/Rule.patch +++ b/patches/Rule.patch @@ -1,5 +1,3 @@ -@package hoa/compiler - --- Llk/Rule/Rule.php 2017-08-08 09:44:07.000000000 +0200 +++ Llk/Rule/Rule.php 2021-10-29 16:42:12.000000000 +0200 @@ -118,7 +118,10 @@ diff --git a/patches/SessionHandler.patch b/patches/SessionHandler.patch index 598c201951..ba45ffc1b5 100644 --- a/patches/SessionHandler.patch +++ b/patches/SessionHandler.patch @@ -1,6 +1,3 @@ -@package jetbrains/phpstorm-stubs -@version dev-master - --- session/SessionHandler.php 2021-11-04 14:27:30.000000000 +0100 +++ session/SessionHandler.php 2021-11-05 11:26:14.000000000 +0100 @@ -147,7 +147,7 @@ diff --git a/patches/Stream.patch b/patches/Stream.patch index 5abd708df1..daf6990e1b 100644 --- a/patches/Stream.patch +++ b/patches/Stream.patch @@ -1,5 +1,3 @@ -@package hoa/stream - --- Stream.php 2017-02-21 17:01:06.000000000 +0100 +++ Stream.php 2021-04-19 17:10:20.000000000 +0200 @@ -192,7 +192,7 @@ diff --git a/patches/Wrapper.patch b/patches/Wrapper.patch index ade4d0a2fe..b8282376bd 100644 --- a/patches/Wrapper.patch +++ b/patches/Wrapper.patch @@ -1,5 +1,3 @@ -@package hoa/protocol - --- Wrapper.php 2017-01-14 13:26:10.000000000 +0100 +++ Wrapper.php 2020-05-05 08:39:18.000000000 +0200 @@ -582,24 +582,3 @@ diff --git a/patches/dom_c.patch b/patches/dom_c.patch new file mode 100644 index 0000000000..9e542c826d --- /dev/null +++ b/patches/dom_c.patch @@ -0,0 +1,17 @@ +--- dom/dom_c.php 2024-01-02 12:04:54 ++++ dom/dom_c.php 2024-01-21 10:41:56 +@@ -1347,6 +1347,14 @@ + */ + class DOMNamedNodeMap implements IteratorAggregate, Countable + { ++ ++ /** ++ * The number of nodes in the map. The range of valid child node indices is 0 to length - 1 inclusive. ++ * @var int ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $length; + /** + * Retrieves a node specified by name + * @link https://php.net/manual/en/domnamednodemap.getnameditem.php diff --git a/patches/paratest.patch b/patches/paratest.patch index 1c38e962db..f2091828ff 100644 --- a/patches/paratest.patch +++ b/patches/paratest.patch @@ -1,6 +1,3 @@ -@package brianium/paratest -@version ^4.0 - --- src/Runners/PHPUnit/Worker/BaseWorker.php 2020-02-07 23:07:07.000000000 +0100 +++ src/Runners/PHPUnit/Worker/BaseWorker.php 2022-03-27 17:35:45.000000000 +0200 @@ -28,17 +28,18 @@ diff --git a/patches/xmlreader.patch b/patches/xmlreader.patch new file mode 100644 index 0000000000..7be168133f --- /dev/null +++ b/patches/xmlreader.patch @@ -0,0 +1,122 @@ +--- xmlreader/xmlreader.php 2024-01-21 10:44:31 ++++ xmlreader/xmlreader.php 2024-01-21 10:48:24 +@@ -28,7 +28,119 @@ + */ + class XMLReader + { ++ /** ++ * The number of attributes on the node ++ * @var int ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $attributeCount; ++ ++ /** ++ * The base URI of the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $baseURI; ++ ++ /** ++ * Depth of the node in the tree, starting at 0 ++ * @var int ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $depth; ++ ++ /** ++ * Indicates if node has attributes ++ * @var bool ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $hasAttributes; ++ ++ /** ++ * Indicates if node has a text value ++ * @var bool ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $hasValue; ++ ++ /** ++ * Indicates if attribute is defaulted from DTD ++ * @var bool ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $isDefault; ++ ++ /** ++ * Indicates if node is an empty element tag ++ * @var bool ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $isEmptyElement; ++ ++ /** ++ * The local name of the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $localName; ++ + /** ++ * The qualified name of the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $name; ++ ++ /** ++ * The URI of the namespace associated with the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $namespaceURI; ++ ++ /** ++ * The node type for the node ++ * @var int ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $nodeType; ++ ++ /** ++ * The prefix of the namespace associated with the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $prefix; ++ ++ /** ++ * The text value of the node ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $value; ++ ++ /** ++ * The xml:lang scope which the node resides ++ * @var string ++ * @readonly ++ */ ++ #[PhpStormStubsElementAvailable(from: '8.1')] ++ public $xmlLang; ++ ++ /** + * No node type + */ + public const NONE = 0; diff --git a/phpcs.xml b/phpcs.xml index 5ed2ae964d..7ef56aaeb0 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -16,6 +16,8 @@ apigen/src changelog-generator/src changelog-generator/run.php + issue-bot/src + issue-bot/console.php @@ -26,6 +28,7 @@ + @@ -44,6 +47,9 @@ + + src/Rules/Whitespace/FileWhitespaceRule.php + 10 @@ -74,9 +80,6 @@ tests - - src/Command/AnalyseApplication.php - @@ -99,6 +102,7 @@ + diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index be5dbffb4d..7300fbd87d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3,17 +3,25 @@ parameters: - message: "#^Function is_a\\(\\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\\. Use objects retrieved from ReflectionProvider instead\\.$#" count: 1 - path: src/Analyser/DirectScopeFactory.php + path: src/Analyser/DirectInternalScopeFactory.php - message: "#^Cannot assign offset 'realCount' to array\\|string\\.$#" count: 1 - path: src/Analyser/IgnoredErrorHelperResult.php + path: src/Analyser/Ignore/IgnoredErrorHelperResult.php - message: "#^Function is_a\\(\\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\\. Use objects retrieved from ReflectionProvider instead\\.$#" count: 1 - path: src/Analyser/LazyScopeFactory.php + path: src/Analyser/LazyInternalScopeFactory.php + + - + message: """ + #^Call to deprecated method getAnyArrays\\(\\) of class PHPStan\\\\Type\\\\TypeUtils\\: + Use PHPStan\\\\Type\\\\Type\\:\\:getArrays\\(\\) instead\\.$# + """ + count: 2 + path: src/Analyser/MutatingScope.php - message: """ @@ -24,25 +32,58 @@ parameters: path: src/Analyser/MutatingScope.php - - message: "#^Only numeric types are allowed in pre\\-decrement, bool\\|float\\|int\\|string\\|null given\\.$#" - count: 1 + message: "#^Casting to string something that's already string\\.$#" + count: 3 + path: src/Analyser/MutatingScope.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 4 + path: src/Analyser/MutatingScope.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 3 path: src/Analyser/MutatingScope.php - - message: "#^Only numeric types are allowed in pre\\-increment, bool\\|float\\|int\\|string\\|null given\\.$#" + message: "#^Only numeric types are allowed in pre\\-increment, float\\|int\\|string\\|null given\\.$#" count: 1 path: src/Analyser/MutatingScope.php - - message: "#^Parameter \\#10 \\$reflection of class PHPStan\\\\Reflection\\\\ClassReflection constructor expects PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionClass\\|PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum, object given\\.$#" + message: """ + #^Call to deprecated method doNotTreatPhpDocTypesAsCertain\\(\\) of class PHPStan\\\\Analyser\\\\MutatingScope\\: + Use getNativeType\\(\\)$# + """ count: 1 path: src/Analyser/NodeScopeResolver.php + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 3 + path: src/Analyser/NodeScopeResolver.php + - message: "#^Parameter \\#2 \\$node of method PHPStan\\\\BetterReflection\\\\SourceLocator\\\\Ast\\\\Strategy\\\\NodeToReflection\\:\\:__invoke\\(\\) expects PhpParser\\\\Node\\\\Expr\\\\ArrowFunction\\|PhpParser\\\\Node\\\\Expr\\\\Closure\\|PhpParser\\\\Node\\\\Expr\\\\FuncCall\\|PhpParser\\\\Node\\\\Stmt\\\\Class_\\|PhpParser\\\\Node\\\\Stmt\\\\Const_\\|PhpParser\\\\Node\\\\Stmt\\\\Enum_\\|PhpParser\\\\Node\\\\Stmt\\\\Function_\\|PhpParser\\\\Node\\\\Stmt\\\\Interface_\\|PhpParser\\\\Node\\\\Stmt\\\\Trait_, PhpParser\\\\Node\\\\Stmt\\\\ClassLike given\\.$#" count: 1 path: src/Analyser/NodeScopeResolver.php + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 2 + path: src/Analyser/TypeSpecifier.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 5 + path: src/Analyser/TypeSpecifier.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 3 + path: src/Analyser/TypeSpecifier.php + - message: "#^Template type TNodeType is declared as covariant, but occurs in contravariant position in parameter node of method PHPStan\\\\Collectors\\\\Collector\\:\\:processNode\\(\\)\\.$#" count: 1 @@ -99,19 +140,14 @@ parameters: path: src/Command/ErrorsConsoleStyle.php - - message: "#^Call to an undefined method React\\\\Promise\\\\PromiseInterface\\:\\:done\\(\\)\\.$#" - count: 2 - path: src/Command/FixerApplication.php - - - - message: "#^Call to an undefined method React\\\\Promise\\\\PromiseInterface\\\\:\\:done\\(\\)\\.$#" + message: "#^Call to an undefined method React\\\\Promise\\\\PromiseInterface\\\\:\\:done\\(\\)\\.$#" count: 1 path: src/Command/FixerApplication.php - - message: "#^Parameter \\#1 \\$arg of function escapeshellarg expects string, string\\|false given\\.$#" + message: "#^Call to an undefined method React\\\\Promise\\\\PromiseInterface\\:\\:done\\(\\)\\.$#" count: 1 - path: src/Command/FixerApplication.php + path: src/Command/FixerWorkerCommand.php - message: "#^Call to an undefined method React\\\\Promise\\\\PromiseInterface\\:\\:done\\(\\)\\.$#" @@ -119,19 +155,24 @@ parameters: path: src/Command/WorkerCommand.php - - message: "#^Fetching class constant PREVENT_MERGING of deprecated class Nette\\\\DI\\\\Config\\\\Helpers\\.$#" + message: "#^Variable method call on Nette\\\\Schema\\\\Elements\\\\AnyOf\\|Nette\\\\Schema\\\\Elements\\\\Structure\\|Nette\\\\Schema\\\\Elements\\\\Type\\.$#" count: 1 - path: src/DependencyInjection/NeonAdapter.php + path: src/DependencyInjection/ContainerFactory.php - - message: "#^Variable method call on Nette\\\\Schema\\\\Elements\\\\AnyOf\\|Nette\\\\Schema\\\\Elements\\\\Structure\\|Nette\\\\Schema\\\\Elements\\\\Type\\.$#" + message: "#^Variable static method call on Nette\\\\Schema\\\\Expect\\.$#" count: 1 - path: src/DependencyInjection/ParametersSchemaExtension.php + path: src/DependencyInjection/ContainerFactory.php - - message: "#^Variable static method call on Nette\\\\Schema\\\\Expect\\.$#" + message: "#^Fetching class constant PREVENT_MERGING of deprecated class Nette\\\\DI\\\\Config\\\\Helpers\\.$#" + count: 1 + path: src/DependencyInjection/NeonAdapter.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" count: 1 - path: src/DependencyInjection/ParametersSchemaExtension.php + path: src/Internal/ContainerDynamicReturnTypeExtension.php - message: "#^Variable method call on PHPStan\\\\Reflection\\\\ClassReflection\\.$#" @@ -143,6 +184,31 @@ parameters: count: 1 path: src/PhpDoc/PhpDocBlock.php + - + message: "#^Call to function method_exists\\(\\) with PHPStan\\\\PhpDocParser\\\\Ast\\\\PhpDoc\\\\PhpDocNode and 'getParamOutTypeTagV…' will always evaluate to true\\.$#" + count: 1 + path: src/PhpDoc/PhpDocNodeResolver.php + + - + message: "#^Call to function method_exists\\(\\) with PHPStan\\\\PhpDocParser\\\\Ast\\\\PhpDoc\\\\PhpDocNode and 'getSelfOutTypeTagVa…' will always evaluate to true\\.$#" + count: 1 + path: src/PhpDoc/PhpDocNodeResolver.php + + - + message: "#^Property PHPStan\\\\PhpDocParser\\\\Ast\\\\PhpDoc\\\\AssertTagMethodValueNode\\:\\:\\$isEquality \\(bool\\) on left side of \\?\\? is not nullable\\.$#" + count: 1 + path: src/PhpDoc/PhpDocNodeResolver.php + + - + message: "#^Property PHPStan\\\\PhpDocParser\\\\Ast\\\\PhpDoc\\\\AssertTagPropertyValueNode\\:\\:\\$isEquality \\(bool\\) on left side of \\?\\? is not nullable\\.$#" + count: 1 + path: src/PhpDoc/PhpDocNodeResolver.php + + - + message: "#^Property PHPStan\\\\PhpDocParser\\\\Ast\\\\PhpDoc\\\\AssertTagValueNode\\:\\:\\$isEquality \\(bool\\) on left side of \\?\\? is not nullable\\.$#" + count: 1 + path: src/PhpDoc/PhpDocNodeResolver.php + - message: "#^Method PHPStan\\\\PhpDoc\\\\ResolvedPhpDocBlock\\:\\:getNameScope\\(\\) should return PHPStan\\\\Analyser\\\\NameScope but returns PHPStan\\\\Analyser\\\\NameScope\\|null\\.$#" count: 1 @@ -156,6 +222,11 @@ parameters: count: 1 path: src/PhpDoc/StubValidator.php + - + message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\ParamOutTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\ParamOutTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#" + count: 1 + path: src/PhpDoc/Tag/ParamOutTag.php + - message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\ParamTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\ParamTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#" count: 1 @@ -166,23 +237,48 @@ parameters: count: 1 path: src/PhpDoc/Tag/ReturnTag.php + - + message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\SelfOutTypeTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\SelfOutTypeTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#" + count: 1 + path: src/PhpDoc/Tag/SelfOutTypeTag.php + - message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\VarTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\VarTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#" count: 1 path: src/PhpDoc/Tag/VarTag.php - - message: "#^Dead catch \\- PHPStan\\\\BetterReflection\\\\Identifier\\\\Exception\\\\InvalidIdentifierName is never thrown in the try block\\.$#" + message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:isArray\\(\\) or Type\\:\\:getArrays\\(\\) instead\\.$#" + count: 1 + path: src/PhpDoc/TypeNodeResolver.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\CallableType is error\\-prone and deprecated\\. Use Type\\:\\:isCallable\\(\\) and Type\\:\\:getCallableParametersAcceptors\\(\\) instead\\.$#" + count: 1 + path: src/PhpDoc/TypeNodeResolver.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IterableType is error\\-prone and deprecated\\. Use Type\\:\\:isIterable\\(\\) instead\\.$#" + count: 1 + path: src/PhpDoc/TypeNodeResolver.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" count: 2 - path: src/Reflection/BetterReflection/BetterReflectionProvider.php + path: src/PhpDoc/TypeNodeResolver.php - - message: "#^Dead catch \\- PHPStan\\\\BetterReflection\\\\NodeCompiler\\\\Exception\\\\UnableToCompileNode\\|PHPStan\\\\BetterReflection\\\\Reflection\\\\Exception\\\\NotAClassReflection\\|PHPStan\\\\BetterReflection\\\\Reflection\\\\Exception\\\\NotAnInterfaceReflection is never thrown in the try block\\.$#" + message: "#^Property PHPStan\\\\PhpDocParser\\\\Ast\\\\Type\\\\CallableTypeNode\\:\\:\\$templateTypes \\(array\\\\) on left side of \\?\\? is not nullable\\.$#" count: 1 + path: src/PhpDoc/TypeNodeResolver.php + + - + message: "#^Dead catch \\- PHPStan\\\\BetterReflection\\\\Identifier\\\\Exception\\\\InvalidIdentifierName is never thrown in the try block\\.$#" + count: 3 path: src/Reflection/BetterReflection/BetterReflectionProvider.php - - message: "#^Parameter \\#10 \\$reflection of class PHPStan\\\\Reflection\\\\ClassReflection constructor expects PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionClass\\|PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum, object given\\.$#" + message: "#^Dead catch \\- PHPStan\\\\BetterReflection\\\\NodeCompiler\\\\Exception\\\\UnableToCompileNode is never thrown in the try block\\.$#" count: 1 path: src/Reflection/BetterReflection/BetterReflectionProvider.php @@ -214,6 +310,11 @@ parameters: count: 1 path: src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php + - + message: "#^Parameter \\#2 \\$node of method PHPStan\\\\BetterReflection\\\\SourceLocator\\\\Ast\\\\Strategy\\\\NodeToReflection\\:\\:__invoke\\(\\) expects PhpParser\\\\Node\\\\Expr\\\\ArrowFunction\\|PhpParser\\\\Node\\\\Expr\\\\Closure\\|PhpParser\\\\Node\\\\Expr\\\\FuncCall\\|PhpParser\\\\Node\\\\Stmt\\\\Class_\\|PhpParser\\\\Node\\\\Stmt\\\\Const_\\|PhpParser\\\\Node\\\\Stmt\\\\Enum_\\|PhpParser\\\\Node\\\\Stmt\\\\Function_\\|PhpParser\\\\Node\\\\Stmt\\\\Interface_\\|PhpParser\\\\Node\\\\Stmt\\\\Trait_, PhpParser\\\\Node\\\\Expr\\\\FuncCall\\|PhpParser\\\\Node\\\\Stmt\\\\ClassLike\\|PhpParser\\\\Node\\\\Stmt\\\\Const_\\|PhpParser\\\\Node\\\\Stmt\\\\Function_ given\\.$#" + count: 1 + path: src/Reflection/BetterReflection/SourceLocator/NewOptimizedDirectorySourceLocator.php + - message: "#^Parameter \\#2 \\$node of method PHPStan\\\\BetterReflection\\\\SourceLocator\\\\Ast\\\\Strategy\\\\NodeToReflection\\:\\:__invoke\\(\\) expects PhpParser\\\\Node\\\\Expr\\\\ArrowFunction\\|PhpParser\\\\Node\\\\Expr\\\\Closure\\|PhpParser\\\\Node\\\\Expr\\\\FuncCall\\|PhpParser\\\\Node\\\\Stmt\\\\Class_\\|PhpParser\\\\Node\\\\Stmt\\\\Const_\\|PhpParser\\\\Node\\\\Stmt\\\\Enum_\\|PhpParser\\\\Node\\\\Stmt\\\\Function_\\|PhpParser\\\\Node\\\\Stmt\\\\Interface_\\|PhpParser\\\\Node\\\\Stmt\\\\Trait_, PhpParser\\\\Node\\\\Expr\\\\FuncCall\\|PhpParser\\\\Node\\\\Stmt\\\\ClassLike\\|PhpParser\\\\Node\\\\Stmt\\\\Const_\\|PhpParser\\\\Node\\\\Stmt\\\\Function_ given\\.$#" count: 1 @@ -221,7 +322,7 @@ parameters: - message: "#^Parameter \\#2 \\$node of method PHPStan\\\\BetterReflection\\\\SourceLocator\\\\Ast\\\\Strategy\\\\NodeToReflection\\:\\:__invoke\\(\\) expects PhpParser\\\\Node\\\\Expr\\\\ArrowFunction\\|PhpParser\\\\Node\\\\Expr\\\\Closure\\|PhpParser\\\\Node\\\\Expr\\\\FuncCall\\|PhpParser\\\\Node\\\\Stmt\\\\Class_\\|PhpParser\\\\Node\\\\Stmt\\\\Const_\\|PhpParser\\\\Node\\\\Stmt\\\\Enum_\\|PhpParser\\\\Node\\\\Stmt\\\\Function_\\|PhpParser\\\\Node\\\\Stmt\\\\Interface_\\|PhpParser\\\\Node\\\\Stmt\\\\Trait_, PhpParser\\\\Node\\\\Stmt\\\\ClassLike given\\.$#" - count: 1 + count: 2 path: src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php - @@ -240,22 +341,27 @@ parameters: path: src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php - - message: "#^Method PHPStan\\\\Reflection\\\\ClassReflection\\:\\:getCacheKey\\(\\) should return string but returns string\\|null\\.$#" + message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericObjectType is error\\-prone and deprecated\\.$#" + count: 3 + path: src/Reflection/ClassReflection.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" count: 1 path: src/Reflection/ClassReflection.php - - message: "#^Binary operation \"&\" between bool\\|float\\|int\\|string\\|null and bool\\|float\\|int\\|string\\|null results in an error\\.$#" + message: "#^Method PHPStan\\\\Reflection\\\\ClassReflection\\:\\:getCacheKey\\(\\) should return string but returns string\\|null\\.$#" count: 1 - path: src/Reflection/InitializerExprTypeResolver.php + path: src/Reflection/ClassReflection.php - - message: "#^Binary operation \"\\*\" between bool\\|float\\|int\\|string\\|null and bool\\|float\\|int\\|string\\|null results in an error\\.$#" + message: "#^Binary operation \"&\" between bool\\|float\\|int\\|string\\|null and bool\\|float\\|int\\|string\\|null results in an error\\.$#" count: 1 path: src/Reflection/InitializerExprTypeResolver.php - - message: "#^Binary operation \"\\*\\*\" between bool\\|float\\|int\\|string\\|null and bool\\|float\\|int\\|string\\|null results in an error\\.$#" + message: "#^Binary operation \"\\*\" between bool\\|float\\|int\\|string\\|null and bool\\|float\\|int\\|string\\|null results in an error\\.$#" count: 1 path: src/Reflection/InitializerExprTypeResolver.php @@ -297,11 +403,179 @@ parameters: count: 1 path: src/Reflection/InitializerExprTypeResolver.php + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 22 + path: src/Reflection/InitializerExprTypeResolver.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + count: 4 + path: src/Reflection/InitializerExprTypeResolver.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 3 + path: src/Reflection/InitializerExprTypeResolver.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 10 + path: src/Reflection/InitializerExprTypeResolver.php + + - + message: "#^PHPDoc tag @var with type float\\|int is not subtype of native type int\\.$#" + count: 1 + path: src/Reflection/InitializerExprTypeResolver.php + + - + message: "#^PHPDoc tag @var with type float\\|int is not subtype of type int\\.$#" + count: 4 + path: src/Reflection/InitializerExprTypeResolver.php + + - + message: "#^PHPDoc tag @var with type float\\|int\\|null is not subtype of type int\\|null\\.$#" + count: 6 + path: src/Reflection/InitializerExprTypeResolver.php + - message: "#^Creating new PHPStan\\\\Php8StubsMap is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" count: 1 path: src/Reflection/SignatureMap/Php8SignatureMapProvider.php + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Rules/Api/NodeConnectingVisitorAttributesRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 1 + path: src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 2 + path: src/Rules/Classes/ImpossibleInstanceOfRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + count: 2 + path: src/Rules/Classes/RequireExtendsRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + count: 1 + path: src/Rules/Classes/RequireImplementsRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 6 + path: src/Rules/Comparison/BooleanAndConstantConditionRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 2 + path: src/Rules/Comparison/BooleanNotConstantConditionRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 6 + path: src/Rules/Comparison/BooleanOrConstantConditionRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 2 + path: src/Rules/Comparison/ConstantLooseComparisonRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 2 + path: src/Rules/Comparison/DoWhileLoopConstantConditionRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 2 + path: src/Rules/Comparison/ElseIfConstantConditionRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 2 + path: src/Rules/Comparison/IfConstantConditionRule.php + + - + message: """ + #^Call to deprecated method doNotTreatPhpDocTypesAsCertain\\(\\) of interface PHPStan\\\\Analyser\\\\Scope\\: + Use getNativeType\\(\\)$# + """ + count: 1 + path: src/Rules/Comparison/ImpossibleCheckTypeHelper.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + count: 2 + path: src/Rules/Comparison/ImpossibleCheckTypeHelper.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 2 + path: src/Rules/Comparison/ImpossibleCheckTypeHelper.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 3 + path: src/Rules/Comparison/ImpossibleCheckTypeHelper.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\TypeWithClassName is error\\-prone and deprecated\\. Use Type\\:\\:getObjectClassNames\\(\\) or Type\\:\\:getObjectClassReflections\\(\\) instead\\.$#" + count: 1 + path: src/Rules/Comparison/ImpossibleCheckTypeHelper.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 4 + path: src/Rules/Comparison/LogicalXorConstantConditionRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 4 + path: src/Rules/Comparison/MatchExpressionRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 2 + path: src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 2 + path: src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 2 + path: src/Rules/Comparison/TernaryOperatorConstantConditionRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 1 + path: src/Rules/Comparison/UnreachableIfBranchesRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 1 + path: src/Rules/Comparison/UnreachableTernaryElseBranchRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 1 + path: src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 1 + path: src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php + - message: "#^Function class_implements\\(\\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\\. Use objects retrieved from ReflectionProvider instead\\.$#" count: 1 @@ -327,6 +601,16 @@ parameters: count: 1 path: src/Rules/DirectRegistry.php + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericObjectType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Rules/Generics/GenericAncestorsCheck.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Rules/Generics/TemplateTypeCheck.php + - message: "#^Function class_implements\\(\\) is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine\\. Use objects retrieved from ReflectionProvider instead\\.$#" count: 1 @@ -353,112 +637,1184 @@ parameters: path: src/Rules/LazyRegistry.php - - message: "#^Anonymous function has an unused use \\$container\\.$#" + message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:isArray\\(\\) or Type\\:\\:getArrays\\(\\) instead\\.$#" count: 1 - path: src/Testing/PHPStanTestCase.php + path: src/Rules/Methods/MethodParameterComparisonHelper.php - - message: """ - #^Call to deprecated method getInstance\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: - Use PHPStan\\\\Reflection\\\\ReflectionProviderStaticAccessor instead$# - """ + message: "#^Doing instanceof PHPStan\\\\Type\\\\IterableType is error\\-prone and deprecated\\. Use Type\\:\\:isIterable\\(\\) instead\\.$#" count: 1 - path: src/Type/ObjectType.php + path: src/Rules/Methods/MethodParameterComparisonHelper.php - - message: """ - #^Call to deprecated method getUniversalObjectCratesClasses\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: - Inject %%universalObjectCratesClasses%% parameter instead\\.$# - """ + message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) and Type\\:\\:getClassStringObjectType\\(\\) instead\\.$#" count: 1 - path: src/Type/ObjectType.php + path: src/Rules/Methods/StaticMethodCallCheck.php - - message: """ - #^Call to deprecated method getTypeFromValue\\(\\) of class PHPStan\\\\Type\\\\ConstantTypeHelper\\: - Use PHPStan\\\\Reflection\\\\InitializerExprTypeResolver$# - """ + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" count: 1 - path: src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php + path: src/Rules/PhpDoc/RequireExtendsCheck.php - - message: "#^Unreachable statement \\- code above always terminates\\.$#" + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" count: 1 - path: tests/PHPStan/Analyser/AnalyserTest.php + path: src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php - - message: "#^Class PHPStan\\\\Analyser\\\\AnonymousClassNameRule implements generic interface PHPStan\\\\Rules\\\\Rule but does not specify its types\\: TNodeType$#" + message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericObjectType is error\\-prone and deprecated\\.$#" count: 1 - path: tests/PHPStan/Analyser/AnonymousClassNameRule.php + path: src/Rules/PhpDoc/VarTagTypeRuleHelper.php - - message: "#^Class PHPStan\\\\Analyser\\\\AnonymousClassNameRuleTest extends generic class PHPStan\\\\Testing\\\\RuleTestCase but does not specify its types\\: TRule$#" - count: 1 - path: tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php + message: "#^Access to an undefined property T of PHPStan\\\\Rules\\\\RuleError\\:\\:\\$tip\\.$#" + count: 2 + path: src/Rules/RuleErrorBuilder.php - - message: "#^Method PHPStan\\\\Analyser\\\\AnonymousClassNameRuleTest\\:\\:getRule\\(\\) return type with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" count: 1 - path: tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php + path: src/Rules/RuleLevelHelper.php - - message: "#^Class PHPStan\\\\Analyser\\\\EvaluationOrderRule implements generic interface PHPStan\\\\Rules\\\\Rule but does not specify its types\\: TNodeType$#" - count: 1 - path: tests/PHPStan/Analyser/EvaluationOrderRule.php + message: "#^Doing instanceof PHPStan\\\\Type\\\\NullType is error\\-prone and deprecated\\. Use Type\\:\\:isNull\\(\\) instead\\.$#" + count: 2 + path: src/Rules/RuleLevelHelper.php - - message: "#^Class PHPStan\\\\Analyser\\\\EvaluationOrderTest extends generic class PHPStan\\\\Testing\\\\RuleTestCase but does not specify its types\\: TRule$#" + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectWithoutClassType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) instead\\.$#" count: 1 - path: tests/PHPStan/Analyser/EvaluationOrderTest.php + path: src/Rules/RuleLevelHelper.php - - message: "#^Method PHPStan\\\\Analyser\\\\EvaluationOrderTest\\:\\:getRule\\(\\) return type with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" count: 1 - path: tests/PHPStan/Analyser/EvaluationOrderTest.php + path: src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php - - message: "#^Constant SOME_CONSTANT_IN_AUTOLOAD_FILE not found\\.$#" + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" count: 1 - path: tests/PHPStan/Command/AnalyseCommandTest.php + path: src/Rules/UnusedFunctionParametersCheck.php - - message: "#^Class PHPStan\\\\Node\\\\FileNodeTest extends generic class PHPStan\\\\Testing\\\\RuleTestCase but does not specify its types\\: TRule$#" + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" count: 1 - path: tests/PHPStan/Node/FileNodeTest.php + path: src/Rules/Variables/CompactVariablesRule.php - - message: "#^Method PHPStan\\\\Node\\\\FileNodeTest\\:\\:getRule\\(\\) return type with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" count: 1 - path: tests/PHPStan/Node/FileNodeTest.php + path: src/Rules/Variables/CompactVariablesRule.php - - message: "#^Creating new PHPStan\\\\Php8StubsMap is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: "#^Anonymous function has an unused use \\$container\\.$#" count: 1 - path: tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php + path: src/Testing/PHPStanTestCase.php - - message: """ - #^Instantiation of deprecated class PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayItemTypeRule\\: - Replaced by PHPStan\\\\Rules\\\\Properties\\\\TypesAssignedToPropertiesRule$# - """ - count: 1 - path: tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 2 + path: src/Testing/TypeInferenceTestCase.php - - message: """ - #^Return type of method PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayItemTypeRuleTest\\:\\:getRule\\(\\) has typehint with deprecated class PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayItemTypeRule\\: - Replaced by PHPStan\\\\Rules\\\\Properties\\\\TypesAssignedToPropertiesRule$# - """ + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" count: 1 - path: tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php + path: src/Type/Accessory/AccessoryArrayListType.php - - message: """ - #^Instantiation of deprecated class PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayKeyTypeRule\\: - Replaced by PHPStan\\\\Rules\\\\Properties\\\\TypesAssignedToPropertiesRule$# - """ + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" count: 1 - path: tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php + path: src/Type/Accessory/AccessoryLiteralStringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Accessory/AccessoryNonEmptyStringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/Accessory/AccessoryNonEmptyStringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/Accessory/AccessoryNonFalsyStringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Accessory/AccessoryNumericStringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/Accessory/AccessoryNumericStringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/Accessory/HasMethodType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/Accessory/HasOffsetType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 2 + path: src/Type/Accessory/HasOffsetValueType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 2 + path: src/Type/Accessory/HasOffsetValueType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/Accessory/HasOffsetValueType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/Accessory/HasPropertyType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/Accessory/NonEmptyArrayType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/Accessory/OversizedArrayType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:isArray\\(\\) or Type\\:\\:getArrays\\(\\) instead\\.$#" + count: 3 + path: src/Type/ArrayType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + count: 1 + path: src/Type/ArrayType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 2 + path: src/Type/ArrayType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/ArrayType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\BooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isBoolean\\(\\) instead\\.$#" + count: 2 + path: src/Type/BooleanType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 1 + path: src/Type/BooleanType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\CallableType is error\\-prone and deprecated\\. Use Type\\:\\:isCallable\\(\\) and Type\\:\\:getCallableParametersAcceptors\\(\\) instead\\.$#" + count: 4 + path: src/Type/CallableType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 2 + path: src/Type/CallableType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/ClosureType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:isArray\\(\\) or Type\\:\\:getArrays\\(\\) instead\\.$#" + count: 1 + path: src/Type/Constant/ConstantArrayType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 2 + path: src/Type/Constant/ConstantArrayType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + count: 8 + path: src/Type/Constant/ConstantArrayType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 3 + path: src/Type/Constant/ConstantArrayType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/Constant/ConstantArrayType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 2 + path: src/Type/Constant/ConstantArrayTypeBuilder.php + + - + message: "#^PHPDoc tag @var assumes the expression with type PHPStan\\\\Type\\\\Type is always PHPStan\\\\Type\\\\Constant\\\\ConstantIntegerType\\|PHPStan\\\\Type\\\\Constant\\\\ConstantStringType but it's error\\-prone and dangerous\\.$#" + count: 2 + path: src/Type/Constant/ConstantArrayTypeBuilder.php + + - + message: "#^PHPDoc tag @var with type float\\|int is not subtype of native type int\\.$#" + count: 2 + path: src/Type/Constant/ConstantArrayTypeBuilder.php + + - + message: "#^PHPDoc tag @var with type float\\|int is not subtype of type int\\.$#" + count: 1 + path: src/Type/Constant/ConstantArrayTypeBuilder.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\BooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isBoolean\\(\\) instead\\.$#" + count: 1 + path: src/Type/Constant/ConstantBooleanType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 4 + path: src/Type/Constant/ConstantBooleanType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 3 + path: src/Type/Constant/ConstantBooleanType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 4 + path: src/Type/Constant/ConstantFloatType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\FloatType is error\\-prone and deprecated\\. Use Type\\:\\:isFloat\\(\\) instead\\.$#" + count: 1 + path: src/Type/Constant/ConstantFloatType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 4 + path: src/Type/Constant/ConstantIntegerType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntegerType is error\\-prone and deprecated\\. Use Type\\:\\:isInteger\\(\\) instead\\.$#" + count: 1 + path: src/Type/Constant/ConstantIntegerType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) instead\\.$#" + count: 1 + path: src/Type/Constant/ConstantStringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 4 + path: src/Type/Constant/ConstantStringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 4 + path: src/Type/Constant/ConstantStringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) and Type\\:\\:getClassStringObjectType\\(\\) instead\\.$#" + count: 1 + path: src/Type/Constant/ConstantStringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\StringType is error\\-prone and deprecated\\. Use Type\\:\\:isString\\(\\) instead\\.$#" + count: 1 + path: src/Type/Constant/ConstantStringType.php + + - + message: "#^PHPDoc tag @var with type int\\|string is not subtype of type string\\.$#" + count: 1 + path: src/Type/Constant/ConstantStringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + count: 1 + path: src/Type/Constant/OversizedArrayBuilder.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Enum\\\\EnumCaseObjectType is error\\-prone and deprecated\\. Use Type\\:\\:getEnumCases\\(\\) instead\\.$#" + count: 2 + path: src/Type/Enum/EnumCaseObjectType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 2 + path: src/Type/ExponentiateHelper.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericObjectType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/FileTypeMapper.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\FloatType is error\\-prone and deprecated\\. Use Type\\:\\:isFloat\\(\\) instead\\.$#" + count: 2 + path: src/Type/FloatType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) instead\\.$#" + count: 1 + path: src/Type/Generic/GenericClassStringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 4 + path: src/Type/Generic/GenericClassStringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) and Type\\:\\:getClassStringObjectType\\(\\) instead\\.$#" + count: 4 + path: src/Type/Generic/GenericClassStringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/Generic/GenericClassStringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\StringType is error\\-prone and deprecated\\. Use Type\\:\\:isString\\(\\) instead\\.$#" + count: 2 + path: src/Type/Generic/GenericClassStringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericObjectType is error\\-prone and deprecated\\.$#" + count: 3 + path: src/Type/Generic/GenericObjectType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/Generic/GenericObjectType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + count: 1 + path: src/Type/Generic/GenericObjectType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\TypeWithClassName is error\\-prone and deprecated\\. Use Type\\:\\:getObjectClassNames\\(\\) or Type\\:\\:getObjectClassReflections\\(\\) instead\\.$#" + count: 2 + path: src/Type/Generic/GenericObjectType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 3 + path: src/Type/Generic/TemplateArrayType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 3 + path: src/Type/Generic/TemplateBenevolentUnionType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 3 + path: src/Type/Generic/TemplateBooleanType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 3 + path: src/Type/Generic/TemplateConstantArrayType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 3 + path: src/Type/Generic/TemplateConstantIntegerType.php + + - + message: "#^Method PHPStan\\\\Type\\\\Generic\\\\TemplateConstantIntegerType\\:\\:toPhpDocNode\\(\\) should return PHPStan\\\\PhpDocParser\\\\Ast\\\\Type\\\\ConstTypeNode but returns PHPStan\\\\PhpDocParser\\\\Ast\\\\Type\\\\IdentifierTypeNode\\.$#" + count: 1 + path: src/Type/Generic/TemplateConstantIntegerType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 3 + path: src/Type/Generic/TemplateConstantStringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 3 + path: src/Type/Generic/TemplateFloatType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 3 + path: src/Type/Generic/TemplateGenericObjectType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 3 + path: src/Type/Generic/TemplateIntegerType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 3 + path: src/Type/Generic/TemplateIntersectionType.php + + - + message: "#^Instanceof between PHPStan\\\\Type\\\\Type and PHPStan\\\\Type\\\\IntersectionType will always evaluate to false\\.$#" + count: 2 + path: src/Type/Generic/TemplateIntersectionType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 3 + path: src/Type/Generic/TemplateKeyOfType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 2 + path: src/Type/Generic/TemplateMixedType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 3 + path: src/Type/Generic/TemplateObjectShapeType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 3 + path: src/Type/Generic/TemplateObjectType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 3 + path: src/Type/Generic/TemplateObjectWithoutClassType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 2 + path: src/Type/Generic/TemplateStrictMixedType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 3 + path: src/Type/Generic/TemplateStringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:isArray\\(\\) or Type\\:\\:getArrays\\(\\) instead\\.$#" + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\BooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isBoolean\\(\\) instead\\.$#" + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\FloatType is error\\-prone and deprecated\\. Use Type\\:\\:isFloat\\(\\) instead\\.$#" + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericObjectType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntegerType is error\\-prone and deprecated\\. Use Type\\:\\:isInteger\\(\\) instead\\.$#" + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectShapeType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) and Type\\:\\:hasProperty\\(\\) instead\\.$#" + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectWithoutClassType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) instead\\.$#" + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\StringType is error\\-prone and deprecated\\. Use Type\\:\\:isString\\(\\) instead\\.$#" + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 3 + path: src/Type/Generic/TemplateUnionType.php + + - + message: "#^Instanceof between PHPStan\\\\Type\\\\Type and PHPStan\\\\Type\\\\UnionType will always evaluate to false\\.$#" + count: 2 + path: src/Type/Generic/TemplateUnionType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 1 + path: src/Type/IntegerRangeType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntegerType is error\\-prone and deprecated\\. Use Type\\:\\:isInteger\\(\\) instead\\.$#" + count: 3 + path: src/Type/IntegerRangeType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/IntegerRangeType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntegerType is error\\-prone and deprecated\\. Use Type\\:\\:isInteger\\(\\) instead\\.$#" + count: 2 + path: src/Type/IntegerType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Accessory\\\\AccessoryType is error\\-prone and deprecated\\. Use methods on PHPStan\\\\Type\\\\Type instead\\.$#" + count: 3 + path: src/Type/IntersectionType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\BooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isBoolean\\(\\) instead\\.$#" + count: 1 + path: src/Type/IntersectionType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\CallableType is error\\-prone and deprecated\\. Use Type\\:\\:isCallable\\(\\) and Type\\:\\:getCallableParametersAcceptors\\(\\) instead\\.$#" + count: 2 + path: src/Type/IntersectionType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 4 + path: src/Type/IntersectionType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 2 + path: src/Type/IterableType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IterableType is error\\-prone and deprecated\\. Use Type\\:\\:isIterable\\(\\) instead\\.$#" + count: 2 + path: src/Type/IterableType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 3 + path: src/Type/NullType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\NullType is error\\-prone and deprecated\\. Use Type\\:\\:isNull\\(\\) instead\\.$#" + count: 3 + path: src/Type/NullType.php + + - + message: """ + #^Call to deprecated method getInstance\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: + Use PHPStan\\\\Reflection\\\\ReflectionProviderStaticAccessor instead$# + """ + count: 2 + path: src/Type/ObjectShapeType.php + + - + message: """ + #^Call to deprecated method getUniversalObjectCratesClasses\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: + Inject %%universalObjectCratesClasses%% parameter instead\\.$# + """ + count: 2 + path: src/Type/ObjectShapeType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/ObjectShapeType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectShapeType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) and Type\\:\\:hasProperty\\(\\) instead\\.$#" + count: 2 + path: src/Type/ObjectShapeType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectWithoutClassType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) instead\\.$#" + count: 1 + path: src/Type/ObjectShapeType.php + + - + message: """ + #^Call to deprecated method getInstance\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: + Use PHPStan\\\\Reflection\\\\ReflectionProviderStaticAccessor instead$# + """ + count: 1 + path: src/Type/ObjectType.php + + - + message: """ + #^Call to deprecated method getUniversalObjectCratesClasses\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: + Inject %%universalObjectCratesClasses%% parameter instead\\.$# + """ + count: 1 + path: src/Type/ObjectType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Enum\\\\EnumCaseObjectType is error\\-prone and deprecated\\. Use Type\\:\\:getEnumCases\\(\\) instead\\.$#" + count: 1 + path: src/Type/ObjectType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericObjectType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/ObjectType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + count: 6 + path: src/Type/ObjectType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectWithoutClassType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) instead\\.$#" + count: 3 + path: src/Type/ObjectType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectShapeType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) and Type\\:\\:hasProperty\\(\\) instead\\.$#" + count: 2 + path: src/Type/ObjectWithoutClassType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectWithoutClassType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) instead\\.$#" + count: 4 + path: src/Type/ObjectWithoutClassType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + count: 2 + path: src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + count: 4 + path: src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 16 + path: src/Type/Php/BcMathStringOrNullReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/CompactFunctionReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/CompactFunctionReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/DefineConstantTypeSpecifyingExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/DefinedConstantTypeSpecifyingExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\TypeWithClassName is error\\-prone and deprecated\\. Use Type\\:\\:getObjectClassNames\\(\\) or Type\\:\\:getObjectClassReflections\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/DsMapDynamicReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 2 + path: src/Type/Php/FilterFunctionReturnTypeHelper.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/FilterFunctionReturnTypeHelper.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/ImplodeFunctionReturnTypeExtension.php + + - + message: """ + #^Call to deprecated method getConstantScalars\\(\\) of class PHPStan\\\\Type\\\\TypeUtils\\: + Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\)$# + """ + count: 2 + path: src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php + + - + message: """ + #^Call to deprecated method getEnumCaseObjects\\(\\) of class PHPStan\\\\Type\\\\TypeUtils\\: + Use Type\\:\\:getEnumCases\\(\\)$# + """ + count: 2 + path: src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/IsAFunctionTypeSpecifyingExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) and Type\\:\\:getClassStringObjectType\\(\\) instead\\.$#" + count: 2 + path: src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php + + - + message: """ + #^Call to deprecated method getTypeFromValue\\(\\) of class PHPStan\\\\Type\\\\ConstantTypeHelper\\: + Use PHPStan\\\\Reflection\\\\InitializerExprTypeResolver$# + """ + count: 1 + path: src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 2 + path: src/Type/Php/LtrimFunctionReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/MbStrlenFunctionReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/MethodExistsTypeSpecifyingExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 4 + path: src/Type/Php/MinMaxFunctionReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantValue\\(\\) or Type\\:\\:generalize\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/MinMaxFunctionReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + count: 2 + path: src/Type/Php/MinMaxFunctionReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 2 + path: src/Type/Php/PropertyExistsTypeSpecifyingExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 2 + path: src/Type/Php/RangeFunctionReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) and Type\\:\\:getClassStringObjectType\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php + + - + message: "#^Cannot access offset int\\<0, max\\> on \\(float\\|int\\)\\.$#" + count: 2 + path: src/Type/Php/StrIncrementDecrementFunctionReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/StrRepeatFunctionReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/Php/StrlenFunctionReturnTypeExtension.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + count: 2 + path: src/Type/StaticType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectWithoutClassType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) instead\\.$#" + count: 1 + path: src/Type/StaticType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 1 + path: src/Type/StringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\StringType is error\\-prone and deprecated\\. Use Type\\:\\:isString\\(\\) instead\\.$#" + count: 2 + path: src/Type/StringType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Accessory\\\\AccessoryType is error\\-prone and deprecated\\. Use methods on PHPStan\\\\Type\\\\Type instead\\.$#" + count: 1 + path: src/Type/TypeCombinator.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:isArray\\(\\) or Type\\:\\:getArrays\\(\\) instead\\.$#" + count: 5 + path: src/Type/TypeCombinator.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\BooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isBoolean\\(\\) instead\\.$#" + count: 1 + path: src/Type/TypeCombinator.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\CallableType is error\\-prone and deprecated\\. Use Type\\:\\:isCallable\\(\\) and Type\\:\\:getCallableParametersAcceptors\\(\\) instead\\.$#" + count: 1 + path: src/Type/TypeCombinator.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) instead\\.$#" + count: 1 + path: src/Type/TypeCombinator.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 1 + path: src/Type/TypeCombinator.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + count: 10 + path: src/Type/TypeCombinator.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 4 + path: src/Type/TypeCombinator.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Enum\\\\EnumCaseObjectType is error\\-prone and deprecated\\. Use Type\\:\\:getEnumCases\\(\\) instead\\.$#" + count: 1 + path: src/Type/TypeCombinator.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\FloatType is error\\-prone and deprecated\\. Use Type\\:\\:isFloat\\(\\) instead\\.$#" + count: 1 + path: src/Type/TypeCombinator.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) and Type\\:\\:getClassStringObjectType\\(\\) instead\\.$#" + count: 2 + path: src/Type/TypeCombinator.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntegerType is error\\-prone and deprecated\\. Use Type\\:\\:isInteger\\(\\) instead\\.$#" + count: 1 + path: src/Type/TypeCombinator.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 3 + path: src/Type/TypeCombinator.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IterableType is error\\-prone and deprecated\\. Use Type\\:\\:isIterable\\(\\) instead\\.$#" + count: 8 + path: src/Type/TypeCombinator.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\NullType is error\\-prone and deprecated\\. Use Type\\:\\:isNull\\(\\) instead\\.$#" + count: 2 + path: src/Type/TypeCombinator.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectShapeType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) and Type\\:\\:hasProperty\\(\\) instead\\.$#" + count: 2 + path: src/Type/TypeCombinator.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\StringType is error\\-prone and deprecated\\. Use Type\\:\\:isString\\(\\) instead\\.$#" + count: 1 + path: src/Type/TypeCombinator.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:isArray\\(\\) or Type\\:\\:getArrays\\(\\) instead\\.$#" + count: 3 + path: src/Type/TypeUtils.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + count: 5 + path: src/Type/TypeUtils.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 5 + path: src/Type/TypeUtils.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\TypeWithClassName is error\\-prone and deprecated\\. Use Type\\:\\:getObjectClassNames\\(\\) or Type\\:\\:getObjectClassReflections\\(\\) instead\\.$#" + count: 1 + path: src/Type/TypeUtils.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:isArray\\(\\) or Type\\:\\:getArrays\\(\\) instead\\.$#" + count: 3 + path: src/Type/TypehintHelper.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IterableType is error\\-prone and deprecated\\. Use Type\\:\\:isIterable\\(\\) instead\\.$#" + count: 1 + path: src/Type/TypehintHelper.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\CallableType is error\\-prone and deprecated\\. Use Type\\:\\:isCallable\\(\\) and Type\\:\\:getCallableParametersAcceptors\\(\\) instead\\.$#" + count: 2 + path: src/Type/UnionType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) and Type\\:\\:getClassStringObjectType\\(\\) instead\\.$#" + count: 1 + path: src/Type/UnionType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 2 + path: src/Type/UnionType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IterableType is error\\-prone and deprecated\\. Use Type\\:\\:isIterable\\(\\) instead\\.$#" + count: 1 + path: src/Type/UnionType.php + + - + message: "#^PHPDoc tag @var assumes the expression with type PHPStan\\\\Type\\\\Type is always PHPStan\\\\Type\\\\BooleanType but it's error\\-prone and dangerous\\.$#" + count: 1 + path: src/Type/UnionType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Accessory\\\\AccessoryType is error\\-prone and deprecated\\. Use methods on PHPStan\\\\Type\\\\Type instead\\.$#" + count: 3 + path: src/Type/UnionTypeHelper.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#" + count: 4 + path: src/Type/UnionTypeHelper.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#" + count: 2 + path: src/Type/UnionTypeHelper.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + count: 2 + path: src/Type/UnionTypeHelper.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntegerType is error\\-prone and deprecated\\. Use Type\\:\\:isInteger\\(\\) instead\\.$#" + count: 2 + path: src/Type/UnionTypeHelper.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\NullType is error\\-prone and deprecated\\. Use Type\\:\\:isNull\\(\\) instead\\.$#" + count: 2 + path: src/Type/UnionTypeHelper.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\VoidType is error\\-prone and deprecated\\. Use Type\\:\\:isVoid\\(\\) instead\\.$#" + count: 2 + path: src/Type/VoidType.php + + - + message: "#^Unreachable statement \\- code above always terminates\\.$#" + count: 1 + path: tests/PHPStan/Analyser/AnalyserTest.php + + - + message: "#^Class PHPStan\\\\Analyser\\\\AnonymousClassNameRuleTest extends generic class PHPStan\\\\Testing\\\\RuleTestCase but does not specify its types\\: TRule$#" + count: 1 + path: tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php + + - + message: "#^Method PHPStan\\\\Analyser\\\\AnonymousClassNameRuleTest\\:\\:getRule\\(\\) return type with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + count: 1 + path: tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php + + - + message: "#^Class PHPStan\\\\Analyser\\\\EvaluationOrderTest extends generic class PHPStan\\\\Testing\\\\RuleTestCase but does not specify its types\\: TRule$#" + count: 1 + path: tests/PHPStan/Analyser/EvaluationOrderTest.php + + - + message: "#^Method PHPStan\\\\Analyser\\\\EvaluationOrderTest\\:\\:getRule\\(\\) return type with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + count: 1 + path: tests/PHPStan/Analyser/EvaluationOrderTest.php + + - + message: "#^Constant SOME_CONSTANT_IN_AUTOLOAD_FILE not found\\.$#" + count: 1 + path: tests/PHPStan/Command/AnalyseCommandTest.php + + - + message: "#^Class PHPStan\\\\Node\\\\FileNodeTest extends generic class PHPStan\\\\Testing\\\\RuleTestCase but does not specify its types\\: TRule$#" + count: 1 + path: tests/PHPStan/Node/FileNodeTest.php + + - + message: "#^Method PHPStan\\\\Node\\\\FileNodeTest\\:\\:getRule\\(\\) return type with generic interface PHPStan\\\\Rules\\\\Rule does not specify its types\\: TNodeType$#" + count: 1 + path: tests/PHPStan/Node/FileNodeTest.php + + - + message: "#^PHPDoc tag @var with type string is not subtype of type class\\-string\\.$#" + count: 1 + path: tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php + + - + message: "#^Creating new PHPStan\\\\Php8StubsMap is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + count: 1 + path: tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php + + - + message: "#^Creating new PHPStan\\\\Php8StubsMap is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + count: 1 + path: tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php + + - + message: """ + #^Instantiation of deprecated class PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayItemTypeRule\\: + Replaced by PHPStan\\\\Rules\\\\Properties\\\\TypesAssignedToPropertiesRule$# + """ + count: 1 + path: tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php + + - + message: """ + #^Return type of method PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayItemTypeRuleTest\\:\\:getRule\\(\\) has typehint with deprecated class PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayItemTypeRule\\: + Replaced by PHPStan\\\\Rules\\\\Properties\\\\TypesAssignedToPropertiesRule$# + """ + count: 1 + path: tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php + + - + message: """ + #^Instantiation of deprecated class PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayKeyTypeRule\\: + Replaced by PHPStan\\\\Rules\\\\Properties\\\\TypesAssignedToPropertiesRule$# + """ + count: 1 + path: tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php - message: """ @@ -468,3 +1824,7 @@ parameters: count: 1 path: tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php + - + message: "#^PHPDoc tag @var assumes the expression with type PHPStan\\\\Type\\\\Generic\\\\TemplateType is always PHPStan\\\\Type\\\\Generic\\\\TemplateMixedType but it's error\\-prone and dangerous\\.$#" + count: 1 + path: tests/PHPStan/Type/IterableTypeTest.php diff --git a/phpstan-baseline.php b/phpstan-baseline.php new file mode 100644 index 0000000000..646cbdbef6 --- /dev/null +++ b/phpstan-baseline.php @@ -0,0 +1,3 @@ + tests/PHPStan + tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php diff --git a/resources/functionMap.php b/resources/functionMap.php index 473b45dc11..23c35ad6a1 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -216,7 +216,7 @@ 'apcu_clear_cache' => ['bool'], 'apcu_dec' => ['int', 'key'=>'string', 'step='=>'int', '&w_success='=>'bool', 'ttl='=>'int'], 'apcu_delete' => ['bool', 'key'=>'string|APCuIterator'], -'apcu_delete\'1' => ['array', 'key'=>'string[]'], +'apcu_delete\'1' => ['list', 'key'=>'string[]'], 'apcu_entry' => ['mixed', 'key'=>'string', 'generator'=>'callable', 'ttl='=>'int'], 'apcu_exists' => ['bool', 'keys'=>'string'], 'apcu_exists\'1' => ['array', 'keys'=>'string[]'], @@ -259,7 +259,7 @@ 'AppendIterator::rewind' => ['void'], 'AppendIterator::valid' => ['bool'], 'array_change_key_case' => ['array', 'input'=>'array', 'case='=>'int'], -'array_chunk' => ['array[]', 'input'=>'array', 'size'=>'positive-int', 'preserve_keys='=>'bool'], +'array_chunk' => ['list', 'input'=>'array', 'size'=>'positive-int', 'preserve_keys='=>'bool'], 'array_column' => ['array', 'array'=>'array', 'column_key'=>'mixed', 'index_key='=>'mixed'], 'array_combine' => ['array|false', 'keys'=>'array', 'values'=>'array'], 'array_count_values' => ['array', 'input'=>'array'], @@ -282,8 +282,8 @@ 'array_intersect_ukey' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'key_compare_func'=>'callable(mixed,mixed):int'], 'array_intersect_ukey\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest'=>'array|callable(mixed,mixed):int'], 'array_key_exists' => ['bool', 'key'=>'string|int', 'search'=>'array'], -'array_key_first' => ['int|string|null', 'array' => 'array'], -'array_key_last' => ['int|string|null', 'array' => 'array'], +'array_key_first' => ['int|string|null', 'array'=>'array'], +'array_key_last' => ['int|string|null', 'array'=>'array'], 'array_keys' => ['list', 'input'=>'array', 'search_value='=>'mixed', 'strict='=>'bool'], 'array_map' => ['array', 'callback'=>'?callable', 'array'=>'array', '...args='=>'array'], 'array_merge' => ['array', 'arr1'=>'array', '...args='=>'array'], @@ -318,7 +318,7 @@ 'array_uintersect_uassoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', 'arg5'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'], 'array_unique' => ['array', 'array'=>'array', 'flags='=>'int'], 'array_unshift' => ['positive-int', '&rw_stack'=>'array', 'var'=>'mixed', '...vars='=>'mixed'], -'array_values' => ['array', 'input'=>'array'], +'array_values' => ['list', 'input'=>'array'], 'array_walk' => ['bool', '&rw_input'=>'array|object', 'callback'=>'callable', 'userdata='=>'mixed'], 'array_walk_recursive' => ['bool', '&rw_input'=>'array|object', 'callback'=>'callable', 'userdata='=>'mixed'], 'ArrayAccess::offsetExists' => ['bool', 'offset'=>'mixed'], @@ -407,7 +407,8 @@ 'BadMethodCallException::getPrevious' => ['(?Throwable)|(?BadMethodCallException)'], 'BadMethodCallException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], 'BadMethodCallException::getTraceAsString' => ['string'], -'base64_decode' => ['string|false', 'str'=>'string', 'strict='=>'bool'], +'base64_decode' => ['string', 'str'=>'string', 'strict='=>'false'], +'base64_decode\'1' => ['string|false', 'str'=>'string', 'strict='=>'true'], 'base64_encode' => ['string', 'str'=>'string'], 'base_convert' => ['string', 'number'=>'string', 'frombase'=>'int', 'tobase'=>'int'], 'basename' => ['string', 'path'=>'string', 'suffix='=>'string'], @@ -932,7 +933,7 @@ 'CallbackFilterIterator::next' => ['void'], 'CallbackFilterIterator::rewind' => ['void'], 'CallbackFilterIterator::valid' => ['bool'], -'ceil' => ['float|false', 'number'=>'float'], +'ceil' => ['__benevolent', 'number'=>'float'], 'chdb::__construct' => ['void', 'pathname'=>'string'], 'chdb::get' => ['string', 'key'=>'string'], 'chdb_create' => ['bool', 'pathname'=>'string', 'data'=>'array'], @@ -948,9 +949,9 @@ 'chunk_split' => ['string', 'str'=>'string', 'chunklen='=>'positive-int', 'ending='=>'string'], 'class_alias' => ['bool', 'user_class_name'=>'string', 'alias_name'=>'string', 'autoload='=>'bool'], 'class_exists' => ['bool', 'classname'=>'string', 'autoload='=>'bool'], -'class_implements' => ['array|false', 'what'=>'object|string', 'autoload='=>'bool'], +'class_implements' => ['array|false', 'what'=>'object|string', 'autoload='=>'bool'], 'class_parents' => ['array|false', 'instance'=>'object|string', 'autoload='=>'bool'], -'class_uses' => ['array|false', 'what'=>'object|string', 'autoload='=>'bool'], +'class_uses' => ['array|false', 'what'=>'object|string', 'autoload='=>'bool'], 'classkit_import' => ['array', 'filename'=>'string'], 'classkit_method_add' => ['bool', 'classname'=>'string', 'methodname'=>'string', 'args'=>'string', 'code'=>'string', 'flags='=>'int'], 'classkit_method_copy' => ['bool', 'dclass'=>'string', 'dmethod'=>'string', 'sclass'=>'string', 'smethod='=>'string'], @@ -1500,7 +1501,7 @@ 'curl_multi_exec' => ['int', 'mh'=>'resource', '&w_still_running'=>'int'], 'curl_multi_getcontent' => ['string', 'ch'=>'resource'], 'curl_multi_info_read' => ['array|false', 'mh'=>'resource', '&w_msgs_in_queue='=>'int'], -'curl_multi_init' => ['resource|false'], +'curl_multi_init' => ['resource'], 'curl_multi_remove_handle' => ['int', 'mh'=>'resource', 'ch'=>'resource'], 'curl_multi_select' => ['int', 'mh'=>'resource', 'timeout='=>'float'], 'curl_multi_setopt' => ['bool', 'mh'=>'resource', 'option'=>'int', 'value'=>'mixed'], @@ -1533,16 +1534,16 @@ 'cyrus_unbind' => ['bool', 'connection'=>'resource', 'trigger_name'=>'string'], 'date' => ['string', 'format'=>'string', 'timestamp='=>'int'], 'date_add' => ['DateTime|false', 'object'=>'', 'interval'=>''], -'date_create' => ['DateTime|false', 'time='=>'string|null', 'timezone='=>'?\DateTimeZone'], -'date_create_from_format' => ['DateTime|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?\DateTimeZone'], -'date_create_immutable' => ['DateTimeImmutable|false', 'time='=>'string', 'timezone='=>'?\DateTimeZone'], -'date_create_immutable_from_format' => ['DateTimeImmutable|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?\DateTimeZone'], +'date_create' => ['DateTime|false', 'time='=>'string|null', 'timezone='=>'?DateTimeZone'], +'date_create_from_format' => ['DateTime|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?DateTimeZone'], +'date_create_immutable' => ['DateTimeImmutable|false', 'time='=>'string', 'timezone='=>'?DateTimeZone'], +'date_create_immutable_from_format' => ['DateTimeImmutable|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?DateTimeZone'], 'date_date_set' => ['DateTime|false', 'object'=>'', 'year'=>'', 'month'=>'', 'day'=>''], 'date_default_timezone_get' => ['string'], 'date_default_timezone_set' => ['bool', 'timezone_identifier'=>'string'], 'date_diff' => ['DateInterval', 'obj1'=>'DateTimeInterface', 'obj2'=>'DateTimeInterface', 'absolute='=>'bool'], 'date_format' => ['string', 'obj'=>'DateTimeInterface', 'format'=>'string'], -'date_get_last_errors' => ['array{warning_count: int, warnings: array, error_count: int, errors: array}|false'], +'date_get_last_errors' => ['array{warning_count: 0|positive-int, warnings: list, error_count: 0|positive-int, errors: list}|false'], 'date_interval_create_from_date_string' => ['DateInterval|false', 'time'=>'string'], 'date_interval_format' => ['string', 'object'=>'DateInterval', 'format'=>'string'], 'date_isodate_set' => ['DateTime|false', 'object'=>'DateTime', 'year'=>'int', 'week'=>'int', 'day='=>'int|mixed'], @@ -1551,7 +1552,7 @@ 'date_parse' => ['array{year: int|false, month: int|false, day: int|false, hour: int|false, minute: int|false, second: int|false, fraction: float|false, warning_count: int, warnings: string[], error_count: int, errors: string[], is_localtime: bool, zone_type?: int|bool, zone?: int|bool, is_dst?: bool, tz_abbr?: string, tz_id?: string, relative?: array{year: int, month: int, day: int, hour: int, minute: int, second: int, weekday?: int, weekdays?: int, first_day_of_month?: bool, last_day_of_month?: bool}}', 'date'=>'string'], 'date_parse_from_format' => ['array{year: int|false, month: int|false, day: int|false, hour: int|false, minute: int|false, second: int|false, fraction: float|false, warning_count: int, warnings: string[], error_count: int, errors: string[], is_localtime: bool, zone_type?: int|bool, zone?: int|bool, is_dst?: bool, tz_abbr?: string, tz_id?: string, relative?: array{year: int, month: int, day: int, hour: int, minute: int, second: int, weekday?: int, weekdays?: int, first_day_of_month?: bool, last_day_of_month?: bool}}', 'format'=>'string', 'date'=>'string'], 'date_sub' => ['DateTime|false', 'object'=>'DateTime', 'interval'=>'DateInterval'], -'date_sun_info' => ['array', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], +'date_sun_info' => ['__benevolent', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], 'date_sunrise' => ['mixed', 'time'=>'int', 'format='=>'int', 'latitude='=>'float', 'longitude='=>'float', 'zenith='=>'float', 'gmt_offset='=>'float'], 'date_sunset' => ['mixed', 'time'=>'int', 'format='=>'int', 'latitude='=>'float', 'longitude='=>'float', 'zenith='=>'float', 'gmt_offset='=>'float'], 'date_time_set' => ['DateTime|false', 'object'=>'', 'hour'=>'', 'minute'=>'', 'second='=>'', 'microseconds='=>''], @@ -1583,7 +1584,7 @@ 'DateInterval::__construct' => ['void', 'spec'=>'string'], 'DateInterval::__set_state' => ['DateInterval', 'array'=>'array'], 'DateInterval::__wakeup' => ['void'], -'DateInterval::createFromDateString' => ['DateInterval', 'time'=>'string'], +'DateInterval::createFromDateString' => ['DateInterval|false', 'time'=>'string'], 'DateInterval::format' => ['string', 'format'=>'string'], 'DatePeriod::__construct' => ['void', 'start'=>'DateTimeInterface', 'interval'=>'DateInterval', 'recur'=>'int', 'options='=>'int'], 'DatePeriod::__construct\'1' => ['void', 'start'=>'DateTimeInterface', 'interval'=>'DateInterval', 'end'=>'DateTimeInterface', 'options='=>'int'], @@ -1600,7 +1601,7 @@ 'DateTime::createFromImmutable' => ['static', 'object'=>'DateTimeImmutable'], 'DateTime::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTime::format' => ['string', 'format'=>'string'], -'DateTime::getLastErrors' => ['array{warning_count: int, warnings: array, error_count: int, errors: array}|false'], +'DateTime::getLastErrors' => ['array{warning_count: 0|positive-int, warnings: list, error_count: 0|positive-int, errors: list}|false'], 'DateTime::getOffset' => ['int'], 'DateTime::getTimestamp' => ['int'], 'DateTime::getTimezone' => ['DateTimeZone'], @@ -1619,7 +1620,7 @@ 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeImmutable::format' => ['string', 'format'=>'string'], -'DateTimeImmutable::getLastErrors' => ['array{warning_count: int, warnings: array, error_count: int, errors: array}|false'], +'DateTimeImmutable::getLastErrors' => ['array{warning_count: 0|positive-int, warnings: list, error_count: 0|positive-int, errors: list}|false'], 'DateTimeImmutable::getOffset' => ['int'], 'DateTimeImmutable::getTimestamp' => ['int'], 'DateTimeImmutable::getTimezone' => ['DateTimeZone'], @@ -1641,12 +1642,12 @@ 'DateTimeZone::getLocation' => ['array{country_code: string, latitude: float, longitude: float, comments: string}|false'], 'DateTimeZone::getName' => ['string'], 'DateTimeZone::getOffset' => ['int', 'datetime'=>'DateTimeInterface'], -'DateTimeZone::getTransitions' => ['array', 'timestamp_begin='=>'int', 'timestamp_end='=>'int'], -'DateTimeZone::listAbbreviations' => ['array'], -'DateTimeZone::listIdentifiers' => ['array', 'what='=>'int', 'country='=>'string'], -'db2_autocommit' => ['mixed', 'connection'=>'resource', 'value='=>'int'], +'DateTimeZone::getTransitions' => ['list', 'timestamp_begin='=>'int', 'timestamp_end='=>'int'], +'DateTimeZone::listAbbreviations' => ['array>'], +'DateTimeZone::listIdentifiers' => ['list', 'what='=>'int', 'country='=>'string'], +'db2_autocommit' => ['DB2_AUTOCOMMIT_OFF|DB2_AUTOCOMMIT_ON|bool', 'connection'=>'resource', 'value='=>'DB2_AUTOCOMMIT_OFF|DB2_AUTOCOMMIT_ON'], 'db2_bind_param' => ['bool', 'stmt'=>'resource', 'parameter_number'=>'int', 'variable_name'=>'string', 'parameter_type='=>'int', 'data_type='=>'int', 'precision='=>'int', 'scale='=>'int'], -'db2_client_info' => ['object|false', 'connection'=>'resource'], +'db2_client_info' => ['stdClass|false', 'connection'=>'resource'], 'db2_close' => ['bool', 'connection'=>'resource'], 'db2_column_privileges' => ['resource|false', 'connection'=>'resource', 'qualifier='=>'string', 'schema='=>'string', 'table_name='=>'string', 'column_name='=>'string'], 'db2_columns' => ['resource|false', 'connection'=>'resource', 'qualifier='=>'string', 'schema='=>'string', 'table_name='=>'string', 'column_name='=>'string'], @@ -1661,7 +1662,7 @@ 'db2_fetch_array' => ['array|false', 'stmt'=>'resource', 'row_number='=>'int'], 'db2_fetch_assoc' => ['array|false', 'stmt'=>'resource', 'row_number='=>'int'], 'db2_fetch_both' => ['array|false', 'stmt'=>'resource', 'row_number='=>'int'], -'db2_fetch_object' => ['object|false', 'stmt'=>'resource', 'row_number='=>'int'], +'db2_fetch_object' => ['stdClass|false', 'stmt'=>'resource', 'row_number='=>'int'], 'db2_fetch_row' => ['bool', 'stmt'=>'resource', 'row_number='=>'int'], 'db2_field_display_size' => ['int|false', 'stmt'=>'resource', 'column'=>'mixed'], 'db2_field_name' => ['string|false', 'stmt'=>'resource', 'column'=>'mixed'], @@ -1678,7 +1679,7 @@ 'db2_lob_read' => ['string|false', 'stmt'=>'resource', 'colnum'=>'int', 'length'=>'int'], 'db2_next_result' => ['resource|false', 'stmt'=>'resource'], 'db2_num_fields' => ['0|positive-int|false', 'stmt'=>'resource'], -'db2_num_rows' => ['0|positive-int', 'stmt'=>'resource'], +'db2_num_rows' => ['0|positive-int|false', 'stmt'=>'resource'], 'db2_pclose' => ['bool', 'resource'=>'resource'], 'db2_pconnect' => ['resource|false', 'database'=>'string', 'username'=>'string', 'password'=>'string', 'options='=>'array'], 'db2_prepare' => ['resource|false', 'connection'=>'resource', 'statement'=>'string', 'options='=>'array'], @@ -1689,7 +1690,7 @@ 'db2_procedures' => ['resource|false', 'connection'=>'resource', 'qualifier'=>'string', 'schema'=>'string', 'procedure'=>'string'], 'db2_result' => ['mixed', 'stmt'=>'resource', 'column'=>'mixed'], 'db2_rollback' => ['bool', 'connection'=>'resource'], -'db2_server_info' => ['object|false', 'connection'=>'resource'], +'db2_server_info' => ['stdClass|false', 'connection'=>'resource'], 'db2_set_option' => ['bool', 'resource'=>'resource', 'options'=>'array', 'type'=>'int'], 'db2_setoption' => [''], 'db2_special_columns' => ['resource|false', 'connection'=>'resource', 'qualifier'=>'string', 'schema'=>'string', 'table_name'=>'string', 'scope'=>'int'], @@ -1863,7 +1864,7 @@ 'dngettext' => ['string', 'domain'=>'string', 'msgid1'=>'string', 'msgid2'=>'string', 'count'=>'int'], 'dns_check_record' => ['bool', 'host'=>'string', 'type='=>'string'], 'dns_get_mx' => ['bool', 'hostname'=>'string', '&w_mxhosts'=>'array', '&w_weight'=>'array'], -'dns_get_record' => ['array|false', 'hostname'=>'string', 'type='=>'int', '&w_authns='=>'array', '&w_addtl='=>'array', 'raw='=>'bool'], +'dns_get_record' => ['list|false', 'hostname'=>'string', 'type='=>'int', '&w_authns='=>'array', '&w_addtl='=>'array', 'raw='=>'bool'], 'dom_document_relaxNG_validate_file' => ['bool', 'filename'=>'string'], 'dom_document_relaxNG_validate_xml' => ['bool', 'source'=>'string'], 'dom_document_schema_validate' => ['bool', 'source'=>'string', 'flags'=>'int'], @@ -1913,10 +1914,10 @@ 'DOMDocument::getElementsByTagName' => ['DOMNodeList', 'name'=>'string'], 'DOMDocument::getElementsByTagNameNS' => ['DOMNodeList', 'namespaceuri'=>'string', 'localname'=>'string'], 'DOMDocument::importNode' => ['DOMNode', 'importednode'=>'DOMNode', 'deep='=>'bool'], -'DOMDocument::load' => ['mixed', 'filename'=>'string', 'options='=>'int'], +'DOMDocument::load' => ['bool', 'filename'=>'string', 'options='=>'int'], 'DOMDocument::loadHTML' => ['bool', 'source'=>'string', 'options='=>'int'], 'DOMDocument::loadHTMLFile' => ['bool', 'filename'=>'string', 'options='=>'int'], -'DOMDocument::loadXML' => ['mixed', 'source'=>'string', 'options='=>'int'], +'DOMDocument::loadXML' => ['bool', 'source'=>'string', 'options='=>'int'], 'DOMDocument::normalizeDocument' => ['void'], 'DOMDocument::registerNodeClass' => ['bool', 'baseclass'=>'string', 'extendedclass'=>'string'], 'DOMDocument::relaxNGValidate' => ['bool', 'filename'=>'string'], @@ -1987,7 +1988,7 @@ 'DOMNode::isDefaultNamespace' => ['bool', 'namespaceuri'=>'string'], 'DOMNode::isSameNode' => ['bool', 'node'=>'DOMNode'], 'DOMNode::isSupported' => ['bool', 'feature'=>'string', 'version'=>'string'], -'DOMNode::lookupNamespaceURI' => ['string', 'prefix'=>'string'], +'DOMNode::lookupNamespaceURI' => ['?string', 'prefix'=>'?string'], 'DOMNode::lookupPrefix' => ['string', 'namespaceuri'=>'string'], 'DOMNode::normalize' => ['void'], 'DOMNode::removeChild' => ['DOMNode', 'oldnode'=>'DOMNode'], @@ -2020,7 +2021,7 @@ 'DomXsltStylesheet::result_dump_mem' => ['string', 'xmldoc'=>'DOMDocument'], 'DOTNET::__construct' => ['void', 'assembly_name'=>'string', 'class_name'=>'string', 'codepage='=>'int'], 'dotnet_load' => ['int', 'assembly_name'=>'string', 'datatype_name='=>'string', 'codepage='=>'int'], -'doubleval' => ['float', 'var'=>'mixed'], +'doubleval' => ['float', 'var'=>'scalar|array|resource|null'], 'Ds\Collection::clear' => ['void'], 'Ds\Collection::copy' => ['Ds\Collection'], 'Ds\Collection::isEmpty' => ['bool'], @@ -2627,7 +2628,7 @@ 'Exception::getPrevious' => ['(?Throwable)|(?Exception)'], 'Exception::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], 'Exception::getTraceAsString' => ['string'], -'exec' => ['string', 'command'=>'string', '&w_output='=>'array', '&w_return_value='=>'int'], +'exec' => ['string|false', 'command'=>'string', '&w_output='=>'array', '&w_return_value='=>'int'], 'exif_imagetype' => ['int|false', 'imagefile'=>'string'], 'exif_read_data' => ['array|false', 'filename'=>'string|resource', 'sections_needed='=>'string', 'sub_arrays='=>'bool', 'read_thumbnail='=>'bool'], 'exif_tagname' => ['string|false', 'index'=>'int'], @@ -2635,10 +2636,10 @@ 'exp' => ['float', 'number'=>'float'], 'expect_expectl' => ['int', 'expect'=>'resource', 'cases'=>'array', 'match='=>'array'], 'expect_popen' => ['resource|false', 'command'=>'string'], -'explode' => ['non-empty-array|false', 'separator'=>'string', 'str'=>'string', 'limit='=>'int'], +'explode' => ['list|false', 'separator'=>'string', 'str'=>'string', 'limit='=>'int'], 'expm1' => ['float', 'number'=>'float'], 'extension_loaded' => ['bool', 'extension_name'=>'string'], -'extract' => ['int', '&rw_var_array'=>'array', 'extract_type='=>'int', 'prefix='=>'string|null'], +'extract' => ['0|positive-int', '&rw_var_array'=>'array', 'extract_type='=>'int', 'prefix='=>'string|null'], 'ezmlm_hash' => ['int', 'addr'=>'string'], 'fam_cancel_monitor' => ['bool', 'fam'=>'resource', 'fam_monitor'=>'resource'], 'fam_close' => ['void', 'fam'=>'resource'], @@ -2933,10 +2934,10 @@ 'ffmpeg_movie::hasAudio' => ['bool'], 'ffmpeg_movie::hasVideo' => ['bool'], 'fgetc' => ['string|false', 'fp'=>'resource'], -'fgetcsv' => ['(?array)|(?false)', 'fp'=>'resource', 'length='=>'0|positive-int', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], +'fgetcsv' => ['list|array{0: null}|false|null', 'fp'=>'resource', 'length='=>'0|positive-int', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'fgets' => ['string|false', 'fp'=>'resource', 'length='=>'0|positive-int'], 'fgetss' => ['string|false', 'fp'=>'resource', 'length='=>'0|positive-int', 'allowable_tags='=>'string'], -'file' => ['array|false', 'filename'=>'string', 'flags='=>'int', 'context='=>'resource'], +'file' => ['list|false', 'filename'=>'string', 'flags='=>'int', 'context='=>'resource'], 'file_exists' => ['bool', 'filename'=>'string'], 'file_get_contents' => ['string|false', 'filename'=>'string', 'use_include_path='=>'bool', 'context='=>'?resource', 'offset='=>'int', 'maxlen='=>'0|positive-int'], 'file_put_contents' => ['0|positive-int|false', 'file'=>'string', 'data'=>'mixed', 'flags='=>'int', 'context='=>'?resource'], @@ -2967,7 +2968,7 @@ 'filter_id' => ['int|false', 'filtername'=>'string'], 'filter_input' => ['mixed', 'type'=>'int', 'variable_name'=>'string', 'filter='=>'int', 'options='=>'array|int'], 'filter_input_array' => ['array|false|null', 'type'=>'int', 'definition='=>'int|array', 'add_empty='=>'bool'], -'filter_list' => ['array'], +'filter_list' => ['non-empty-list'], 'filter_var' => ['mixed', 'variable'=>'mixed', 'filter='=>'int', 'options='=>'mixed'], 'filter_var_array' => ['array|false|null', 'data'=>'array', 'definition='=>'mixed', 'add_empty='=>'bool'], 'FilterIterator::__construct' => ['void', 'iterator'=>'Iterator'], @@ -2988,9 +2989,9 @@ 'finfo_file' => ['string|false', 'finfo'=>'resource', 'file_name'=>'string', 'options='=>'int', 'context='=>'resource'], 'finfo_open' => ['resource|false', 'options='=>'int', 'arg='=>'string'], 'finfo_set_flags' => ['bool', 'finfo'=>'resource', 'options'=>'int'], -'floatval' => ['float', 'var'=>'mixed'], +'floatval' => ['float', 'var'=>'scalar|array|resource|null'], 'flock' => ['bool', 'fp'=>'resource', 'operation'=>'int', '&w_wouldblock='=>'int'], -'floor' => ['float|false', 'number'=>'float'], +'floor' => ['__benevolent', 'number'=>'float'], 'flush' => ['void'], 'fmod' => ['float', 'x'=>'float', 'y'=>'float'], 'fnmatch' => ['bool', 'pattern'=>'string', 'filename'=>'string', 'flags='=>'int'], @@ -2999,13 +3000,13 @@ 'forward_static_call_array' => ['mixed', 'function'=>'callable', 'parameters'=>'array'], 'fpassthru' => ['0|positive-int|false', 'fp'=>'resource'], 'fpm_get_status' => ['array{pool: string, process-manager: \'dynamic\'|\'ondemand\'|\'static\', start-time: int<0, max>, start-since: int<0, max>, accepted-conn: int<0, max>, listen-queue: int<0, max>, max-listen-queue: int<0, max>, listen-queue-len: int<0, max>, idle-processes: int<0, max>, active-processes: int<1, max>, total-processes: int<1, max>, max-active-processes: int<1, max>, max-children-reached: 0|1, slow-requests: int<0, max>, procs: array, state: \'Idle\'|\'Running\', start-time: int<0, max>, start-since: int<0, max>, requests: int<0, max>, request-duration: int<0, max>, request-method: string, request-uri: string, query-string: string, request-length: int<0, max>, user: string, script: string, last-request-cpu: float, last-request-memory: int<0, max>}>}|false'], -'fprintf' => ['int', 'stream'=>'resource', 'format'=>'string', '...values='=>'string|int|float'], -'fputcsv' => ['0|positive-int|false', 'fp'=>'resource', 'fields'=>'array', 'delimiter='=>'string', 'enclosure='=>'string', 'escape_char='=>'string'], +'fprintf' => ['int', 'stream'=>'resource', 'format'=>'string', '...values='=>'__stringAndStringable|int|float|null|bool'], +'fputcsv' => ['0|positive-int|false', 'fp'=>'resource', 'fields'=>'array', 'delimiter='=>'string', 'enclosure='=>'string', 'escape_char='=>'string'], 'fputs' => ['0|positive-int|false', 'fp'=>'resource', 'str'=>'string', 'length='=>'0|positive-int'], 'fread' => ['string|false', 'fp'=>'resource', 'length'=>'0|positive-int'], 'frenchtojd' => ['int', 'month'=>'int', 'day'=>'int', 'year'=>'int'], 'fribidi_log2vis' => ['string', 'str'=>'string', 'direction'=>'string', 'charset'=>'int'], -'fscanf' => ['array|int|false', 'stream'=>'resource', 'format'=>'string', '&...w_vars='=>'string|int|float|null'], +'fscanf' => ['list|int|false', 'stream'=>'resource', 'format'=>'string', '&...w_vars='=>'string|int|float|null'], 'fseek' => ['0|-1', 'fp'=>'resource', 'offset'=>'int', 'whence='=>'int'], 'fsockopen' => ['resource|false', 'hostname'=>'string', 'port='=>'int', '&w_errno='=>'int', '&w_errstr='=>'string', 'timeout='=>'float'], 'fstat' => ['array|false', 'fp'=>'resource'], @@ -3029,13 +3030,13 @@ 'ftp_mkdir' => ['string|false', 'stream'=>'resource', 'directory'=>'string'], 'ftp_mlsd' => ['array|false', 'ftp_stream'=>'resource', 'directory'=>'string'], 'ftp_nb_continue' => ['int', 'stream'=>'resource'], -'ftp_nb_fget' => ['int', 'stream'=>'resource', 'fp'=>'resource', 'remote_file'=>'string', 'mode'=>'int', 'resumepos='=>'int'], -'ftp_nb_fput' => ['int', 'stream'=>'resource', 'remote_file'=>'string', 'fp'=>'resource', 'mode'=>'int', 'startpos='=>'int'], -'ftp_nb_get' => ['int', 'stream'=>'resource', 'local_file'=>'string', 'remote_file'=>'string', 'mode'=>'int', 'resume_pos='=>'int'], -'ftp_nb_put' => ['int|false', 'stream'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode'=>'int', 'startpos='=>'int'], +'ftp_nb_fget' => ['int', 'stream'=>'resource', 'fp'=>'resource', 'remote_file'=>'string', 'mode='=>'int', 'resumepos='=>'int'], +'ftp_nb_fput' => ['int', 'stream'=>'resource', 'remote_file'=>'string', 'fp'=>'resource', 'mode='=>'int', 'startpos='=>'int'], +'ftp_nb_get' => ['int|false', 'stream'=>'resource', 'local_file'=>'string', 'remote_file'=>'string', 'mode='=>'int', 'resume_pos='=>'int'], +'ftp_nb_put' => ['int|false', 'stream'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode='=>'int', 'startpos='=>'int'], 'ftp_nlist' => ['array|false', 'stream'=>'resource', 'directory'=>'string'], 'ftp_pasv' => ['bool', 'stream'=>'resource', 'pasv'=>'bool'], -'ftp_put' => ['bool', 'stream'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode'=>'int', 'startpos='=>'int'], +'ftp_put' => ['bool', 'stream'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode='=>'int', 'startpos='=>'int'], 'ftp_pwd' => ['string|false', 'stream'=>'resource'], 'ftp_raw' => ['array', 'stream'=>'resource', 'command'=>'string'], 'ftp_rawlist' => ['array|false', 'stream'=>'resource', 'directory'=>'string', 'recursive='=>'bool'], @@ -3048,7 +3049,7 @@ 'ftp_systype' => ['string|false', 'stream'=>'resource'], 'ftruncate' => ['bool', 'fp'=>'resource', 'size'=>'0|positive-int'], 'func_get_arg' => ['mixed', 'arg_num'=>'0|positive-int'], -'func_get_args' => ['array'], +'func_get_args' => ['list'], 'func_num_args' => ['0|positive-int'], 'function_exists' => ['bool', 'function_name'=>'string'], 'fwrite' => ['0|positive-int|false', 'fp'=>'resource', 'str'=>'string', 'length='=>'0|positive-int'], @@ -3298,19 +3299,19 @@ 'get_called_class' => ['class-string'], 'get_cfg_var' => ['mixed', 'option_name'=>'string'], 'get_class' => ['class-string', 'object='=>'object'], -'get_class_methods' => ['array', 'class'=>'mixed'], +'get_class_methods' => ['list', 'class'=>'mixed'], 'get_class_vars' => ['array', 'class_name'=>'string'], 'get_current_user' => ['string'], -'get_declared_classes' => ['array'], -'get_declared_interfaces' => ['array'], -'get_declared_traits' => ['array'], +'get_declared_classes' => ['list'], +'get_declared_interfaces' => ['list'], +'get_declared_traits' => ['list'], 'get_defined_constants' => ['array', 'categorize='=>'bool'], -'get_defined_functions' => ['array>', 'exclude_disabled='=>'bool'], +'get_defined_functions' => ['array{internal:non-empty-list,user:list}', 'exclude_disabled='=>'bool'], 'get_defined_vars' => ['array'], -'get_extension_funcs' => ['list|false', 'extension_name'=>'string'], +'get_extension_funcs' => ['list|false', 'extension_name'=>'string'], 'get_headers' => ['array|false', 'url'=>'string', 'format='=>'int', 'context='=>'resource'], 'get_html_translation_table' => ['array', 'table='=>'int', 'flags='=>'int', 'encoding='=>'string'], -'get_include_path' => ['string|false'], +'get_include_path' => ['__benevolent'], 'get_included_files' => ['list'], 'get_loaded_extensions' => ['list', 'zend_extensions='=>'bool'], 'get_magic_quotes_gpc' => ['false'], @@ -3328,17 +3329,17 @@ 'getenv\'1' => ['array'], 'gethostbyaddr' => ['string|false', 'ip_address'=>'string'], 'gethostbyname' => ['string', 'hostname'=>'string'], -'gethostbynamel' => ['array|false', 'hostname'=>'string'], +'gethostbynamel' => ['list|false', 'hostname'=>'string'], 'gethostname' => ['string|false'], -'getimagesize' => ['array|false', 'imagefile'=>'string', '&w_info='=>'array'], -'getimagesizefromstring' => ['array|false', 'data'=>'string', '&w_info='=>'array'], +'getimagesize' => ['array{0:int, 1: int, 2: int, 3: string, mime: string, channels?: int, bits?: int}|false', 'imagefile'=>'string', '&w_info='=>'array'], +'getimagesizefromstring' => ['array{0:int, 1: int, 2: int, 3: string, mime: string, channels?: int, bits?: int}|false', 'data'=>'string', '&w_info='=>'array'], 'getlastmod' => ['int|false'], 'getmxrr' => ['bool', 'hostname'=>'string', '&w_mxhosts'=>'array', '&w_weight='=>'array'], 'getmygid' => ['int|false'], 'getmyinode' => ['int|false'], 'getmypid' => ['int|false'], 'getmyuid' => ['int|false'], -'getopt' => ['__benevolent|array|array>|false>', 'options'=>'string', 'longopts='=>'array', '&w_optind='=>'int'], +'getopt' => ['__benevolent|array|array>|false>', 'options'=>'string', 'longopts='=>'array', '&w_optind='=>'int'], 'getprotobyname' => ['int|false', 'name'=>'string'], 'getprotobynumber' => ['string|false', 'proto'=>'int'], 'getrandmax' => ['int'], @@ -3348,7 +3349,7 @@ 'gettext' => ['string', 'msgid'=>'string'], 'gettimeofday' => ['array|float', 'get_as_float='=>'bool'], 'gettype' => ['string', 'var'=>'mixed'], -'glob' => ['array|false', 'pattern'=>'string', 'flags='=>'int'], +'glob' => ['list|false', 'pattern'=>'string', 'flags='=>'int'], 'GlobIterator::__construct' => ['void', 'path'=>'string', 'flags='=>'int'], 'GlobIterator::cont' => ['int'], 'GlobIterator::count' => ['0|positive-int'], @@ -3911,14 +3912,14 @@ 'HaruPage::textOut' => ['bool', 'x'=>'float', 'y'=>'float', 'text'=>'string'], 'HaruPage::textRect' => ['bool', 'left'=>'float', 'top'=>'float', 'right'=>'float', 'bottom'=>'float', 'text'=>'string', 'align='=>'int'], 'hash' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], -'hash_algos' => ['array'], +'hash_algos' => ['non-empty-list'], 'hash_copy' => ['HashContext', 'context'=>'HashContext'], 'hash_equals' => ['bool', 'known_string'=>'string', 'user_string'=>'string'], 'hash_file' => ['non-empty-string|false', 'algo'=>'string', 'filename'=>'string', 'raw_output='=>'bool'], 'hash_final' => ['non-empty-string', 'context'=>'HashContext', 'raw_output='=>'bool'], 'hash_hkdf' => ['non-empty-string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], 'hash_hmac' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], -'hash_hmac_algos' => ['array'], +'hash_hmac_algos' => ['non-empty-list'], 'hash_hmac_file' => ['non-empty-string|false', 'algo'=>'string', 'filename'=>'string', 'key'=>'string', 'raw_output='=>'bool'], 'hash_init' => ['HashContext', 'algo'=>'string', 'options='=>'int', 'key='=>'string'], 'hash_pbkdf2' => ['non-empty-string|false', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], @@ -4513,7 +4514,7 @@ 'ifxus_write_slob' => ['int', 'bid'=>'int', 'content'=>'string'], 'igbinary_serialize' => ['string|null', 'value'=>'mixed'], 'igbinary_unserialize' => ['mixed', 'str'=>'string'], -'ignore_user_abort' => ['int', 'value='=>'bool'], +'ignore_user_abort' => ['0|1', 'value='=>'bool'], 'iis_add_server' => ['int', 'path'=>'string', 'comment'=>'string', 'server_ip'=>'string', 'port'=>'int', 'host_name'=>'string', 'rights'=>'int', 'start_server'=>'int'], 'iis_get_dir_security' => ['int', 'server_instance'=>'int', 'virtual_path'=>'string'], 'iis_get_script_map' => ['string', 'server_instance'=>'int', 'virtual_path'=>'string', 'script_extension'=>'string'], @@ -4660,7 +4661,7 @@ 'Imagick::annotateImage' => ['bool', 'draw_settings'=>'imagickdraw', 'x'=>'float', 'y'=>'float', 'angle'=>'float', 'text'=>'string'], 'Imagick::appendImages' => ['Imagick', 'stack'=>'bool'], 'Imagick::autoGammaImage' => ['bool', 'channel='=>'int'], -'Imagick::autoLevelImage' => ['bool', 'CHANNEL='=>'string'], +'Imagick::autoLevelImage' => ['bool', 'channel='=>'int'], 'Imagick::autoOrient' => ['bool'], 'Imagick::averageImages' => ['Imagick'], 'Imagick::blackThresholdImage' => ['bool', 'threshold'=>'mixed'], @@ -4683,9 +4684,9 @@ 'Imagick::colorMatrixImage' => ['bool', 'color_matrix'=>'array'], 'Imagick::combineImages' => ['Imagick', 'channeltype'=>'int'], 'Imagick::commentImage' => ['bool', 'comment'=>'string'], -'Imagick::compareImageChannels' => ['array', 'image'=>'imagick', 'channeltype'=>'int', 'metrictype'=>'int'], +'Imagick::compareImageChannels' => ['array{Imagick,float}', 'image'=>'imagick', 'channeltype'=>'int', 'metrictype'=>'int'], 'Imagick::compareImageLayers' => ['Imagick', 'method'=>'int'], -'Imagick::compareImages' => ['array', 'compare'=>'imagick', 'metric'=>'int'], +'Imagick::compareImages' => ['array{Imagick,float}', 'compare'=>'imagick', 'metric'=>'int'], 'Imagick::compositeImage' => ['bool', 'composite_object'=>'imagick', 'composite'=>'int', 'x'=>'int', 'y'=>'int', 'channel='=>'int'], 'Imagick::compositeImageGravity' => ['bool', 'imagick'=>'Imagick', 'COMPOSITE_CONSTANT'=>'int', 'GRAVITY_CONSTANT'=>'int'], 'Imagick::contrastImage' => ['bool', 'sharpen'=>'bool'], @@ -4714,7 +4715,7 @@ 'Imagick::equalizeImage' => ['bool'], 'Imagick::evaluateImage' => ['bool', 'op'=>'int', 'constant'=>'float', 'channel='=>'int'], 'Imagick::evaluateImages' => ['bool', 'EVALUATE_CONSTANT'=>'int'], -'Imagick::exportImagePixels' => ['array', 'x'=>'int', 'y'=>'int', 'width'=>'int', 'height'=>'int', 'map'=>'string', 'storage'=>'int'], +'Imagick::exportImagePixels' => ['list', 'x'=>'int', 'y'=>'int', 'width'=>'int', 'height'=>'int', 'map'=>'string', 'storage'=>'int'], 'Imagick::extentImage' => ['bool', 'width'=>'int', 'height'=>'int', 'x'=>'int', 'y'=>'int'], 'Imagick::filter' => ['bool', 'ImagickKernel'=>'ImagickKernel', 'CHANNEL='=>'int'], 'Imagick::flattenImages' => ['Imagick'], @@ -4727,8 +4728,8 @@ 'Imagick::fxImage' => ['Imagick', 'expression'=>'string', 'channel='=>'int'], 'Imagick::gammaImage' => ['bool', 'gamma'=>'float', 'channel='=>'int'], 'Imagick::gaussianBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'int'], -'Imagick::getColorspace' => ['int'], -'Imagick::getCompression' => ['int'], +'Imagick::getColorspace' => ['Imagick::COLORSPACE_*'], +'Imagick::getCompression' => ['Imagick::COMPRESSION_*'], 'Imagick::getCompressionQuality' => ['int'], 'Imagick::getConfigureOptions' => ['string'], 'Imagick::getCopyright' => ['string'], @@ -4736,101 +4737,101 @@ 'Imagick::getFilename' => ['string'], 'Imagick::getFont' => ['string'], 'Imagick::getFormat' => ['string'], -'Imagick::getGravity' => ['int'], +'Imagick::getGravity' => ['Imagick::GRAVITY_*'], 'Imagick::getHDRIEnabled' => ['int'], 'Imagick::getHomeURL' => ['string'], 'Imagick::getImage' => ['Imagick'], -'Imagick::getImageAlphaChannel' => ['int'], +'Imagick::getImageAlphaChannel' => ['bool'], 'Imagick::getImageArtifact' => ['string', 'artifact'=>'string'], 'Imagick::getImageAttribute' => ['string', 'key'=>'string'], 'Imagick::getImageBackgroundColor' => ['ImagickPixel'], 'Imagick::getImageBlob' => ['string'], -'Imagick::getImageBluePrimary' => ['array'], +'Imagick::getImageBluePrimary' => ['array{x:float,y:float}'], 'Imagick::getImageBorderColor' => ['ImagickPixel'], 'Imagick::getImageChannelDepth' => ['int', 'channel'=>'int'], 'Imagick::getImageChannelDistortion' => ['float', 'reference'=>'imagick', 'channel'=>'int', 'metric'=>'int'], 'Imagick::getImageChannelDistortions' => ['float', 'reference'=>'imagick', 'metric'=>'int', 'channel='=>'int'], -'Imagick::getImageChannelExtrema' => ['array', 'channel'=>'int'], -'Imagick::getImageChannelKurtosis' => ['array', 'channel='=>'int'], -'Imagick::getImageChannelMean' => ['array', 'channel'=>'int'], -'Imagick::getImageChannelRange' => ['array', 'channel'=>'int'], -'Imagick::getImageChannelStatistics' => ['array'], +'Imagick::getImageChannelExtrema' => ['array{minima:0|positive-int,maxima:0|positive-int}', 'channel'=>'int'], +'Imagick::getImageChannelKurtosis' => ['array{kurtosis:float,skewness:float}', 'channel='=>'int'], +'Imagick::getImageChannelMean' => ['array{mean:float,standardDeviation:float}', 'channel'=>'int'], +'Imagick::getImageChannelRange' => ['array{minima:float,maxima:float}', 'channel'=>'int'], +'Imagick::getImageChannelStatistics' => ['array{mean:float,minima:float,maxima:float,standardDeviation:float,depth:int}'], 'Imagick::getImageClipMask' => ['Imagick'], 'Imagick::getImageColormapColor' => ['ImagickPixel', 'index'=>'int'], 'Imagick::getImageColors' => ['int'], -'Imagick::getImageColorspace' => ['int'], -'Imagick::getImageCompose' => ['int'], -'Imagick::getImageCompression' => ['int'], +'Imagick::getImageColorspace' => ['Imagick::COLORSPACE_*'], +'Imagick::getImageCompose' => ['Imagick::COMPOSITE_*'], +'Imagick::getImageCompression' => ['Imagick::COMPRESSION_*'], 'Imagick::getImageCompressionQuality' => ['int'], 'Imagick::getImageDelay' => ['int'], 'Imagick::getImageDepth' => ['int'], -'Imagick::getImageDispose' => ['int'], +'Imagick::getImageDispose' => ['Imagick::DISPOSE_*'], 'Imagick::getImageDistortion' => ['float', 'reference'=>'magickwand', 'metric'=>'int'], -'Imagick::getImageExtrema' => ['array'], +'Imagick::getImageExtrema' => ['array{min:0|positive-int,max:0|positive-int}'], 'Imagick::getImageFilename' => ['string'], 'Imagick::getImageFormat' => ['string'], 'Imagick::getImageGamma' => ['float'], -'Imagick::getImageGeometry' => ['array'], -'Imagick::getImageGravity' => ['int'], -'Imagick::getImageGreenPrimary' => ['array'], +'Imagick::getImageGeometry' => ['array{width:int,height:int}'], +'Imagick::getImageGravity' => ['Imagick::GRAVITY_*'], +'Imagick::getImageGreenPrimary' => ['array{x:float,y:float}'], 'Imagick::getImageHeight' => ['int'], -'Imagick::getImageHistogram' => ['array'], +'Imagick::getImageHistogram' => ['list'], 'Imagick::getImageIndex' => ['int'], -'Imagick::getImageInterlaceScheme' => ['int'], -'Imagick::getImageInterpolateMethod' => ['int'], +'Imagick::getImageInterlaceScheme' => ['Imagick::INTERLACE_*'], +'Imagick::getImageInterpolateMethod' => ['Imagick::INTERPOLATE_*'], 'Imagick::getImageIterations' => ['int'], -'Imagick::getImageLength' => ['int'], +'Imagick::getImageLength' => ['0|positive-int'], 'Imagick::getImageMagickLicense' => ['string'], 'Imagick::getImageMatte' => ['bool'], 'Imagick::getImageMatteColor' => ['ImagickPixel'], -'Imagick::getImageMimeType' => ['string'], -'Imagick::getImageOrientation' => ['int'], -'Imagick::getImagePage' => ['array'], +'Imagick::getImageMimeType' => ['non-empty-string'], +'Imagick::getImageOrientation' => ['Imagick::ORIENTATION_*'], +'Imagick::getImagePage' => ['array{width:int,height:int,x:int,y:int}'], 'Imagick::getImagePixelColor' => ['ImagickPixel', 'x'=>'int', 'y'=>'int'], 'Imagick::getImageProfile' => ['string', 'name'=>'string'], 'Imagick::getImageProfiles' => ['array', 'pattern='=>'string', 'only_names='=>'bool'], 'Imagick::getImageProperties' => ['array', 'pattern='=>'string', 'only_names='=>'bool'], 'Imagick::getImageProperty' => ['string', 'name'=>'string'], -'Imagick::getImageRedPrimary' => ['array'], +'Imagick::getImageRedPrimary' => ['array{x:float,y:float}'], 'Imagick::getImageRegion' => ['Imagick', 'width'=>'int', 'height'=>'int', 'x'=>'int', 'y'=>'int'], -'Imagick::getImageRenderingIntent' => ['int'], -'Imagick::getImageResolution' => ['array'], +'Imagick::getImageRenderingIntent' => ['Imagick::RENDERINGINTENT_*'], +'Imagick::getImageResolution' => ['array{x:float,y:float}'], 'Imagick::getImagesBlob' => ['string'], -'Imagick::getImageScene' => ['int'], +'Imagick::getImageScene' => ['0|positive-int'], 'Imagick::getImageSignature' => ['string'], -'Imagick::getImageSize' => ['int'], -'Imagick::getImageTicksPerSecond' => ['int'], +'Imagick::getImageSize' => ['0|positive-int'], +'Imagick::getImageTicksPerSecond' => ['0|positive-int'], 'Imagick::getImageTotalInkDensity' => ['float'], -'Imagick::getImageType' => ['int'], +'Imagick::getImageType' => ['Imagick::IMGTYPE_*'], 'Imagick::getImageUnits' => ['int'], 'Imagick::getImageVirtualPixelMethod' => ['int'], -'Imagick::getImageWhitePoint' => ['array'], -'Imagick::getImageWidth' => ['int'], -'Imagick::getInterlaceScheme' => ['int'], +'Imagick::getImageWhitePoint' => ['array{x:float,y:float}'], +'Imagick::getImageWidth' => ['0|positive-int'], +'Imagick::getInterlaceScheme' => ['Imagick::INTERLACE_*'], 'Imagick::getIteratorIndex' => ['int'], -'Imagick::getNumberImages' => ['int'], +'Imagick::getNumberImages' => ['0|positive-int'], 'Imagick::getOption' => ['string', 'key'=>'string'], 'Imagick::getPackageName' => ['string'], -'Imagick::getPage' => ['array'], +'Imagick::getPage' => ['array{width:int,height:int,x:int,y:int}'], 'Imagick::getPixelIterator' => ['ImagickPixelIterator'], 'Imagick::getPixelRegionIterator' => ['ImagickPixelIterator', 'x'=>'int', 'y'=>'int', 'columns'=>'int', 'rows'=>'int'], 'Imagick::getPointSize' => ['float'], -'Imagick::getQuantum' => ['int'], -'Imagick::getQuantumDepth' => ['array'], -'Imagick::getQuantumRange' => ['array'], +'Imagick::getQuantum' => ['0|positive-int'], +'Imagick::getQuantumDepth' => ['array{quantumDepthLong:0|positive-int,quantumDepthString:numeric-string}'], +'Imagick::getQuantumRange' => ['array{quantumRangeLong:0|positive-int,quantumRangeString:numeric-string}'], 'Imagick::getRegistry' => ['string', 'key'=>'string'], 'Imagick::getReleaseDate' => ['string'], 'Imagick::getResource' => ['int', 'type'=>'int'], 'Imagick::getResourceLimit' => ['int', 'type'=>'int'], -'Imagick::getSamplingFactors' => ['array'], -'Imagick::getSize' => ['array'], +'Imagick::getSamplingFactors' => ['list'], +'Imagick::getSize' => ['array{columns:0|positive-int,rows:0|positive-int}'], 'Imagick::getSizeOffset' => ['int'], -'Imagick::getVersion' => ['array'], +'Imagick::getVersion' => ['array{versionNumber:0|positive-int,versionString:non-falsy-string}'], 'Imagick::haldClutImage' => ['bool', 'clut'=>'imagick', 'channel='=>'int'], 'Imagick::hasNextImage' => ['bool'], 'Imagick::hasPreviousImage' => ['bool'], 'Imagick::identifyFormat' => ['string|false', 'embedText'=>'string'], -'Imagick::identifyImage' => ['array', 'appendrawoutput='=>'bool'], +'Imagick::identifyImage' => ['array{imageName:string,mimetype:string,format:string,units:string,colorSpace:string,type:string,compression:string,fileSize:string,geometry:array{width:0|positive-int,height:0|positive-int},resolution:array{x:float,y:float},signature:string}', 'appendrawoutput='=>'bool'], 'Imagick::identifyImageType' => ['int'], 'Imagick::implodeImage' => ['bool', 'radius'=>'float'], 'Imagick::importImagePixels' => ['bool', 'x'=>'int', 'y'=>'int', 'width'=>'int', 'height'=>'int', 'map'=>'string', 'storage'=>'int', 'pixels'=>'array'], @@ -4840,7 +4841,7 @@ 'Imagick::levelImage' => ['bool', 'blackpoint'=>'float', 'gamma'=>'float', 'whitepoint'=>'float', 'channel='=>'int'], 'Imagick::linearStretchImage' => ['bool', 'blackpoint'=>'float', 'whitepoint'=>'float'], 'Imagick::liquidRescaleImage' => ['bool', 'width'=>'int', 'height'=>'int', 'delta_x'=>'float', 'rigidity'=>'float'], -'Imagick::listRegistry' => ['array'], +'Imagick::listRegistry' => ['array'], 'Imagick::localContrastImage' => ['bool', 'radius'=>'float', 'strength'=>'float'], 'Imagick::magnifyImage' => ['bool'], 'Imagick::mapImage' => ['bool', 'map'=>'imagick', 'dither'=>'bool'], @@ -4874,12 +4875,12 @@ 'Imagick::posterizeImage' => ['bool', 'levels'=>'int', 'dither'=>'bool'], 'Imagick::previewImages' => ['bool', 'preview'=>'int'], 'Imagick::previousImage' => ['bool'], -'Imagick::profileImage' => ['bool', 'name'=>'string', 'profile'=>'string'], +'Imagick::profileImage' => ['bool', 'name'=>'string', 'profile'=>'?string'], 'Imagick::quantizeImage' => ['bool', 'numbercolors'=>'int', 'colorspace'=>'int', 'treedepth'=>'int', 'dither'=>'bool', 'measureerror'=>'bool'], 'Imagick::quantizeImages' => ['bool', 'numbercolors'=>'int', 'colorspace'=>'int', 'treedepth'=>'int', 'dither'=>'bool', 'measureerror'=>'bool'], 'Imagick::queryFontMetrics' => ['array{characterWidth:float,characterHeight:float,ascender:float,descender:float,textWidth:float,textHeight:float,maxHorizontalAdvance:float,boundingBox:array{x1:float,x2:float,y1:float,y2:float},originX:float,originY:float}', 'properties'=>'imagickdraw', 'text'=>'string', 'multiline='=>'bool'], -'Imagick::queryFonts' => ['array', 'pattern='=>'string'], -'Imagick::queryFormats' => ['array', 'pattern='=>'string'], +'Imagick::queryFonts' => ['list', 'pattern='=>'string'], +'Imagick::queryFormats' => ['list', 'pattern='=>'string'], 'Imagick::radialBlurImage' => ['bool', 'angle'=>'float', 'channel='=>'int'], 'Imagick::raiseImage' => ['bool', 'width'=>'int', 'height'=>'int', 'x'=>'int', 'y'=>'int', 'raise'=>'bool'], 'Imagick::randomThresholdImage' => ['bool', 'low'=>'float', 'high'=>'float', 'channel='=>'int'], @@ -4990,7 +4991,7 @@ 'Imagick::similarityImage' => ['Imagick', 'imagick'=>'Imagick', '&bestMatch'=>'array', '&similarity'=>'float', 'similarity_threshold'=>'float', 'metric'=>'int'], 'Imagick::sketchImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'angle'=>'float'], 'Imagick::smushImages' => ['Imagick', 'stack'=>'bool', 'offset'=>'int'], -'Imagick::solarizeImage' => ['bool', 'threshold'=>'int'], +'Imagick::solarizeImage' => ['bool', 'threshold'=>'0|positive-int'], 'Imagick::sparseColorImage' => ['bool', 'sparse_method'=>'int', 'arguments'=>'array', 'channel='=>'int'], 'Imagick::spliceImage' => ['bool', 'width'=>'int', 'height'=>'int', 'x'=>'int', 'y'=>'int'], 'Imagick::spreadImage' => ['bool', 'radius'=>'float'], @@ -5040,28 +5041,28 @@ 'ImagickDraw::getDensity' => ['null|string'], 'ImagickDraw::getFillColor' => ['ImagickPixel'], 'ImagickDraw::getFillOpacity' => ['float'], -'ImagickDraw::getFillRule' => ['int'], +'ImagickDraw::getFillRule' => ['Imagick::FILLRULE_*'], 'ImagickDraw::getFont' => ['string'], 'ImagickDraw::getFontFamily' => ['string'], 'ImagickDraw::getFontResolution' => ['array'], 'ImagickDraw::getFontSize' => ['float'], -'ImagickDraw::getFontStretch' => ['int'], -'ImagickDraw::getFontStyle' => ['int'], +'ImagickDraw::getFontStretch' => ['Imagick::STRETCH_*'], +'ImagickDraw::getFontStyle' => ['Imagick::STYLE_*'], 'ImagickDraw::getFontWeight' => ['int'], -'ImagickDraw::getGravity' => ['int'], +'ImagickDraw::getGravity' => ['Imagick::GRAVITY_*'], 'ImagickDraw::getOpacity' => ['float'], 'ImagickDraw::getStrokeAntialias' => ['bool'], 'ImagickDraw::getStrokeColor' => ['ImagickPixel'], 'ImagickDraw::getStrokeDashArray' => ['array'], 'ImagickDraw::getStrokeDashOffset' => ['float'], -'ImagickDraw::getStrokeLineCap' => ['int'], -'ImagickDraw::getStrokeLineJoin' => ['int'], +'ImagickDraw::getStrokeLineCap' => ['Imagick::LINECAP_*'], +'ImagickDraw::getStrokeLineJoin' => ['Imagick::LINEJOIN_*'], 'ImagickDraw::getStrokeMiterLimit' => ['int'], 'ImagickDraw::getStrokeOpacity' => ['float'], 'ImagickDraw::getStrokeWidth' => ['float'], -'ImagickDraw::getTextAlignment' => ['int'], +'ImagickDraw::getTextAlignment' => ['Imagick::ALIGN_*'], 'ImagickDraw::getTextAntialias' => ['bool'], -'ImagickDraw::getTextDecoration' => ['int'], +'ImagickDraw::getTextDecoration' => ['Imagick::DECORATION_*'], 'ImagickDraw::getTextDirection' => ['bool'], 'ImagickDraw::getTextEncoding' => ['string'], 'ImagickDraw::getTextInterlineSpacing' => ['float'], @@ -5156,17 +5157,16 @@ 'ImagickDraw::translate' => ['bool', 'x'=>'float', 'y'=>'float'], 'ImagickKernel::addKernel' => ['void', 'ImagickKernel'=>'ImagickKernel'], 'ImagickKernel::addUnityKernel' => ['void'], -'ImagickKernel::fromBuiltin' => ['ImagickKernel', 'kernelType'=>'string', 'kernelString'=>'string'], +'ImagickKernel::fromBuiltin' => ['ImagickKernel', 'kernelType'=>'int', 'kernelString'=>'string'], 'ImagickKernel::fromMatrix' => ['ImagickKernel', 'matrix'=>'array', 'origin='=>'array'], -'ImagickKernel::getMatrix' => ['array'], -'ImagickKernel::scale' => ['void'], +'ImagickKernel::getMatrix' => ['list>'], +'ImagickKernel::scale' => ['void', 'scale'=>'float', 'normalizeFlag'=>'int'], 'ImagickKernel::separate' => ['array'], -'ImagickKernel::seperate' => ['void'], 'ImagickPixel::__construct' => ['void', 'color='=>'string'], 'ImagickPixel::clear' => ['bool'], 'ImagickPixel::clone' => ['void'], 'ImagickPixel::destroy' => ['bool'], -'ImagickPixel::getColor' => ['array', 'normalized='=>'bool'], +'ImagickPixel::getColor' => ['array{r: int|float, g: int|float, b: int|float, a: int|float}', 'normalized='=>'0|1|2'], 'ImagickPixel::getColorAsString' => ['string'], 'ImagickPixel::getColorCount' => ['int'], 'ImagickPixel::getColorQuantum' => ['mixed'], @@ -5252,7 +5252,7 @@ 'imap_mutf7_to_utf8' => ['string|false', 'in'=>'string'], 'imap_num_msg' => ['int|false', 'stream_id'=>'resource'], 'imap_num_recent' => ['int|false', 'stream_id'=>'resource'], -'imap_open' => ['resource|false', 'mailbox'=>'string', 'user'=>'string', 'password'=>'string', 'options='=>'int', 'n_retries='=>'int', 'params=' => 'array|null'], +'imap_open' => ['resource|false', 'mailbox'=>'string', 'user'=>'string', 'password'=>'string', 'options='=>'int', 'n_retries='=>'int', 'params='=>'array|null'], 'imap_ping' => ['bool', 'stream_id'=>'resource'], 'imap_qprint' => ['string|false', 'text'=>'string'], 'imap_rename' => ['bool', 'stream_id'=>'resource', 'old_name'=>'string', 'new_name'=>'string'], @@ -5333,10 +5333,10 @@ 'ini_get_all' => ['array|false', 'extension='=>'?string', 'details='=>'bool'], 'ini_restore' => ['void', 'varname'=>'string'], 'ini_set' => ['string|false', 'varname'=>'string', 'newvalue'=>'string'], -'inotify_add_watch' => ['int', 'inotify_instance'=>'resource', 'pathname'=>'string', 'mask'=>'int'], +'inotify_add_watch' => ['int<1,max>|false', 'inotify_instance'=>'resource', 'pathname'=>'string', 'mask'=>'int'], 'inotify_init' => ['resource'], -'inotify_queue_len' => ['int', 'inotify_instance'=>'resource'], -'inotify_read' => ['array', 'inotify_instance'=>'resource'], +'inotify_queue_len' => ['int<0,max>', 'inotify_instance'=>'resource'], +'inotify_read' => ['list,mask:int<0,max>,cookie:int<0,max>,name:string}>|false', 'inotify_instance'=>'resource'], 'inotify_rm_watch' => ['bool', 'inotify_instance'=>'resource', 'watch_descriptor'=>'int'], 'intdiv' => ['int', 'numerator'=>'int', 'divisor'=>'int'], 'interface_exists' => ['bool', 'classname'=>'string', 'autoload='=>'bool'], @@ -5357,7 +5357,7 @@ 'IntlBreakIterator::getErrorCode' => ['int'], 'IntlBreakIterator::getErrorMessage' => ['string'], 'IntlBreakIterator::getLocale' => ['string', 'locale_type'=>'string'], -'IntlBreakIterator::getPartsIterator' => ['IntlPartsIterator', 'key_type='=>'int'], +'IntlBreakIterator::getPartsIterator' => ['IntlPartsIterator', 'key_type='=>'IntlPartsIterator::KEY_*'], 'IntlBreakIterator::getText' => ['string'], 'IntlBreakIterator::isBoundary' => ['bool', 'offset'=>'int'], 'IntlBreakIterator::last' => ['int'], @@ -5546,6 +5546,7 @@ 'IntlIterator::next' => ['void'], 'IntlIterator::rewind' => ['void'], 'IntlIterator::valid' => ['bool'], +'IntlPartsIterator::current' => ['non-empty-string'], 'IntlPartsIterator::getBreakIterator' => ['IntlBreakIterator'], 'IntlRuleBasedBreakIterator::__construct' => ['void', 'rules'=>'string', 'areCompiled='=>'string'], 'IntlRuleBasedBreakIterator::createCharacterInstance' => ['IntlRuleBasedBreakIterator', 'locale'=>'string'], @@ -5615,7 +5616,7 @@ 'intltz_to_date_time_zone' => ['DateTimeZone|false', 'obj'=>''], 'intltz_use_daylight_time' => ['bool', 'obj'=>''], 'intlz_create_default' => ['IntlTimeZone'], -'intval' => ['int', 'var'=>'mixed', 'base='=>'int'], +'intval' => ['int', 'var'=>'scalar|array|resource|null', 'base='=>'int'], 'InvalidArgumentException::__clone' => ['void'], 'InvalidArgumentException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?InvalidArgumentException)'], 'InvalidArgumentException::__toString' => ['string'], @@ -5628,7 +5629,7 @@ 'InvalidArgumentException::getTraceAsString' => ['string'], 'ip2long' => ['int|false', 'ip_address'=>'string'], 'iptcembed' => ['string|bool', 'iptcdata'=>'string', 'jpeg_file_name'=>'string', 'spool='=>'int'], -'iptcparse' => ['array|false', 'iptcdata'=>'string'], +'iptcparse' => ['array>|false', 'iptcdata'=>'string'], 'is_a' => ['bool', 'object_or_string'=>'object|string', 'class_name'=>'string', 'allow_string='=>'bool'], 'is_array' => ['bool', 'var'=>'mixed'], 'is_bool' => ['bool', 'var'=>'mixed'], @@ -5698,8 +5699,8 @@ 'join\'1' => ['string', 'pieces'=>'array'], 'jpeg2wbmp' => ['bool', 'jpegname'=>'string', 'wbmpname'=>'string', 'dest_height'=>'int', 'dest_width'=>'int', 'threshold'=>'int'], 'json_decode' => ['mixed', 'json'=>'string', 'assoc='=>'bool|null', 'depth='=>'positive-int', 'options='=>'int'], -'json_encode' => ['string|false', 'data'=>'mixed', 'options='=>'int', 'depth='=>'positive-int'], -'json_last_error' => ['int'], +'json_encode' => ['non-empty-string|false', 'data'=>'mixed', 'options='=>'int', 'depth='=>'positive-int'], +'json_last_error' => ['JSON_ERROR_NONE|JSON_ERROR_DEPTH|JSON_ERROR_STATE_MISMATCH|JSON_ERROR_CTRL_CHAR|JSON_ERROR_SYNTAX|JSON_ERROR_UTF8|JSON_ERROR_RECURSION|JSON_ERROR_INF_OR_NAN|JSON_ERROR_UNSUPPORTED_TYPE|JSON_ERROR_INVALID_PROPERTY_NAME|JSON_ERROR_UTF16'], 'json_last_error_msg' => ['string'], 'JsonIncrementalParser::__construct' => ['void', 'depth'=>'', 'options'=>''], 'JsonIncrementalParser::get' => ['', 'options'=>''], @@ -5851,8 +5852,8 @@ 'ldap_8859_to_t61' => ['string|false', 'value'=>'string'], 'ldap_add' => ['bool', 'link_identifier'=>'resource', 'dn'=>'string', 'entry'=>'array', 'servercontrols='=>'array'], 'ldap_add_ext' => ['resource|false', 'link_identifier'=>'resource', 'dn'=>'string', 'entry'=>'array', 'servercontrols='=>'array'], -'ldap_bind' => ['bool', 'link_identifier'=>'resource', 'bind_rdn='=>'string|null', 'bind_password='=>'string|null', 'serverctrls=' => 'array'], -'ldap_bind_ext' => ['resource|false', 'link_identifier'=>'resource', 'bind_rdn='=>'string|null', 'bind_password='=>'string|null', 'serverctrls=' => 'array'], +'ldap_bind' => ['bool', 'link_identifier'=>'resource', 'bind_rdn='=>'string|null', 'bind_password='=>'string|null', 'serverctrls='=>'array'], +'ldap_bind_ext' => ['resource|false', 'link_identifier'=>'resource', 'bind_rdn='=>'string|null', 'bind_password='=>'string|null', 'serverctrls='=>'array'], 'ldap_close' => ['bool', 'link_identifier'=>'resource'], 'ldap_compare' => ['bool', 'link_identifier'=>'resource', 'dn'=>'string', 'attr'=>'string', 'value'=>'string', 'servercontrols='=>'array'], 'ldap_connect' => ['resource|false', 'host='=>'string', 'port='=>'int', 'wallet='=>'string', 'wallet_passwd='=>'string', 'authmode='=>'int'], @@ -6065,7 +6066,7 @@ 'mailparse_msg_extract_part_file' => ['string', 'mimemail'=>'resource', 'filename'=>'mixed', 'callbackfunc='=>'callable'], 'mailparse_msg_extract_whole_part_file' => ['string', 'mimemail'=>'resource', 'filename'=>'string', 'callbackfunc='=>'callable'], 'mailparse_msg_free' => ['bool', 'mimemail'=>'resource'], -'mailparse_msg_get_part' => ['resource', 'mimemail'=>'resource', 'mimesection'=>'string'], +'mailparse_msg_get_part' => ['resource|false', 'mimemail'=>'resource', 'mimesection'=>'string'], 'mailparse_msg_get_part_data' => ['array', 'mimemail'=>'resource'], 'mailparse_msg_get_structure' => ['array', 'mimemail'=>'resource'], 'mailparse_msg_parse' => ['bool', 'mimemail'=>'resource', 'data'=>'string'], @@ -6310,7 +6311,7 @@ 'maxdb_thread_safe' => ['bool'], 'maxdb_use_result' => ['resource', 'link'=>''], 'maxdb_warning_count' => ['int', 'link'=>'resource'], -'mb_check_encoding' => ['bool', 'var='=>'string', 'encoding='=>'string'], +'mb_check_encoding' => ['bool', 'var='=>'string|array', 'encoding='=>'string'], 'mb_chr' => ['string|false', 'cp'=>'int', 'encoding='=>'string'], 'mb_convert_case' => ['string', 'sourcestring'=>'string', 'mode'=>'int', 'encoding='=>'string'], 'mb_convert_encoding' => ['string|array|false', 'val'=>'string|array', 'to_encoding'=>'string', 'from_encoding='=>'mixed'], @@ -6319,10 +6320,10 @@ 'mb_decode_mimeheader' => ['string', 'string'=>'string'], 'mb_decode_numericentity' => ['string', 'string'=>'string', 'convmap'=>'array', 'encoding'=>'string'], 'mb_detect_encoding' => ['string|false', 'str'=>'string', 'encoding_list='=>'mixed', 'strict='=>'bool'], -'mb_detect_order' => ['bool|array', 'encoding_list='=>'mixed'], +'mb_detect_order' => ['bool|list', 'encoding_list='=>'mixed'], 'mb_encode_mimeheader' => ['string', 'str'=>'string', 'charset='=>'string', 'transfer_encoding='=>'string', 'linefeed='=>'string', 'indent='=>'int'], 'mb_encode_numericentity' => ['string', 'string'=>'string', 'convmap'=>'array', 'encoding='=>'string', 'is_hex='=>'bool'], -'mb_encoding_aliases' => ['array|false', 'encoding'=>'string'], +'mb_encoding_aliases' => ['list|false', 'encoding'=>'string'], 'mb_ereg' => ['int|false', 'pattern'=>'string', 'string'=>'string', '&w_registers='=>'array'], 'mb_ereg_match' => ['bool', 'pattern'=>'string', 'string'=>'string', 'option='=>'string'], 'mb_ereg_replace' => ['string|false|null', 'pattern'=>'string', 'replacement'=>'string', 'string'=>'string', 'option='=>'string'], @@ -6341,7 +6342,7 @@ 'mb_http_output' => ['string|bool', 'encoding='=>'string'], 'mb_internal_encoding' => ['string|bool', 'encoding='=>'string'], 'mb_language' => ['string|bool', 'language='=>'string'], -'mb_list_encodings' => ['array'], +'mb_list_encodings' => ['non-empty-list'], 'mb_ord' => ['int|false', 'str'=>'string', 'enc='=>'string'], 'mb_output_handler' => ['string', 'contents'=>'string', 'status'=>'int'], 'mb_parse_str' => ['bool', 'encoded_string'=>'string', '&w_result='=>'array'], @@ -6404,8 +6405,8 @@ 'mcrypt_module_open' => ['resource|false', 'cipher'=>'string', 'cipher_directory'=>'string', 'mode'=>'string', 'mode_directory'=>'string'], 'mcrypt_module_self_test' => ['bool', 'algorithm'=>'string', 'lib_dir='=>'string'], 'mcrypt_ofb' => ['string', 'cipher'=>'string', 'key'=>'string', 'data'=>'string', 'mode'=>'int', 'iv='=>'string'], -'md5' => ['non-empty-string', 'str'=>'string', 'raw_output='=>'bool'], -'md5_file' => ['non-empty-string|false', 'filename'=>'string', 'raw_output='=>'bool'], +'md5' => ['non-falsy-string', 'str'=>'string', 'raw_output='=>'bool'], +'md5_file' => ['non-falsy-string|false', 'filename'=>'string', 'raw_output='=>'bool'], 'mdecrypt_generic' => ['string', 'td'=>'resource', 'data'=>'string'], 'Memcache::add' => ['bool', 'key'=>'string', 'var'=>'mixed', 'flag='=>'int', 'expire='=>'int'], 'Memcache::addServer' => ['bool', 'host'=>'string', 'port='=>'int', 'persistent='=>'bool', 'weight='=>'int', 'timeout='=>'int', 'retry_interval='=>'int', 'status='=>'bool', 'failure_callback='=>'callable', 'timeoutms='=>'int'], @@ -6414,7 +6415,8 @@ 'Memcache::decrement' => ['int', 'key'=>'string', 'value='=>'int'], 'Memcache::delete' => ['bool', 'key'=>'string', 'timeout='=>'int'], 'Memcache::flush' => ['bool'], -'Memcache::get' => ['string|array|false', 'key'=>'string', '&flags='=>'array', '&keys='=>'array'], +'Memcache::get' => ['mixed', 'key'=>'string', '&flags='=>'int'], +'Memcache::get\'1' => ['mixed[]|false', 'keys'=>'string[]', '&flags='=>'int[]'], 'Memcache::getExtendedStats' => ['array', 'type='=>'string', 'slabid='=>'int', 'limit='=>'int'], 'Memcache::getServerStatus' => ['int', 'host'=>'string', 'port='=>'int'], 'Memcache::getStats' => ['array', 'type='=>'string', 'slabid='=>'int', 'limit='=>'int'], @@ -6481,7 +6483,8 @@ 'MemcachePool::decrement' => ['int', 'key'=>'string', 'value='=>'int'], 'MemcachePool::delete' => ['bool', 'key'=>'string', 'timeout='=>'int'], 'MemcachePool::flush' => ['bool'], -'MemcachePool::get' => ['string|array|false', 'key'=>'string', '&flags='=>'array', '&keys='=>'array'], +'MemcachePool::get' => ['mixed', 'key'=>'string', '&flags='=>'int'], +'MemcachePool::get\'1' => ['mixed[]|false', 'keys'=>'string[]', '&flags='=>'int[]'], 'MemcachePool::getExtendedStats' => ['array', 'type='=>'string', 'slabid='=>'int', 'limit='=>'int'], 'MemcachePool::getServerStatus' => ['int', 'host'=>'string', 'port='=>'int'], 'MemcachePool::getStats' => ['array', 'type='=>'string', 'slabid='=>'int', 'limit='=>'int'], @@ -6491,8 +6494,8 @@ 'MemcachePool::set' => ['bool', 'key'=>'string', 'var'=>'mixed', 'flag='=>'int', 'expire='=>'int'], 'MemcachePool::setCompressThreshold' => ['bool', 'threshold'=>'int', 'min_savings='=>'float'], 'MemcachePool::setServerParams' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'int', 'retry_interval='=>'int', 'status='=>'bool', 'failure_callback='=>'callable'], -'memory_get_peak_usage' => ['int', 'real_usage='=>'bool'], -'memory_get_usage' => ['int', 'real_usage='=>'bool'], +'memory_get_peak_usage' => ['positive-int', 'real_usage='=>'bool'], +'memory_get_usage' => ['positive-int', 'real_usage='=>'bool'], 'MessageFormatter::__construct' => ['void', 'locale'=>'string', 'pattern'=>'string'], 'MessageFormatter::create' => ['MessageFormatter', 'locale'=>'string', 'pattern'=>'string'], 'MessageFormatter::format' => ['false|string', 'args'=>'array'], @@ -6522,7 +6525,7 @@ 'ming_useconstants' => ['void', 'use'=>'int'], 'ming_useswfversion' => ['void', 'version'=>'int'], 'mkdir' => ['bool', 'pathname'=>'string', 'mode='=>'int', 'recursive='=>'bool', 'context='=>'resource'], -'mktime' => ['int|false', 'hour='=>'int', 'min='=>'int', 'sec='=>'int', 'mon='=>'int', 'day='=>'int', 'year='=>'int'], +'mktime' => ['__benevolent', 'hour='=>'int', 'min='=>'int', 'sec='=>'int', 'mon='=>'int', 'day='=>'int', 'year='=>'int'], 'money_format' => ['string', 'format'=>'string', 'value'=>'float'], 'Mongo::__construct' => ['void', 'server='=>'string', 'options='=>'array', 'driver_options='=>'array'], 'Mongo::__get' => ['MongoDB', 'dbname'=>'string'], @@ -6711,125 +6714,360 @@ 'MongoDB::setReadPreference' => ['bool', 'read_preference'=>'string', 'tags='=>'array'], 'MongoDB::setSlaveOkay' => ['bool', 'ok='=>'bool'], 'MongoDB::setWriteConcern' => ['bool', 'w'=>'mixed', 'wtimeout='=>'int'], -'MongoDB\BSON\Binary::__construct' => ['void', 'data'=>'string', 'type'=>'int'], +'MongoDB\BSON\fromJSON' => ['string', 'json'=>'string'], +'MongoDB\BSON\fromPHP' => ['string', 'value'=>'object|array'], +'MongoDB\BSON\toCanonicalExtendedJSON' => ['string', 'bson'=>'string'], +'MongoDB\BSON\toJSON' => ['string', 'bson'=>'string'], +'MongoDB\BSON\toPHP' => ['object|array', 'bson'=>'string', 'typemap='=>'?array'], +'MongoDB\BSON\toRelaxedExtendedJSON' => ['string', 'bson'=>'string'], +'MongoDB\Driver\Monitoring\addSubscriber' => ['void', 'subscriber'=>'MongoDB\Driver\Monitoring\Subscriber'], +'MongoDB\Driver\Monitoring\removeSubscriber' => ['void', 'subscriber'=>'MongoDB\Driver\Monitoring\Subscriber'], +'MongoDB\BSON\Binary::__construct' => ['void', 'data'=>'string', 'type='=>'int'], 'MongoDB\BSON\Binary::getData' => ['string'], 'MongoDB\BSON\Binary::getType' => ['int'], -'MongoDB\BSON\Decimal128::__construct' => ['void', 'value='=>'string'], +'MongoDB\BSON\Binary::__toString' => ['string'], +'MongoDB\BSON\Binary::serialize' => ['string'], +'MongoDB\BSON\Binary::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Binary::jsonSerialize' => ['mixed'], +'MongoDB\BSON\BinaryInterface::getData' => ['string'], +'MongoDB\BSON\BinaryInterface::getType' => ['int'], +'MongoDB\BSON\BinaryInterface::__toString' => ['string'], +'MongoDB\BSON\DBPointer::__toString' => ['string'], +'MongoDB\BSON\DBPointer::serialize' => ['string'], +'MongoDB\BSON\DBPointer::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\DBPointer::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Decimal128::__construct' => ['void', 'value'=>'string'], 'MongoDB\BSON\Decimal128::__toString' => ['string'], -'MongoDB\BSON\fromJSON' => ['string', 'json'=>'string'], -'MongoDB\BSON\fromPHP' => ['string', 'value'=>'array|object'], -'MongoDB\BSON\Javascript::__construct' => ['void', 'code'=>'string', 'scope='=>'array|object'], -'MongoDB\BSON\ObjectId::__construct' => ['void', 'id='=>'string'], +'MongoDB\BSON\Decimal128::serialize' => ['string'], +'MongoDB\BSON\Decimal128::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Decimal128::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Decimal128Interface::__toString' => ['string'], +'MongoDB\BSON\Document::fromBSON' => ['MongoDB\BSON\Document', 'bson'=>'string'], +'MongoDB\BSON\Document::fromJSON' => ['MongoDB\BSON\Document', 'json'=>'string'], +'MongoDB\BSON\Document::fromPHP' => ['MongoDB\BSON\Document', 'value'=>'object|array'], +'MongoDB\BSON\Document::get' => ['mixed', 'key'=>'string'], +'MongoDB\BSON\Document::getIterator' => ['MongoDB\BSON\Iterator'], +'MongoDB\BSON\Document::has' => ['bool', 'key'=>'string'], +'MongoDB\BSON\Document::toPHP' => ['object|array', 'typeMap='=>'?array'], +'MongoDB\BSON\Document::toCanonicalExtendedJSON' => ['string'], +'MongoDB\BSON\Document::toRelaxedExtendedJSON' => ['string'], +'MongoDB\BSON\Document::offsetExists' => ['bool', 'offset'=>'mixed'], +'MongoDB\BSON\Document::offsetGet' => ['mixed', 'offset'=>'mixed'], +'MongoDB\BSON\Document::offsetSet' => ['void', 'offset'=>'mixed', 'value'=>'mixed'], +'MongoDB\BSON\Document::offsetUnset' => ['void', 'offset'=>'mixed'], +'MongoDB\BSON\Document::__toString' => ['string'], +'MongoDB\BSON\Document::serialize' => ['string'], +'MongoDB\BSON\Document::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Int64::__construct' => ['void', 'value'=>'string|int'], +'MongoDB\BSON\Int64::__toString' => ['string'], +'MongoDB\BSON\Int64::serialize' => ['string'], +'MongoDB\BSON\Int64::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Int64::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Iterator::current' => ['mixed'], +'MongoDB\BSON\Iterator::key' => ['string|int'], +'MongoDB\BSON\Iterator::next' => ['void'], +'MongoDB\BSON\Iterator::rewind' => ['void'], +'MongoDB\BSON\Iterator::valid' => ['bool'], +'MongoDB\BSON\Javascript::__construct' => ['void', 'code'=>'string', 'scope='=>'object|array|null'], +'MongoDB\BSON\Javascript::getCode' => ['string'], +'MongoDB\BSON\Javascript::getScope' => ['?object'], +'MongoDB\BSON\Javascript::__toString' => ['string'], +'MongoDB\BSON\Javascript::serialize' => ['string'], +'MongoDB\BSON\Javascript::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Javascript::jsonSerialize' => ['mixed'], +'MongoDB\BSON\JavascriptInterface::getCode' => ['string'], +'MongoDB\BSON\JavascriptInterface::getScope' => ['?object'], +'MongoDB\BSON\JavascriptInterface::__toString' => ['string'], +'MongoDB\BSON\MaxKey::serialize' => ['string'], +'MongoDB\BSON\MaxKey::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\MaxKey::jsonSerialize' => ['mixed'], +'MongoDB\BSON\MinKey::serialize' => ['string'], +'MongoDB\BSON\MinKey::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\MinKey::jsonSerialize' => ['mixed'], +'MongoDB\BSON\ObjectId::__construct' => ['void', 'id='=>'?string'], +'MongoDB\BSON\ObjectId::getTimestamp' => ['int'], 'MongoDB\BSON\ObjectId::__toString' => ['string'], +'MongoDB\BSON\ObjectId::serialize' => ['string'], +'MongoDB\BSON\ObjectId::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\ObjectId::jsonSerialize' => ['mixed'], +'MongoDB\BSON\ObjectIdInterface::getTimestamp' => ['int'], +'MongoDB\BSON\ObjectIdInterface::__toString' => ['string'], +'MongoDB\BSON\PackedArray::fromPHP' => ['MongoDB\BSON\PackedArray', 'value'=>'array'], +'MongoDB\BSON\PackedArray::get' => ['mixed', 'index'=>'int'], +'MongoDB\BSON\PackedArray::getIterator' => ['MongoDB\BSON\Iterator'], +'MongoDB\BSON\PackedArray::has' => ['bool', 'index'=>'int'], +'MongoDB\BSON\PackedArray::toPHP' => ['object|array', 'typeMap='=>'?array'], +'MongoDB\BSON\PackedArray::offsetExists' => ['bool', 'offset'=>'mixed'], +'MongoDB\BSON\PackedArray::offsetGet' => ['mixed', 'offset'=>'mixed'], +'MongoDB\BSON\PackedArray::offsetSet' => ['void', 'offset'=>'mixed', 'value'=>'mixed'], +'MongoDB\BSON\PackedArray::offsetUnset' => ['void', 'offset'=>'mixed'], +'MongoDB\BSON\PackedArray::__toString' => ['string'], +'MongoDB\BSON\PackedArray::serialize' => ['string'], +'MongoDB\BSON\PackedArray::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Persistable::bsonSerialize' => ['stdClass|MongoDB\BSON\Document|array'], 'MongoDB\BSON\Regex::__construct' => ['void', 'pattern'=>'string', 'flags='=>'string'], -'MongoDB\BSON\Regex::__toString' => ['string'], -'MongoDB\BSON\Regex::getFlags' => [''], 'MongoDB\BSON\Regex::getPattern' => ['string'], -'MongoDB\BSON\Serializable::bsonSerialize' => ['array|object'], -'MongoDB\BSON\Timestamp::__construct' => ['void', 'increment'=>'int', 'timestamp'=>'int'], +'MongoDB\BSON\Regex::getFlags' => ['string'], +'MongoDB\BSON\Regex::__toString' => ['string'], +'MongoDB\BSON\Regex::serialize' => ['string'], +'MongoDB\BSON\Regex::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Regex::jsonSerialize' => ['mixed'], +'MongoDB\BSON\RegexInterface::getPattern' => ['string'], +'MongoDB\BSON\RegexInterface::getFlags' => ['string'], +'MongoDB\BSON\RegexInterface::__toString' => ['string'], +'MongoDB\BSON\Serializable::bsonSerialize' => ['stdClass|MongoDB\BSON\Document|MongoDB\BSON\PackedArray|array'], +'MongoDB\BSON\Symbol::__toString' => ['string'], +'MongoDB\BSON\Symbol::serialize' => ['string'], +'MongoDB\BSON\Symbol::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Symbol::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Timestamp::__construct' => ['void', 'increment'=>'string|int', 'timestamp'=>'string|int'], +'MongoDB\BSON\Timestamp::getTimestamp' => ['int'], +'MongoDB\BSON\Timestamp::getIncrement' => ['int'], 'MongoDB\BSON\Timestamp::__toString' => ['string'], -'MongoDB\BSON\toJSON' => ['string', 'bson'=>'string'], -'MongoDB\BSON\toPHP' => ['object', 'bson'=>'string', 'typeMap='=>'array'], -'MongoDB\BSON\Unserializable::bsonUnserialize' => ['', 'data'=>'array'], -'MongoDB\BSON\UTCDateTime::__construct' => ['void', 'milliseconds='=>'int|DateTimeInterface'], -'MongoDB\BSON\UTCDateTime::__toString' => ['string'], +'MongoDB\BSON\Timestamp::serialize' => ['string'], +'MongoDB\BSON\Timestamp::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Timestamp::jsonSerialize' => ['mixed'], +'MongoDB\BSON\TimestampInterface::getTimestamp' => ['int'], +'MongoDB\BSON\TimestampInterface::getIncrement' => ['int'], +'MongoDB\BSON\TimestampInterface::__toString' => ['string'], +'MongoDB\BSON\UTCDateTime::__construct' => ['void', 'milliseconds='=>'DateTimeInterface|string|int|float|null'], 'MongoDB\BSON\UTCDateTime::toDateTime' => ['DateTime'], -'MongoDB\Driver\BulkWrite::__construct' => ['void', 'ordered='=>'bool'], -'MongoDB\Driver\BulkWrite::count' => ['0|positive-int'], -'MongoDB\Driver\BulkWrite::delete' => ['void', 'filter'=>'array|object', 'deleteOptions='=>'array'], -'MongoDB\Driver\BulkWrite::insert' => ['MongoDB\Driver\ObjectID', 'document'=>'array|object'], -'MongoDB\Driver\BulkWrite::update' => ['void', 'filter'=>'array|object', 'newObj'=>'array|object', 'updateOptions='=>'array'], -'MongoDB\Driver\Command::__construct' => ['void', 'document'=>'array|object'], -'MongoDB\Driver\Cursor::__construct' => ['void', 'server'=>'Server', 'responseDocument'=>'string'], +'MongoDB\BSON\UTCDateTime::__toString' => ['string'], +'MongoDB\BSON\UTCDateTime::serialize' => ['string'], +'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\UTCDateTime::jsonSerialize' => ['mixed'], +'MongoDB\BSON\UTCDateTimeInterface::toDateTime' => ['DateTime'], +'MongoDB\BSON\UTCDateTimeInterface::__toString' => ['string'], +'MongoDB\BSON\Undefined::__toString' => ['string'], +'MongoDB\BSON\Undefined::serialize' => ['string'], +'MongoDB\BSON\Undefined::unserialize' => ['void', 'data'=>'string'], +'MongoDB\BSON\Undefined::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Unserializable::bsonUnserialize' => ['void', 'data'=>'array'], +'MongoDB\Driver\BulkWrite::__construct' => ['void', 'options='=>'?array'], +'MongoDB\Driver\BulkWrite::count' => ['int'], +'MongoDB\Driver\BulkWrite::delete' => ['void', 'filter'=>'object|array', 'deleteOptions='=>'?array'], +'MongoDB\Driver\BulkWrite::insert' => ['mixed', 'document'=>'object|array'], +'MongoDB\Driver\BulkWrite::update' => ['void', 'filter'=>'object|array', 'newObj'=>'object|array', 'updateOptions='=>'?array'], +'MongoDB\Driver\ClientEncryption::__construct' => ['void', 'options'=>'array'], +'MongoDB\Driver\ClientEncryption::addKeyAltName' => ['?object', 'keyId'=>'MongoDB\BSON\Binary', 'keyAltName'=>'string'], +'MongoDB\Driver\ClientEncryption::createDataKey' => ['MongoDB\BSON\Binary', 'kmsProvider'=>'string', 'options='=>'?array'], +'MongoDB\Driver\ClientEncryption::decrypt' => ['mixed', 'value'=>'MongoDB\BSON\Binary'], +'MongoDB\Driver\ClientEncryption::deleteKey' => ['object', 'keyId'=>'MongoDB\BSON\Binary'], +'MongoDB\Driver\ClientEncryption::encrypt' => ['MongoDB\BSON\Binary', 'value'=>'mixed', 'options='=>'?array'], +'MongoDB\Driver\ClientEncryption::encryptExpression' => ['object', 'expr'=>'object|array', 'options='=>'?array'], +'MongoDB\Driver\ClientEncryption::getKey' => ['?object', 'keyId'=>'MongoDB\BSON\Binary'], +'MongoDB\Driver\ClientEncryption::getKeyByAltName' => ['?object', 'keyAltName'=>'string'], +'MongoDB\Driver\ClientEncryption::getKeys' => ['MongoDB\Driver\Cursor'], +'MongoDB\Driver\ClientEncryption::removeKeyAltName' => ['?object', 'keyId'=>'MongoDB\BSON\Binary', 'keyAltName'=>'string'], +'MongoDB\Driver\ClientEncryption::rewrapManyDataKey' => ['object', 'filter'=>'object|array', 'options='=>'?array'], +'MongoDB\Driver\Command::__construct' => ['void', 'document'=>'object|array', 'commandOptions='=>'?array'], +'MongoDB\Driver\Cursor::current' => ['object|array|null'], 'MongoDB\Driver\Cursor::getId' => ['MongoDB\Driver\CursorId'], 'MongoDB\Driver\Cursor::getServer' => ['MongoDB\Driver\Server'], 'MongoDB\Driver\Cursor::isDead' => ['bool'], +'MongoDB\Driver\Cursor::key' => ['?int'], +'MongoDB\Driver\Cursor::next' => ['void'], +'MongoDB\Driver\Cursor::rewind' => ['void'], 'MongoDB\Driver\Cursor::setTypeMap' => ['void', 'typemap'=>'array'], 'MongoDB\Driver\Cursor::toArray' => ['array'], -'MongoDB\Driver\CursorId::__construct' => ['void', 'id'=>'string'], +'MongoDB\Driver\Cursor::valid' => ['bool'], 'MongoDB\Driver\CursorId::__toString' => ['string'], -'MongoDB\Driver\Exception\RuntimeException::__clone' => ['void'], -'MongoDB\Driver\Exception\RuntimeException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?RuntimeException)|(?Throwable)'], +'MongoDB\Driver\CursorId::serialize' => ['string'], +'MongoDB\Driver\CursorId::unserialize' => ['void', 'data'=>'string'], +'MongoDB\Driver\CursorInterface::getId' => ['MongoDB\Driver\CursorId'], +'MongoDB\Driver\CursorInterface::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\CursorInterface::isDead' => ['bool'], +'MongoDB\Driver\CursorInterface::setTypeMap' => ['void', 'typemap'=>'array'], +'MongoDB\Driver\CursorInterface::toArray' => ['array'], +'MongoDB\Driver\Exception\AuthenticationException::__toString' => ['string'], +'MongoDB\Driver\Exception\BulkWriteException::__toString' => ['string'], +'MongoDB\Driver\Exception\CommandException::getResultDocument' => ['object'], +'MongoDB\Driver\Exception\CommandException::__toString' => ['string'], +'MongoDB\Driver\Exception\ConnectionException::__toString' => ['string'], +'MongoDB\Driver\Exception\ConnectionTimeoutException::__toString' => ['string'], +'MongoDB\Driver\Exception\EncryptionException::__toString' => ['string'], +'MongoDB\Driver\Exception\Exception::__toString' => ['string'], +'MongoDB\Driver\Exception\ExecutionTimeoutException::__toString' => ['string'], +'MongoDB\Driver\Exception\InvalidArgumentException::__toString' => ['string'], +'MongoDB\Driver\Exception\LogicException::__toString' => ['string'], +'MongoDB\Driver\Exception\RuntimeException::hasErrorLabel' => ['bool', 'errorLabel'=>'string'], 'MongoDB\Driver\Exception\RuntimeException::__toString' => ['string'], -'MongoDB\Driver\Exception\RuntimeException::__wakeup' => ['void'], -'MongoDB\Driver\Exception\RuntimeException::getCode' => ['int'], -'MongoDB\Driver\Exception\RuntimeException::getFile' => ['string'], -'MongoDB\Driver\Exception\RuntimeException::getLine' => ['int'], -'MongoDB\Driver\Exception\RuntimeException::getMessage' => ['string'], -'MongoDB\Driver\Exception\RuntimeException::getPrevious' => ['RuntimeException|Throwable'], -'MongoDB\Driver\Exception\RuntimeException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], -'MongoDB\Driver\Exception\RuntimeException::getTraceAsString' => ['string'], -'MongoDB\Driver\Exception\WriteException::__clone' => ['void'], -'MongoDB\Driver\Exception\WriteException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?RuntimeException)|(?Throwable)'], -'MongoDB\Driver\Exception\WriteException::__toString' => ['string'], -'MongoDB\Driver\Exception\WriteException::__wakeup' => ['void'], -'MongoDB\Driver\Exception\WriteException::getCode' => ['int'], -'MongoDB\Driver\Exception\WriteException::getFile' => ['string'], -'MongoDB\Driver\Exception\WriteException::getLine' => ['int'], -'MongoDB\Driver\Exception\WriteException::getMessage' => ['string'], -'MongoDB\Driver\Exception\WriteException::getPrevious' => ['RuntimeException|Throwable'], -'MongoDB\Driver\Exception\WriteException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], -'MongoDB\Driver\Exception\WriteException::getTraceAsString' => ['string'], +'MongoDB\Driver\Exception\SSLConnectionException::__toString' => ['string'], +'MongoDB\Driver\Exception\ServerException::__toString' => ['string'], +'MongoDB\Driver\Exception\UnexpectedValueException::__toString' => ['string'], 'MongoDB\Driver\Exception\WriteException::getWriteResult' => ['MongoDB\Driver\WriteResult'], -'MongoDB\Driver\Manager::__construct' => ['void', 'uri'=>'string', 'options='=>'array', 'driverOptions='=>'array'], -'MongoDB\Driver\Manager::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'bulk'=>'MongoDB\Driver\BulkWrite', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], -'MongoDB\Driver\Manager::executeCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'readPreference='=>'MongoDB\Driver\ReadPreference'], -'MongoDB\Driver\Manager::executeDelete' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'filter'=>'array|object', 'deleteOptions='=>'array', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], -'MongoDB\Driver\Manager::executeInsert' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'document'=>'array|object', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], -'MongoDB\Driver\Manager::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace'=>'string', 'query'=>'MongoDB\Driver\Query', 'readPreference='=>'MongoDB\Driver\ReadPreference'], -'MongoDB\Driver\Manager::executeUpdate' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'filter'=>'array|object', 'newObj'=>'array|object', 'updateOptions='=>'array', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], +'MongoDB\Driver\Exception\WriteException::__toString' => ['string'], +'MongoDB\Driver\Manager::__construct' => ['void', 'uri='=>'?string', 'uriOptions='=>'?array', 'driverOptions='=>'?array'], +'MongoDB\Driver\Manager::addSubscriber' => ['void', 'subscriber'=>'MongoDB\Driver\Monitoring\Subscriber'], +'MongoDB\Driver\Manager::createClientEncryption' => ['MongoDB\Driver\ClientEncryption', 'options'=>'array'], +'MongoDB\Driver\Manager::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'bulk'=>'MongoDB\Driver\BulkWrite', 'options='=>'MongoDB\Driver\WriteConcern|array|null'], +'MongoDB\Driver\Manager::executeCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'MongoDB\Driver\ReadPreference|array|null'], +'MongoDB\Driver\Manager::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace'=>'string', 'query'=>'MongoDB\Driver\Query', 'options='=>'MongoDB\Driver\ReadPreference|array|null'], +'MongoDB\Driver\Manager::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'?array'], +'MongoDB\Driver\Manager::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'?array'], +'MongoDB\Driver\Manager::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'?array'], +'MongoDB\Driver\Manager::getEncryptedFieldsMap' => ['object|array|null'], 'MongoDB\Driver\Manager::getReadConcern' => ['MongoDB\Driver\ReadConcern'], 'MongoDB\Driver\Manager::getReadPreference' => ['MongoDB\Driver\ReadPreference'], 'MongoDB\Driver\Manager::getServers' => ['array'], 'MongoDB\Driver\Manager::getWriteConcern' => ['MongoDB\Driver\WriteConcern'], -'MongoDB\Driver\Manager::selectServer' => ['MongoDB\Driver\Server', 'readPreference'=>'MongoDB\Driver\ReadPreference'], -'MongoDB\Driver\Query::__construct' => ['void', 'filter'=>'array|object', 'queryOptions='=>'array'], -'MongoDB\Driver\ReadConcern::__construct' => ['void', 'level='=>'string'], -'MongoDB\Driver\ReadConcern::bsonSerialize' => ['object'], -'MongoDB\Driver\ReadConcern::getLevel' => ['null|string'], -'MongoDB\Driver\ReadPreference::__construct' => ['void', 'mode'=>'string|int', 'tagSets='=>'array', 'options='=>'array'], -'MongoDB\Driver\ReadPreference::bsonSerialize' => ['object'], +'MongoDB\Driver\Manager::removeSubscriber' => ['void', 'subscriber'=>'MongoDB\Driver\Monitoring\Subscriber'], +'MongoDB\Driver\Manager::selectServer' => ['MongoDB\Driver\Server', 'readPreference='=>'?MongoDB\Driver\ReadPreference'], +'MongoDB\Driver\Manager::startSession' => ['MongoDB\Driver\Session', 'options='=>'?array'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getCommandName' => ['string'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getDurationMicros' => ['int'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getError' => ['Exception'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getOperationId' => ['string'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getReply' => ['object'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getRequestId' => ['string'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getServerConnectionId' => ['?int'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getCommand' => ['object'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getCommandName' => ['string'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getDatabaseName' => ['string'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getOperationId' => ['string'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getRequestId' => ['string'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getServerConnectionId' => ['?int'], +'MongoDB\Driver\Monitoring\CommandSubscriber::commandStarted' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandStartedEvent'], +'MongoDB\Driver\Monitoring\CommandSubscriber::commandSucceeded' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandSucceededEvent'], +'MongoDB\Driver\Monitoring\CommandSubscriber::commandFailed' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandFailedEvent'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getCommandName' => ['string'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getDurationMicros' => ['int'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getOperationId' => ['string'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getReply' => ['object'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getRequestId' => ['string'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServerConnectionId' => ['?int'], +'MongoDB\Driver\Monitoring\LogSubscriber::log' => ['void', 'level'=>'int', 'domain'=>'string', 'message'=>'string'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverChanged' => ['void', 'event'=>'MongoDB\Driver\Monitoring\ServerChangedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverClosed' => ['void', 'event'=>'MongoDB\Driver\Monitoring\ServerClosedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverOpening' => ['void', 'event'=>'MongoDB\Driver\Monitoring\ServerOpeningEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatFailed' => ['void', 'event'=>'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatStarted' => ['void', 'event'=>'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatSucceeded' => ['void', 'event'=>'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyChanged' => ['void', 'event'=>'MongoDB\Driver\Monitoring\TopologyChangedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyClosed' => ['void', 'event'=>'MongoDB\Driver\Monitoring\TopologyClosedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyOpening' => ['void', 'event'=>'MongoDB\Driver\Monitoring\TopologyOpeningEvent'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getNewDescription' => ['MongoDB\Driver\ServerDescription'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getPreviousDescription' => ['MongoDB\Driver\ServerDescription'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\ServerClosedEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerClosedEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerClosedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getDurationMicros' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getError' => ['Exception'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::isAwaited' => ['bool'], +'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::isAwaited' => ['bool'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getDurationMicros' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getReply' => ['object'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::isAwaited' => ['bool'], +'MongoDB\Driver\Monitoring\ServerOpeningEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerOpeningEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerOpeningEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\TopologyChangedEvent::getNewDescription' => ['MongoDB\Driver\TopologyDescription'], +'MongoDB\Driver\Monitoring\TopologyChangedEvent::getPreviousDescription' => ['MongoDB\Driver\TopologyDescription'], +'MongoDB\Driver\Monitoring\TopologyChangedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\TopologyClosedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\TopologyOpeningEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Query::__construct' => ['void', 'filter'=>'object|array', 'queryOptions='=>'?array'], +'MongoDB\Driver\ReadConcern::__construct' => ['void', 'level='=>'?string'], +'MongoDB\Driver\ReadConcern::getLevel' => ['?string'], +'MongoDB\Driver\ReadConcern::isDefault' => ['bool'], +'MongoDB\Driver\ReadConcern::bsonSerialize' => ['stdClass'], +'MongoDB\Driver\ReadConcern::serialize' => ['string'], +'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'data'=>'string'], +'MongoDB\Driver\ReadPreference::__construct' => ['void', 'mode'=>'string|int', 'tagSets='=>'?array', 'options='=>'?array'], +'MongoDB\Driver\ReadPreference::getHedge' => ['?object'], +'MongoDB\Driver\ReadPreference::getMaxStalenessSeconds' => ['int'], 'MongoDB\Driver\ReadPreference::getMode' => ['int'], +'MongoDB\Driver\ReadPreference::getModeString' => ['string'], 'MongoDB\Driver\ReadPreference::getTagSets' => ['array'], -'MongoDB\Driver\Server::__construct' => ['void', 'host'=>'string', 'port'=>'string', 'options='=>'array', 'driverOptions='=>'array'], -'MongoDB\Driver\Server::executeBulkWrite' => ['', 'namespace'=>'string', 'zwrite'=>'BulkWrite'], -'MongoDB\Driver\Server::executeCommand' => ['', 'db'=>'string', 'command'=>'Command'], -'MongoDB\Driver\Server::executeQuery' => ['', 'namespace'=>'string', 'zquery'=>'Query'], -'MongoDB\Driver\Server::getHost' => [''], -'MongoDB\Driver\Server::getInfo' => [''], -'MongoDB\Driver\Server::getLatency' => [''], -'MongoDB\Driver\Server::getPort' => [''], -'MongoDB\Driver\Server::getState' => [''], +'MongoDB\Driver\ReadPreference::bsonSerialize' => ['stdClass'], +'MongoDB\Driver\ReadPreference::serialize' => ['string'], +'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'data'=>'string'], +'MongoDB\Driver\Server::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'bulkWrite'=>'MongoDB\Driver\BulkWrite', 'options='=>'MongoDB\Driver\WriteConcern|array|null'], +'MongoDB\Driver\Server::executeCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'MongoDB\Driver\ReadPreference|array|null'], +'MongoDB\Driver\Server::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace'=>'string', 'query'=>'MongoDB\Driver\Query', 'options='=>'MongoDB\Driver\ReadPreference|array|null'], +'MongoDB\Driver\Server::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'?array'], +'MongoDB\Driver\Server::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'?array'], +'MongoDB\Driver\Server::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'?array'], +'MongoDB\Driver\Server::getHost' => ['string'], +'MongoDB\Driver\Server::getInfo' => ['array'], +'MongoDB\Driver\Server::getLatency' => ['?int'], +'MongoDB\Driver\Server::getPort' => ['int'], +'MongoDB\Driver\Server::getServerDescription' => ['MongoDB\Driver\ServerDescription'], 'MongoDB\Driver\Server::getTags' => ['array'], -'MongoDB\Driver\Server::getType' => [''], +'MongoDB\Driver\Server::getType' => ['int'], 'MongoDB\Driver\Server::isArbiter' => ['bool'], -'MongoDB\Driver\Server::isDelayed' => [''], 'MongoDB\Driver\Server::isHidden' => ['bool'], -'MongoDB\Driver\Server::isPassive' => [''], +'MongoDB\Driver\Server::isPassive' => ['bool'], 'MongoDB\Driver\Server::isPrimary' => ['bool'], 'MongoDB\Driver\Server::isSecondary' => ['bool'], -'MongoDB\Driver\WriteConcern::__construct' => ['void', 'w'=>'string|int', 'wtimeout='=>'int', 'journal='=>'bool', 'fsync='=>'bool'], -'MongoDB\Driver\WriteConcern::getJurnal' => ['bool|null'], -'MongoDB\Driver\WriteConcern::getW' => ['int|null|string'], +'MongoDB\Driver\ServerApi::__construct' => ['void', 'version'=>'string', 'strict='=>'?bool', 'deprecationErrors='=>'?bool'], +'MongoDB\Driver\ServerApi::bsonSerialize' => ['stdClass'], +'MongoDB\Driver\ServerApi::serialize' => ['string'], +'MongoDB\Driver\ServerApi::unserialize' => ['void', 'data'=>'string'], +'MongoDB\Driver\ServerDescription::getHelloResponse' => ['array'], +'MongoDB\Driver\ServerDescription::getHost' => ['string'], +'MongoDB\Driver\ServerDescription::getLastUpdateTime' => ['int'], +'MongoDB\Driver\ServerDescription::getPort' => ['int'], +'MongoDB\Driver\ServerDescription::getRoundTripTime' => ['?int'], +'MongoDB\Driver\ServerDescription::getType' => ['string'], +'MongoDB\Driver\Session::abortTransaction' => ['void'], +'MongoDB\Driver\Session::advanceClusterTime' => ['void', 'clusterTime'=>'object|array'], +'MongoDB\Driver\Session::advanceOperationTime' => ['void', 'operationTime'=>'MongoDB\BSON\TimestampInterface'], +'MongoDB\Driver\Session::commitTransaction' => ['void'], +'MongoDB\Driver\Session::endSession' => ['void'], +'MongoDB\Driver\Session::getClusterTime' => ['?object'], +'MongoDB\Driver\Session::getLogicalSessionId' => ['object'], +'MongoDB\Driver\Session::getOperationTime' => ['?MongoDB\BSON\Timestamp'], +'MongoDB\Driver\Session::getServer' => ['?MongoDB\Driver\Server'], +'MongoDB\Driver\Session::getTransactionOptions' => ['?array'], +'MongoDB\Driver\Session::getTransactionState' => ['string'], +'MongoDB\Driver\Session::isDirty' => ['bool'], +'MongoDB\Driver\Session::isInTransaction' => ['bool'], +'MongoDB\Driver\Session::startTransaction' => ['void', 'options='=>'?array'], +'MongoDB\Driver\TopologyDescription::getServers' => ['array'], +'MongoDB\Driver\TopologyDescription::getType' => ['string'], +'MongoDB\Driver\TopologyDescription::hasReadableServer' => ['bool', 'readPreference='=>'?MongoDB\Driver\ReadPreference'], +'MongoDB\Driver\TopologyDescription::hasWritableServer' => ['bool'], +'MongoDB\Driver\WriteConcern::__construct' => ['void', 'w'=>'string|int', 'wtimeout='=>'?int', 'journal='=>'?bool'], +'MongoDB\Driver\WriteConcern::getJournal' => ['?bool'], +'MongoDB\Driver\WriteConcern::getW' => ['string|int|null'], 'MongoDB\Driver\WriteConcern::getWtimeout' => ['int'], -'MongoDB\Driver\WriteConcernError::getCode' => [''], -'MongoDB\Driver\WriteConcernError::getInfo' => [''], -'MongoDB\Driver\WriteConcernError::getMessage' => [''], -'MongoDB\Driver\WriteError::getCode' => [''], -'MongoDB\Driver\WriteError::getIndex' => [''], -'MongoDB\Driver\WriteError::getInfo' => ['mixed'], -'MongoDB\Driver\WriteError::getMessage' => [''], -'MongoDB\Driver\WriteException::getWriteResult' => [''], -'MongoDB\Driver\WriteResult::getDeletedCount' => ['int'], -'MongoDB\Driver\WriteResult::getInfo' => [''], -'MongoDB\Driver\WriteResult::getInsertedCount' => ['int'], -'MongoDB\Driver\WriteResult::getMatchedCount' => ['int'], -'MongoDB\Driver\WriteResult::getModifiedCount' => ['int'], -'MongoDB\Driver\WriteResult::getServer' => [''], -'MongoDB\Driver\WriteResult::getUpsertedCount' => ['int'], -'MongoDB\Driver\WriteResult::getUpsertedIds' => [''], -'MongoDB\Driver\WriteResult::getWriteConcernError' => [''], -'MongoDB\Driver\WriteResult::getWriteErrors' => [''], +'MongoDB\Driver\WriteConcern::isDefault' => ['bool'], +'MongoDB\Driver\WriteConcern::bsonSerialize' => ['stdClass'], +'MongoDB\Driver\WriteConcern::serialize' => ['string'], +'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'data'=>'string'], +'MongoDB\Driver\WriteConcernError::getCode' => ['int'], +'MongoDB\Driver\WriteConcernError::getInfo' => ['?object'], +'MongoDB\Driver\WriteConcernError::getMessage' => ['string'], +'MongoDB\Driver\WriteError::getCode' => ['int'], +'MongoDB\Driver\WriteError::getIndex' => ['int'], +'MongoDB\Driver\WriteError::getInfo' => ['?object'], +'MongoDB\Driver\WriteError::getMessage' => ['string'], +'MongoDB\Driver\WriteResult::getInsertedCount' => ['?int'], +'MongoDB\Driver\WriteResult::getMatchedCount' => ['?int'], +'MongoDB\Driver\WriteResult::getModifiedCount' => ['?int'], +'MongoDB\Driver\WriteResult::getDeletedCount' => ['?int'], +'MongoDB\Driver\WriteResult::getUpsertedCount' => ['?int'], +'MongoDB\Driver\WriteResult::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\WriteResult::getUpsertedIds' => ['array'], +'MongoDB\Driver\WriteResult::getWriteConcernError' => ['?MongoDB\Driver\WriteConcernError'], +'MongoDB\Driver\WriteResult::getWriteErrors' => ['array'], +'MongoDB\Driver\WriteResult::getErrorReplies' => ['array'], 'MongoDB\Driver\WriteResult::isAcknowledged' => ['bool'], 'MongoDBRef::create' => ['array', 'collection'=>'string', 'id'=>'mixed', 'database='=>'string'], 'MongoDBRef::get' => ['array', 'db'=>'mongodb', 'ref'=>'array'], @@ -7182,7 +7420,7 @@ 'mysqli::get_charset' => ['object'], 'mysqli::get_client_info' => ['string'], 'mysqli::get_connection_stats' => ['array|false'], -'mysqli::get_warnings' => ['mysqli_warning'], +'mysqli::get_warnings' => ['mysqli_warning|false'], 'mysqli::init' => ['mysqli'], 'mysqli::kill' => ['bool', 'processid'=>'int'], 'mysqli::more_results' => ['bool'], @@ -7193,7 +7431,7 @@ 'mysqli::poll' => ['int|false', '&w_read'=>'array', '&w_error'=>'array', '&w_reject'=>'array', 'sec'=>'int', 'usec='=>'int'], 'mysqli::prepare' => ['mysqli_stmt|false', 'query'=>'string'], 'mysqli::query' => ['bool|mysqli_result', 'query'=>'string', 'resultmode='=>'int'], -'mysqli::real_connect' => ['bool', 'host='=>'string', 'username='=>'string', 'passwd='=>'string', 'dbname='=>'string', 'port='=>'int', 'socket='=>'string', 'flags='=>'int'], +'mysqli::real_connect' => ['bool', 'host='=>'?string', 'username='=>'?string', 'passwd='=>'?string', 'dbname='=>'?string', 'port='=>'?int', 'socket='=>'?string', 'flags='=>'int'], 'mysqli::real_escape_string' => ['string', 'escapestr'=>'string'], 'mysqli::real_query' => ['bool', 'query'=>'string'], 'mysqli::reap_async_query' => ['mysqli_result|false'], @@ -7240,11 +7478,11 @@ 'mysqli_fetch_all' => ['array|false', 'result'=>'mysqli_result', 'resulttype='=>'int'], 'mysqli_fetch_array' => ['array|null|false', 'result'=>'mysqli_result', 'resulttype='=>'int'], 'mysqli_fetch_assoc' => ['array|null', 'result'=>'mysqli_result'], -'mysqli_fetch_field' => ['object|false', 'result'=>'mysqli_result'], -'mysqli_fetch_field_direct' => ['object|false', 'result'=>'mysqli_result', 'fieldnr'=>'int'], -'mysqli_fetch_fields' => ['array', 'result'=>'mysqli_result'], +'mysqli_fetch_field' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: int, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'result'=>'mysqli_result'], +'mysqli_fetch_field_direct' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: int, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'result'=>'mysqli_result', 'fieldnr'=>'int'], +'mysqli_fetch_fields' => ['list', 'result'=>'mysqli_result'], 'mysqli_fetch_lengths' => ['array|false', 'result'=>'mysqli_result'], -'mysqli_fetch_object' => ['object|null', 'result'=>'mysqli_result', 'class_name='=>'string', 'params='=>'?array'], +'mysqli_fetch_object' => ['object|false|null', 'result'=>'mysqli_result', 'class_name='=>'string', 'params='=>'?array'], 'mysqli_fetch_row' => ['array|null', 'result'=>'mysqli_result'], 'mysqli_field_count' => ['int', 'link'=>'mysqli'], 'mysqli_field_seek' => ['bool', 'result'=>'mysqli_result', 'fieldnr'=>'int'], @@ -7272,13 +7510,13 @@ 'mysqli_multi_query' => ['bool', 'link'=>'mysqli', 'query'=>'string'], 'mysqli_next_result' => ['bool', 'link'=>'mysqli'], 'mysqli_num_fields' => ['int', 'link'=>'mysqli_result'], -'mysqli_num_rows' => ['int', 'link'=>'mysqli_result'], +'mysqli_num_rows' => ['int<0,max>|numeric-string', 'link'=>'mysqli_result'], 'mysqli_options' => ['bool', 'link'=>'mysqli', 'option'=>'int', 'value'=>'mixed'], 'mysqli_ping' => ['bool', 'link'=>'mysqli'], 'mysqli_poll' => ['int|false', 'read'=>'array', 'error'=>'array', 'reject'=>'array', 'sec'=>'int', 'usec='=>'int'], 'mysqli_prepare' => ['mysqli_stmt|false', 'link'=>'mysqli', 'query'=>'string'], 'mysqli_query' => ['mysqli_result|bool', 'link'=>'mysqli', 'query'=>'string', 'resultmode='=>'int'], -'mysqli_real_connect' => ['bool', 'link='=>'mysqli', 'host='=>'string', 'username='=>'string', 'passwd='=>'string', 'dbname='=>'string', 'port='=>'int', 'socket='=>'string', 'flags='=>'int'], +'mysqli_real_connect' => ['bool', 'link='=>'mysqli', 'host='=>'?string', 'username='=>'?string', 'passwd='=>'?string', 'dbname='=>'?string', 'port='=>'?int', 'socket='=>'?string', 'flags='=>'int'], 'mysqli_real_escape_string' => ['string', 'link'=>'mysqli', 'escapestr'=>'string'], 'mysqli_real_query' => ['bool', 'link'=>'mysqli', 'query'=>'string'], 'mysqli_reap_async_query' => ['mysqli_result|false', 'link'=>'mysqli'], @@ -7291,9 +7529,9 @@ 'mysqli_result::fetch_all' => ['array', 'resulttype='=>'int'], 'mysqli_result::fetch_array' => ['array|null', 'resulttype='=>'int'], 'mysqli_result::fetch_assoc' => ['array|null'], -'mysqli_result::fetch_field' => ['object|false'], -'mysqli_result::fetch_field_direct' => ['object|false', 'fieldnr'=>'int'], -'mysqli_result::fetch_fields' => ['array'], +'mysqli_result::fetch_field' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: int, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false'], +'mysqli_result::fetch_field_direct' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: int, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'fieldnr'=>'int'], +'mysqli_result::fetch_fields' => ['list'], 'mysqli_result::fetch_object' => ['object|null', 'class_name='=>'string', 'params='=>'array'], 'mysqli_result::fetch_row' => ['array|null'], 'mysqli_result::field_seek' => ['bool', 'fieldnr'=>'int'], @@ -7325,10 +7563,10 @@ 'mysqli_stmt::fetch' => ['bool|null'], 'mysqli_stmt::free_result' => ['void'], 'mysqli_stmt::get_result' => ['mysqli_result|false'], -'mysqli_stmt::get_warnings' => ['object'], +'mysqli_stmt::get_warnings' => ['mysqli_warning|false'], 'mysqli_stmt::more_results' => ['bool'], 'mysqli_stmt::next_result' => ['bool'], -'mysqli_stmt::num_rows' => ['int'], +'mysqli_stmt::num_rows' => ['int<0,max>|numeric-string'], 'mysqli_stmt::prepare' => ['bool', 'query'=>'string'], 'mysqli_stmt::reset' => ['bool'], 'mysqli_stmt::result_metadata' => ['mysqli_result|false'], @@ -7349,7 +7587,7 @@ 'mysqli_stmt_field_count' => ['0|positive-int', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_free_result' => ['void', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_get_result' => ['mysqli_result|false', 'stmt'=>'mysqli_stmt'], -'mysqli_stmt_get_warnings' => ['object|false', 'stmt'=>'mysqli_stmt'], +'mysqli_stmt_get_warnings' => ['mysqli_warning|false', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_init' => ['mysqli_stmt|false', 'link'=>'mysqli'], 'mysqli_stmt_insert_id' => ['', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_more_results' => ['bool', 'stmt'=>'mysqli_stmt'], @@ -7788,7 +8026,7 @@ 'nsapi_response_headers' => ['array'], 'nsapi_virtual' => ['bool', 'uri'=>'string'], 'nthmac' => ['string', 'clent'=>'string', 'data'=>'string'], -'number_format' => ['string', 'number'=>'float', 'num_decimal_places='=>'int', 'dec_separator='=>'string|null', 'thousands_separator='=>'string|null'], +'number_format' => ['non-empty-string', 'number'=>'float', 'num_decimal_places='=>'int', 'dec_separator='=>'string|null', 'thousands_separator='=>'string|null'], 'NumberFormatter::__construct' => ['void', 'locale'=>'string', 'style'=>'int', 'pattern='=>'string'], 'NumberFormatter::create' => ['NumberFormatter', 'locale'=>'string', 'style'=>'int', 'pattern='=>'string'], 'NumberFormatter::format' => ['string|false', 'num'=>'', 'type='=>'int'], @@ -7801,7 +8039,7 @@ 'NumberFormatter::getSymbol' => ['string', 'attr'=>'int'], 'NumberFormatter::getTextAttribute' => ['string', 'attr'=>'int'], 'NumberFormatter::parse' => ['float|false', 'str'=>'string', 'type='=>'int', '&rw_position='=>'int'], -'NumberFormatter::parseCurrency' => ['float', 'str'=>'string', '&w_currency'=>'string', '&rw_position='=>'int'], +'NumberFormatter::parseCurrency' => ['float|false', 'str'=>'string', '&w_currency'=>'string', '&rw_position='=>'int'], 'NumberFormatter::setAttribute' => ['bool', 'attr'=>'int', 'value'=>''], 'NumberFormatter::setPattern' => ['bool', 'pattern'=>'string'], 'NumberFormatter::setSymbol' => ['bool', 'attr'=>'int', 'symbol'=>'string'], @@ -7882,7 +8120,7 @@ 'ob_iconv_handler' => ['string', 'contents'=>'string', 'status'=>'int'], 'ob_implicit_flush' => ['void', 'flag='=>'int'], 'ob_inflatehandler' => ['string', 'data'=>'string', 'mode'=>'int'], -'ob_list_handlers' => ['false|array'], +'ob_list_handlers' => ['false|list'], 'ob_start' => ['bool', 'user_function='=>'string|array|callable|null', 'chunk_size='=>'int', 'flags='=>'int'], 'ob_tidyhandler' => ['string', 'input'=>'string', 'mode='=>'int'], 'OCI-Collection::append' => ['bool', 'value'=>'mixed'], @@ -8090,10 +8328,10 @@ 'openssl_encrypt' => ['string|false', 'data'=>'string', 'method'=>'string', 'key'=>'string', 'options='=>'int', 'iv='=>'string', '&w_tag='=>'string', 'aad='=>'string', 'tag_length='=>'int'], 'openssl_error_string' => ['string|false'], 'openssl_free_key' => ['void', 'key_identifier'=>'resource'], -'openssl_get_cert_locations' => ['array'], -'openssl_get_cipher_methods' => ['array', 'aliases='=>'bool'], -'openssl_get_curve_names' => ['array|false'], -'openssl_get_md_methods' => ['array', 'aliases='=>'bool'], +'openssl_get_cert_locations' => ['array'], +'openssl_get_cipher_methods' => ['list', 'aliases='=>'bool'], +'openssl_get_curve_names' => ['list|false'], +'openssl_get_md_methods' => ['list', 'aliases='=>'bool'], 'openssl_get_privatekey' => ['resource|false', 'key'=>'string', 'passphrase='=>'string'], 'openssl_get_publickey' => ['resource|false', 'cert'=>'resource|string'], 'openssl_open' => ['bool', 'sealed_data'=>'string', '&w_open_data'=>'string', 'env_key'=>'string', 'priv_key_id'=>'string|array|resource', 'method='=>'string', 'iv='=>'string'], @@ -8249,7 +8487,7 @@ 'parsekit_func_arginfo' => ['array', 'function'=>'mixed'], 'passthru' => ['void', 'command'=>'string', '&w_return_value='=>'int'], 'password_get_info' => ['array', 'hash'=>'string'], -'password_hash' => ['non-empty-string|false', 'password'=>'string', 'algo'=>'int', 'options='=>'array'], +'password_hash' => ['__benevolent', 'password'=>'string', 'algo'=>'string|int', 'options='=>'array'], 'password_make_salt' => ['bool', 'password'=>'string', 'hash'=>'string'], 'password_needs_rehash' => ['bool', 'hash'=>'string', 'algo'=>'int', 'options='=>'array'], 'password_verify' => ['bool', 'password'=>'string', 'hash'=>'string'], @@ -8435,7 +8673,7 @@ 'PDF_utf32_to_utf16' => ['string', 'pdfdoc'=>'resource', 'utf32string'=>'string', 'ordering'=>'string'], 'PDF_utf8_to_utf16' => ['string', 'pdfdoc'=>'resource', 'utf8string'=>'string', 'ordering'=>'string'], 'PDO::__construct' => ['void', 'dsn'=>'string', 'username='=>'?string', 'passwd='=>'?string', 'options='=>'?array'], -'PDO::__sleep' => ['array'], +'PDO::__sleep' => ['list'], 'PDO::__wakeup' => ['void'], 'PDO::beginTransaction' => ['bool'], 'PDO::commit' => ['bool'], @@ -8466,7 +8704,7 @@ 'PDO::setAttribute' => ['bool', 'attribute'=>'int', 'value'=>''], 'PDO::sqliteCreateAggregate' => ['bool', 'function_name'=>'string', 'step_func'=>'callable', 'finalize_func'=>'callable', 'num_args='=>'int'], 'PDO::sqliteCreateCollation' => ['bool', 'name'=>'string', 'callback'=>'callable'], -'PDO::sqliteCreateFunction' => ['bool', 'function_name'=>'string', 'callback'=>'callable', 'num_args='=>'int'], +'PDO::sqliteCreateFunction' => ['bool', 'function_name'=>'string', 'callback'=>'callable', 'num_args='=>'int', 'flags='=>'int'], 'pdo_drivers' => ['array'], 'PDOException::getCode' => [''], 'PDOException::getFile' => [''], @@ -8475,7 +8713,7 @@ 'PDOException::getPrevious' => [''], 'PDOException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:\'::\'|\'->\',args?:mixed[],object?:object}'], 'PDOException::getTraceAsString' => [''], -'PDOStatement::__sleep' => ['array'], +'PDOStatement::__sleep' => ['list'], 'PDOStatement::__wakeup' => ['void'], 'PDOStatement::bindColumn' => ['bool', 'column'=>'mixed', '&w_param'=>'mixed', 'type='=>'int', 'maxlen='=>'int', 'driverdata='=>'mixed'], 'PDOStatement::bindParam' => ['bool', 'parameter'=>'mixed', '&w_variable'=>'mixed', 'data_type='=>'int', 'length='=>'int', 'driver_options='=>'mixed'], @@ -8575,7 +8813,7 @@ 'pg_options' => ['string', 'connection='=>'resource'], 'pg_parameter_status' => ['string|false', 'connection'=>'resource', 'param_name'=>'string'], 'pg_parameter_status\'1' => ['string|false', 'param_name'=>'string'], -'pg_pconnect' => ['resource|false', 'connection_string'=>'string', 'host='=>'string', 'port='=>'string|int', 'options='=>'string', 'tty='=>'string', 'database='=>'string'], +'pg_pconnect' => ['resource|false', 'connection_string'=>'string', 'connect_type='=>'int'], 'pg_ping' => ['bool', 'connection='=>'resource'], 'pg_port' => ['int', 'connection='=>'resource'], 'pg_prepare' => ['resource|false', 'connection'=>'resource', 'stmtname'=>'string', 'query'=>'string'], @@ -8724,10 +8962,10 @@ 'phdfs::tell' => ['int', 'path'=>'string'], 'phdfs::write' => ['bool', 'path'=>'string', 'buffer'=>'string', 'mode='=>'string'], 'php_check_syntax' => ['bool', 'filename'=>'string', 'error_message='=>'string'], -'php_ini_loaded_file' => ['string|false'], +'php_ini_loaded_file' => ['non-empty-string|false'], 'php_ini_scanned_files' => ['string|false'], 'php_logo_guid' => ['string'], -'php_sapi_name' => ['string|false'], +'php_sapi_name' => ['__benevolent'], 'php_strip_whitespace' => ['string', 'file_name'=>'string'], 'php_uname' => ['string', 'mode='=>'string'], 'php_user_filter::filter' => ['int', 'in'=>'resource', 'out'=>'resource', '&rw_consumed'=>'int', 'closing'=>'bool'], @@ -8802,9 +9040,9 @@ 'posix_getegid' => ['int'], 'posix_geteuid' => ['int'], 'posix_getgid' => ['int'], -'posix_getgrgid' => ['array|false', 'gid'=>'int'], -'posix_getgrnam' => ['array|false', 'groupname'=>'string'], -'posix_getgroups' => ['array|false'], +'posix_getgrgid' => ['array{name: string, passwd: string, gid: int, members: list}|false', 'gid'=>'int'], +'posix_getgrnam' => ['array{name: string, passwd: string, gid: int, members: list}|false', 'groupname'=>'string'], +'posix_getgroups' => ['list|false'], 'posix_getlogin' => ['string|false'], 'posix_getpgid' => ['int|false', 'pid'=>'int'], 'posix_getpgrp' => ['int'], @@ -8843,10 +9081,10 @@ 'preg_replace' => ['string|array|null', 'regex'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], 'preg_replace_callback' => ['string|array|null', 'regex'=>'string|array', 'callback'=>'callable(array):string', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], 'preg_replace_callback_array' => ['string|array|null', 'pattern'=>'array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], -'preg_split' => ['array|false', 'pattern'=>'string', 'subject'=>'string', 'limit='=>'?int', 'flags='=>'int'], +'preg_split' => ['list|false', 'pattern'=>'string', 'subject'=>'string', 'limit='=>'?int', 'flags='=>'int'], 'prev' => ['mixed', '&rw_array_arg'=>'array|object'], 'print_r' => ['string|true', 'var'=>'mixed', 'return='=>'bool'], -'printf' => ['int', 'format'=>'string', '...values='=>'string|int|float'], +'printf' => ['int', 'format'=>'string', '...values='=>'__stringAndStringable|int|float|null|bool'], 'proc_close' => ['int', 'process'=>'resource'], 'proc_get_status' => ['array{command: string, pid: int, running: bool, signaled: bool, stopped: bool, exitcode: int, termsig: int, stopsig: int}|false', 'process'=>'resource'], 'proc_nice' => ['bool', 'priority'=>'int'], @@ -9239,222 +9477,269 @@ 'RecursiveTreeIterator::setPostfix' => ['void', 'prefix'=>'string'], 'RecursiveTreeIterator::setPrefixPart' => ['void', 'part'=>'int', 'prefix'=>'string'], 'RecursiveTreeIterator::valid' => ['bool'], -'Redis::__construct' => ['void'], -'Redis::_prefix' => ['string', 'value'=>'mixed'], -'Redis::_serialize' => ['mixed', 'value'=>'mixed'], -'Redis::_unserialize' => ['mixed', 'value'=>'string'], -'Redis::append' => ['int', 'key'=>'string', 'value'=>'string'], -'Redis::auth' => ['bool', 'password'=>'string|string[]'], -'Redis::bgRewriteAOF' => ['bool'], -'Redis::bgSave' => ['bool'], -'Redis::bitCount' => ['int', 'key'=>'string'], -'Redis::bitOp' => ['int', 'operation'=>'string', '...args'=>'string'], -'Redis::bitpos' => ['int', 'key'=>'string', 'bit'=>'int', 'start='=>'int', 'end='=>'int'], -'Redis::blPop' => ['array', 'keys'=>'string[]', 'timeout'=>'int'], +'Redis::__construct' => ['void', 'options='=>'?array{host?:string,port?:int,connectTimeout?:float,auth?:list{string|null|false,string}|list{string},ssl?:array,backoff?:array}'], +'Redis::_compress' => ['string', 'value'=>'string'], +'Redis::_uncompress' => ['string', 'value'=>'string'], +'Redis::_prefix' => ['string', 'key'=>'mixed'], +'Redis::_serialize' => ['mixed', 'value'=>'string'], +'Redis::_unserialize' => ['string', 'value'=>'mixed'], +'Redis::_pack' => ['mixed', 'value'=>'string'], +'Redis::_unpack' => ['string', 'value'=>'mixed'], +'Redis::acl' => ['mixed', 'subcmd'=>'string', '...args='=>'string'], +'Redis::append' => ['__benevolent', 'key'=>'string', 'value'=>'string'], +'Redis::auth' => ['__benevolent', 'credentials'=>'string|string[]'], +'Redis::bgrewriteaof' => ['__benevolent'], +'Redis::bgSave' => ['__benevolent'], +'Redis::bitcount' => ['__benevolent', 'key'=>'string', 'start='=>'int', 'end='=>'int', 'bybit='=>'bool'], +'Redis::bitop' => ['__benevolent', 'operation'=>'string', 'deskey'=>'string', 'srckey'=>'string', '...other_keys'=>'string'], +'Redis::bitpos' => ['__benevolent', 'key'=>'string', 'bit'=>'bool', 'start='=>'int', 'end='=>'int', 'bybit='=>'bool'], +'Redis::blmove' => ['__benevolent', 'src'=>'string', 'dst'=>'string', 'wherefrom'=>'string', 'whereto'=>'string', 'timeout'=>'float'], +'Redis::blmpop' => ['__benevolent', 'timeout'=>'float', 'keys'=>'string[]', 'from'=>'string', 'count='=>'int'], +'Redis::blPop' => ['__benevolent', 'key_or_keys'=>'string|string[]', 'timeout_or_key'=>'string|float|int', '...extra_args'=>'mixed'], 'Redis::blPop\'1' => ['array', 'key'=>'string', 'timeout_or_key'=>'int|string', '...extra_args'=>'int|string'], -'Redis::brPop' => ['array', 'keys'=>'string[]', 'timeout'=>'int'], +'Redis::brPop' => ['__benevolent', 'key_or_keys'=>'string|string[]', 'timeout_or_key'=>'string|float|int', '...extra_args'=>'mixed'], 'Redis::brPop\'1' => ['array', 'key'=>'string', 'timeout_or_key'=>'int|string', '...extra_args'=>'int|string'], -'Redis::brpoplpush' => ['string|false', 'srcKey'=>'string', 'dstKey'=>'string', 'timeout'=>'int'], +'Redis::brpoplpush' => ['__benevolent', 'src'=>'string', 'dst'=>'string', 'timeout'=>'int|float'], +'Redis::bzPopMax' => ['__benevolent', 'key'=>'string|string[]', 'timeout_or_key'=>'string|int', '...extra_args'=>'mixed'], +'Redis::bzPopMin' => ['__benevolent', 'key'=>'string|string[]', 'timeout_or_key'=>'string|int', '...extra_args'=>'mixed'], +'Redis::bzmpop' => ['__benevolent', 'timeout'=>'float', 'keys'=>'string[]', 'from'=>'string', 'count='=>'int'], 'Redis::clearLastError' => ['bool'], -'Redis::client' => ['mixed', 'command'=>'string', 'arg='=>'string'], +'Redis::clearTransferredBytes' => ['void'], +'Redis::client' => ['mixed', 'opt'=>'string', '...args='=>'mixed'], 'Redis::close' => ['bool'], -'Redis::config' => ['string', 'operation'=>'string', 'key'=>'string', 'value='=>'string'], -'Redis::connect' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'reserved='=>'null', 'retry_interval='=>'?int', 'read_timeout='=>'float'], -'Redis::dbSize' => ['int'], -'Redis::decr' => ['int', 'key'=>'string'], -'Redis::decrBy' => ['int', 'key'=>'string', 'value'=>'int'], +'Redis::command' => ['mixed', 'opt='=>'?string', '...args'=>'mixed'], +'Redis::config' => ['mixed', 'operation'=>'string', 'key_or_settings='=>'array|string[]|string|null', 'value='=>'?string'], +'Redis::connect' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'persistent_id='=>'?string', 'retry_interval='=>'int', 'read_timeout='=>'float', 'context='=>'?array{auth?:list{string|null|false,string}|list{string},stream?:array}'], +'Redis::copy' => ['__benevolent', 'src'=>'string', 'dst'=>'string', 'options='=>'?array'], +'Redis::dbSize' => ['__benevolent'], +'Redis::debug' => ['__benevolent', 'key'=>'string'], +'Redis::decr' => ['__benevolent', 'key'=>'string', 'by='=>'int'], +'Redis::decrBy' => ['__benevolent', 'key'=>'string', 'value'=>'int'], 'Redis::decrByFloat' => ['float', 'key'=>'string', 'value'=>'float'], -'Redis::del' => ['int', 'key'=>'string', '...args'=>'string'], +'Redis::del' => ['__benevolent', 'key'=>'string[]|string', '...other_keys='=>'string'], 'Redis::del\'1' => ['int', 'key'=>'string[]'], -'Redis::delete' => ['int', 'key'=>'string', '...args'=>'string'], +'Redis::delete' => ['__benevolent', 'key'=>'string[]|string', '...other_keys='=>'string'], 'Redis::delete\'1' => ['int', 'key'=>'string[]'], -'Redis::discard' => [''], -'Redis::dump' => ['string|false', 'key'=>'string'], -'Redis::echo' => ['string', 'message'=>'string'], -'Redis::eval' => ['mixed', 'script'=>'', 'args='=>'', 'numKeys='=>''], -'Redis::evalSha' => ['mixed', 'scriptSha'=>'string', 'args='=>'array', 'numKeys='=>'int'], +'Redis::discard' => ['__benevolent'], +'Redis::dump' => ['__benevolent', 'key'=>'string'], +'Redis::echo' => ['__benevolent', 'str'=>'string'], +'Redis::eval' => ['mixed', 'script'=>'string', 'args='=>'array', 'num_keys='=>'int'], +'Redis::eval_ro' => ['mixed', 'script'=>'string', 'args='=>'array', 'num_keys='=>'int'], +'Redis::evalsha' => ['mixed', 'sha1'=>'string', 'args='=>'array', 'num_keys='=>'int'], +'Redis::evalsha_ro' => ['mixed', 'sha1'=>'string', 'args='=>'array', 'num_keys='=>'int'], 'Redis::evaluate' => ['mixed', 'script'=>'string', 'args='=>'array', 'numKeys='=>'int'], -'Redis::evaluateSha' => ['', 'scriptSha'=>'string', 'args='=>'array', 'numKeys='=>'int'], -'Redis::exec' => ['array'], -'Redis::exists' => ['int', 'keys'=>'string|string[]'], +'Redis::exec' => ['__benevolent'], +'Redis::exists' => ['__benevolent', 'keys'=>'string|string[]', '...other_keys='=>'string'], 'Redis::exists\'1' => ['int', '...keys'=>'string'], -'Redis::expire' => ['bool', 'key'=>'string', 'ttl'=>'int'], -'Redis::expireAt' => ['bool', 'key'=>'string', 'expiry'=>'int'], -'Redis::flushAll' => ['bool', 'async='=>'bool'], -'Redis::flushDb' => ['bool', 'async='=>'bool'], -'Redis::geoAdd' => ['int', 'key'=>'string', 'longitude'=>'float', 'latitude'=>'float', 'member'=>'string', '...other_triples='=>'string|int|float'], -'Redis::geoDist' => ['float', 'key'=>'string', 'member1'=>'string', 'member2'=>'string', 'unit='=>'string'], -'Redis::geoHash' => ['array', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], -'Redis::geoPos' => ['array', 'key'=>'string', 'member'=>'string', '...members'=>'string'], -'Redis::geoRadius' => ['array|int', 'key'=>'string', 'longitude'=>'float', 'latitude'=>'float', 'radius'=>'float', 'unit'=>'float', 'options='=>'array'], -'Redis::geoRadiusByMember' => ['array|int', 'key'=>'string', 'member'=>'string', 'radius'=>'float', 'units'=>'string', 'options='=>'array'], -'Redis::get' => ['string|false', 'key'=>'string'], +'Redis::expire' => ['__benevolent', 'key'=>'string', 'timeout'=>'int', 'mode='=>'?string'], +'Redis::expireAt' => ['__benevolent', 'key'=>'string', 'timeout'=>'int', 'mode='=>'?string'], +'Redis::expiretime' => ['__benevolent', 'key'=>'string'], +'Redis::failover' => ['__benevolent', 'to='=>'?array', 'abort='=>'bool', 'timeout='=>'int'], +'Redis::fcall' => ['mixed', 'fn'=>'string', 'keys='=>'string[]', 'args='=>'array'], +'Redis::fcall_ro' => ['mixed', 'fn'=>'string', 'keys='=>'string[]', 'args='=>'array'], +'Redis::flushAll' => ['__benevolent', 'sync='=>'?bool'], +'Redis::flushDb' => ['__benevolent', 'sync='=>'?bool'], +'Redis::function' => ['__benevolent', 'operation'=>'string', '...args='=>'mixed'], +'Redis::geoadd' => ['__benevolent', 'key'=>'string', 'lng'=>'float', 'lat'=>'float', 'member'=>'string', '...other_triples_and_options='=>'mixed'], +'Redis::geodist' => ['__benevolent', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], +'Redis::geohash' => ['__benevolent|false>', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], +'Redis::geopos' => ['__benevolent|false>', 'key'=>'string', 'member'=>'string', '...other_members'=>'string'], +'Redis::georadius' => ['__benevolent>', 'key'=>'string', 'lng'=>'float', 'lat'=>'float', 'radius'=>'float', 'unit'=>'string', 'options='=>'array'], +'Redis::georadiusbymember' => ['__benevolent>', 'key'=>'string', 'lng'=>'float', 'lat'=>'float', 'radius'=>'float', 'unit'=>'string', 'options='=>'array'], +'Redis::georadiusbymember_ro' => ['__benevolent>', 'key'=>'string', 'lng'=>'float', 'lat'=>'float', 'radius'=>'float', 'unit'=>'string', 'options='=>'array'], +'Redis::geosearch' => ['__benevolent>', 'key'=>'string', 'position'=>'array|string', 'shape'=>'array|int|float', 'unit'=>'string', 'options='=>'array'], +'Redis::geosearchstore' => ['__benevolent|int|false>', 'dst'=>'string', 'src'=>'string', 'position'=>'array|string', 'shape'=>'array|int|float', 'unit'=>'string', 'options='=>'array'], +'Redis::get' => ['mixed', 'key'=>'string'], 'Redis::getAuth' => ['string|false|null'], -'Redis::getBit' => ['int', 'key'=>'string', 'offset'=>'int'], +'Redis::getBit' => ['__benevolent', 'key'=>'string', 'idx'=>'int'], +'Redis::getEx' => ['__benevolent', 'key'=>'string', 'options'=>'?array{EX?:int,PX?:int,EXAT?:int,PXAT?:int,PERSIST?:bool}'], +'Redis::getDBNum' => ['int'], +'Redis::getDel' => ['__benevolent', 'key'=>'string'], +'Redis::getHost' => ['string'], 'Redis::getKeys' => ['array', 'pattern'=>'string'], -'Redis::getLastError' => ['null|string'], +'Redis::getLastError' => ['?string'], 'Redis::getMode' => ['int'], 'Redis::getMultiple' => ['array', 'keys'=>'string[]'], 'Redis::getOption' => ['int', 'name'=>'int'], -'Redis::getRange' => ['int', 'key'=>'string', 'start'=>'int', 'end'=>'int'], -'Redis::getSet' => ['string', 'key'=>'string', 'string'=>'string'], -'Redis::hDel' => ['int|false', 'key'=>'string', 'hashKey1'=>'string', '...otherHashKeys='=>'string'], -'Redis::hExists' => ['bool', 'key'=>'string', 'hashKey'=>'string'], -'Redis::hGet' => ['string|false', 'key'=>'string', 'hashKey'=>'string'], -'Redis::hGetAll' => ['array', 'key'=>'string'], -'Redis::hIncrBy' => ['int', 'key'=>'string', 'hashKey'=>'string', 'value'=>'int'], -'Redis::hIncrByFloat' => ['float', 'key'=>'string', 'field'=>'string', 'increment'=>'float'], -'Redis::hKeys' => ['array', 'key'=>'string'], -'Redis::hLen' => ['int|false', 'key'=>'string'], -'Redis::hMGet' => ['array', 'key'=>'string', 'hashKeys'=>'array'], -'Redis::hMSet' => ['bool', 'key'=>'string', 'hashKeys'=>'array'], -'Redis::hScan' => ['array', 'key'=>'string', '&iterator'=>'int', 'pattern='=>'string', 'count='=>'int'], -'Redis::hSet' => ['int|false', 'key'=>'string', 'hashKey'=>'string', 'value'=>'string'], -'Redis::hSetNx' => ['bool', 'key'=>'string', 'hashKey'=>'string', 'value'=>'string'], -'Redis::hVals' => ['array', 'key'=>'string'], -'Redis::incr' => ['int', 'key'=>'string'], -'Redis::incrBy' => ['int', 'key'=>'string', 'value'=>'int'], -'Redis::incrByFloat' => ['float', 'key'=>'string', 'value'=>'float'], -'Redis::info' => ['array', 'option='=>'string'], -'Redis::keys' => ['array', 'pattern'=>'string'], +'Redis::getPersistentID' => ['?string'], +'Redis::getPort' => ['int'], +'Redis::getRange' => ['__benevolent', 'key'=>'string', 'start'=>'int', 'end'=>'int'], +'Redis::getReadTimeout' => ['float'], +'Redis::getset' => ['__benevolent', 'key'=>'string', 'value'=>'mixed'], +'Redis::getTimeout' => ['float|false'], +'Redis::getTransferredBytes' => ['array'], +'Redis::hDel' => ['__benevolent', 'key'=>'string', 'field'=>'string', '...other_fields='=>'string'], +'Redis::hExists' => ['__benevolent', 'key'=>'string', 'field'=>'string'], +'Redis::hGet' => ['__benevolent', 'key'=>'string', 'member'=>'string'], +'Redis::hGetAll' => ['__benevolent', 'key'=>'string'], +'Redis::hIncrBy' => ['__benevolent', 'key'=>'string', 'field'=>'string', 'value'=>'int'], +'Redis::hIncrByFloat' => ['__benevolent', 'key'=>'string', 'field'=>'string', 'value'=>'float'], +'Redis::hKeys' => ['__benevolent', 'key'=>'string'], +'Redis::hLen' => ['__benevolent', 'key'=>'string'], +'Redis::hMget' => ['__benevolent|false>', 'key'=>'string', 'fields'=>'string[]'], +'Redis::hMset' => ['__benevolent', 'key'=>'string', 'fieldvals'=>'array'], +'Redis::hRandField' => ['__benevolent>', 'key'=>'string', 'options'=>'?array{COUNT?:int,WITHVALUES?:bool}'], +'Redis::hscan' => ['__benevolent|bool>', 'key'=>'string', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'int'], +'Redis::hSet' => ['__benevolent', 'key'=>'string', 'member'=>'string', 'value'=>'mixed'], +'Redis::hSetNx' => ['__benevolent', 'key'=>'string', 'field'=>'string', 'value'=>'string'], +'Redis::hStrLen' => ['__benevolent', 'key'=>'string', 'field'=>'string'], +'Redis::hVals' => ['__benevolent', 'key'=>'string'], +'Redis::incr' => ['__benevolent', 'key'=>'string', 'by='=>'int'], +'Redis::incrBy' => ['__benevolent', 'key'=>'string', 'value'=>'int'], +'Redis::incrByFloat' => ['__benevolent', 'key'=>'string', 'value'=>'float'], +'Redis::info' => ['__benevolent|false>', '...sections='=>'string'], +'Redis::isConnected' => ['bool'], +'Redis::keys' => ['__benevolent|false>', 'pattern'=>'string'], 'Redis::lastSave' => ['int'], +'Redis::lcs' => ['__benevolent', 'key1'=>'string', 'key2'=>'string', 'options'=>'?array{MINMATCHLEN?:int,WITHMATCHLEN?:bool,LEN?:bool,IDX?:bool}'], 'Redis::lGet' => ['', 'key'=>'string', 'index'=>'int'], 'Redis::lGetRange' => ['', 'key'=>'string', 'start'=>'int', 'end'=>'int'], -'Redis::lIndex' => ['string|false', 'key'=>'string', 'index'=>'int'], -'Redis::lInsert' => ['int', 'key'=>'string', 'position'=>'int', 'pivot'=>'string', 'value'=>'string'], +'Redis::lindex' => ['null|string|false', 'key'=>'string', 'index'=>'int'], +'Redis::lInsert' => ['__benevolent', 'key'=>'string', 'pos'=>'int', 'pivot'=>'mixed', 'value'=>'mixed'], 'Redis::listTrim' => ['', 'key'=>'string', 'start'=>'int', 'stop'=>'int'], -'Redis::lLen' => ['int|false', 'key'=>'string'], -'Redis::lPop' => ['string', 'key'=>'string'], -'Redis::lPush' => ['int|false', 'key'=>'string', 'value1'=>'string', 'value2='=>'string', 'valueN='=>'string'], -'Redis::lPushx' => ['int|false', 'key'=>'string', 'value'=>'string'], -'Redis::lRange' => ['array', 'key'=>'string', 'start'=>'int', 'end'=>'int'], -'Redis::lRem' => ['int|false', 'key'=>'string', 'value'=>'string', 'count'=>'int'], -'Redis::lRemove' => ['', 'key'=>'string', 'value'=>'string', 'count'=>'int'], -'Redis::lSet' => ['bool', 'key'=>'string', 'index'=>'int', 'value'=>'string'], +'Redis::lLen' => ['__benevolent', 'key'=>'string'], +'Redis::lMove' => ['__benevolent', 'src'=>'string', 'dst'=>'string', 'wherefrom'=>'string', 'whereto'=>'string'], +'Redis::lmpop' => ['__benevolent|null|false>', 'keys'=>'string[]', 'from'=>'string', 'count='=>'int'], +'Redis::lPop' => ['__benevolent', 'key'=>'string', 'count='=>'int'], +'Redis::lPos' => ['__benevolent', 'key'=>'string', 'value'=>'mixed', 'options'=>'?array{COUNT?:int,RANK?:int,MAXLEN?:int}'], +'Redis::lPush' => ['__benevolent', 'key'=>'string', '...elements='=>'mixed'], +'Redis::lPushx' => ['__benevolent', 'key'=>'string', 'value'=>'mixed'], +'Redis::lrange' => ['__benevolent', 'key'=>'string', 'start'=>'int', 'end'=>'int'], +'Redis::lrem' => ['__benevolent', 'key'=>'string', 'value'=>'mixed', 'count='=>'int'], +'Redis::lSet' => ['__benevolent', 'key'=>'string', 'index'=>'int', 'value'=>'mixed'], 'Redis::lSize' => ['', 'key'=>'string'], -'Redis::lTrim' => ['array|false', 'key'=>'string', 'start'=>'int', 'stop'=>'int'], -'Redis::mGet' => ['array', 'keys'=>'string[]'], -'Redis::migrate' => ['bool', 'host'=>'string', 'port'=>'int', 'key'=>'string|string[]', 'db'=>'int', 'timeout'=>'int', 'copy='=>'bool', 'replace='=>'bool'], -'Redis::move' => ['bool', 'key'=>'string', 'dbindex'=>'int'], -'Redis::mSet' => ['bool', 'pairs'=>'array'], -'Redis::mSetNx' => ['bool', 'pairs'=>'array'], -'Redis::multi' => ['Redis', 'mode='=>'int'], -'Redis::object' => ['string|long|false', 'info'=>'string', 'key'=>'string'], -'Redis::open' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'reserved='=>'null', 'retry_interval='=>'?int', 'read_timeout='=>'float'], -'Redis::pconnect' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'persistent_id='=>'string', 'retry_interval='=>'?int'], -'Redis::persist' => ['bool', 'key'=>'string'], -'Redis::pExpire' => ['bool', 'key'=>'string', 'ttl'=>'int'], -'Redis::pexpireAt' => ['bool', 'key'=>'string', 'expiry'=>'int'], -'Redis::pfAdd' => ['bool', 'key'=>'string', 'elements'=>'array'], -'Redis::pfCount' => ['int', 'key'=>'array|string'], -'Redis::pfMerge' => ['bool', 'destkey'=>'string', 'sourcekeys'=>'array'], -'Redis::ping' => ['string'], -'Redis::popen' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'persistent_id='=>'string', 'retry_interval='=>'?int'], -'Redis::psetex' => ['bool', 'key'=>'string', 'ttl'=>'int', 'value'=>'string'], -'Redis::psubscribe' => ['', 'patterns'=>'array', 'callback'=>'array|string'], -'Redis::pttl' => ['int|false', 'key'=>'string'], -'Redis::publish' => ['int', 'channel'=>'string', 'message'=>'string'], -'Redis::pubsub' => ['array|int', 'keyword'=>'string', 'argument'=>'array|string'], -'Redis::punsubscribe' => ['', 'pattern'=>'string', '...other_patterns='=>'string'], -'Redis::randomKey' => ['string'], -'Redis::rawCommand' => ['mixed', 'command'=>'string', '...arguments='=>'mixed'], -'Redis::rename' => ['bool', 'srckey'=>'string', 'dstkey'=>'string'], +'Redis::ltrim' => ['__benevolent', 'key'=>'string', 'start'=>'int', 'end'=>'int'], +'Redis::mget' => ['__benevolent>', 'keys'=>'string[]'], +'Redis::migrate' => ['__benevolent', 'host'=>'string', 'port'=>'int', 'key'=>'string|string[]', 'dstdb'=>'int', 'timeout'=>'int', 'copy='=>'bool', 'replace='=>'bool', 'credentials='=>'mixed'], +'Redis::move' => ['__benevolent', 'key'=>'string', 'index'=>'int'], +'Redis::mset' => ['__benevolent', 'key_values'=>'array'], +'Redis::msetnx' => ['__benevolent', 'key_values'=>'array'], +'Redis::multi' => ['__benevolent', 'value='=>'int'], +'Redis::object' => ['__benevolent', 'subcommand'=>'string', 'key'=>'string'], +'Redis::open' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'persistent_id='=>'?string', 'retry_interval='=>'int', 'read_timeout='=>'float', 'context='=>'?array{auth?:list{string|null|false,string}|list{string},stream?:array}'], +'Redis::pconnect' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'persistent_id='=>'?string', 'retry_interval='=>'int', 'read_timeout='=>'float', 'context='=>'?array{auth?:list{string|null|false,string}|list{string},stream?:array}'], +'Redis::persist' => ['__benevolent', 'key'=>'string'], +'Redis::pexpire' => ['bool', 'key'=>'string', 'timeout'=>'int', 'mode='=>'?string'], +'Redis::pexpireAt' => ['__benevolent', 'key'=>'string', 'timestamp'=>'int', 'mode='=>'?string'], +'Redis::pexpiretime' => ['__benevolent', 'key'=>'string'], +'Redis::pfadd' => ['__benevolent', 'key'=>'string', 'elements'=>'array'], +'Redis::pfcount' => ['__benevolent', 'key_or_keys'=>'string[]|string'], +'Redis::pfmerge' => ['__benevolent', 'dst'=>'string', 'srckeys'=>'string[]'], +'Redis::ping' => ['__benevolent', 'message='=>'?string'], +'Redis::pipeline' => ['__benevolent'], +'Redis::popen' => ['bool', 'host'=>'string', 'port='=>'int', 'timeout='=>'float', 'persistent_id='=>'?string', 'retry_interval='=>'int', 'read_timeout='=>'float', 'context='=>'?array{auth?:list{string|null|false,string}|list{string},stream?:array}'], +'Redis::psetex' => ['__benevolent', 'key'=>'string', 'expire'=>'int', 'value'=>'mixed'], +'Redis::psubscribe' => ['bool', 'patterns'=>'string[]', 'cb'=>'callable'], +'Redis::pttl' => ['__benevolent', 'key'=>'string'], +'Redis::publish' => ['__benevolent', 'channel'=>'string', 'message'=>'string'], +'Redis::pubsub' => ['array|int', 'command'=>'string', 'arg'=>'array|string'], +'Redis::punsubscribe' => ['__benevolent', 'patterns='=>'string[]'], +'Redis::randomKey' => ['__benevolent'], +'Redis::rawcommand' => ['mixed', 'command'=>'string', '...args='=>'mixed'], +'Redis::rename' => ['__benevolent', 'old_name'=>'string', 'new_name'=>'string'], 'Redis::renameKey' => ['bool', 'srckey'=>'string', 'dstkey'=>'string'], -'Redis::renameNx' => ['bool', 'srckey'=>'string', 'dstkey'=>'string'], +'Redis::renameNx' => ['__benevolent', 'old_name'=>'string', 'new_name'=>'string'], +'Redis::reset' => ['__benevolent'], 'Redis::resetStat' => ['bool'], -'Redis::restore' => ['bool', 'key'=>'string', 'ttl'=>'int', 'value'=>'string'], -'Redis::rPop' => ['string', 'key'=>'string'], -'Redis::rpoplpush' => ['string', 'srcKey'=>'string', 'dstKey'=>'string'], -'Redis::rPush' => ['int|false', 'key'=>'string', 'value1'=>'string', 'value2='=>'string', 'valueN='=>'string'], -'Redis::rPushx' => ['int|false', 'key'=>'string', 'value'=>'string'], -'Redis::sAdd' => ['int|false', 'key'=>'string', 'value1'=>'string', 'value2='=>'string', 'valueN='=>'string'], -'Redis::sAddArray' => ['bool', 'key'=>'string', 'values'=>'array'], -'Redis::save' => ['bool'], -'Redis::scan' => ['array|false', '&w_iterator'=>'?int', 'pattern='=>'?string', 'count='=>'?int'], -'Redis::sCard' => ['int', 'key'=>'string'], +'Redis::restore' => ['__benevolent', 'key'=>'string', 'ttl'=>'int', 'value'=>'string', 'options='=>'?array{ABSTTL?:bool,REPLACE?:bool,IDLETIME?:int,FREQ?:int}'], +'Redis::role' => ['mixed'], +'Redis::rPop' => ['__benevolent|string|bool>', 'key'=>'string', 'count='=>'int'], +'Redis::rpoplpush' => ['__benevolent', 'srckey'=>'string', 'dstkey'=>'string'], +'Redis::rPush' => ['__benevolent', 'key'=>'string', '...elements='=>'mixed'], +'Redis::rPushx' => ['__benevolent', 'key'=>'string', 'value'=>'mixed'], +'Redis::sAdd' => ['__benevolent', 'key'=>'string', 'value'=>'mixed', '...other_values='=>'string'], +'Redis::sAddArray' => ['int', 'key'=>'string', 'values'=>'array'], +'Redis::save' => ['__benevolent'], +'Redis::scan' => ['array|false', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'?int', 'type='=>'?string'], +'Redis::scard' => ['__benevolent', 'key'=>'string'], 'Redis::sContains' => ['', 'key'=>'string', 'value'=>'string'], 'Redis::script' => ['mixed', 'command'=>'string', '...args='=>'mixed'], -'Redis::sDiff' => ['array', 'key1'=>'string', '...other_keys='=>'string'], -'Redis::sDiffStore' => ['int|false', 'dstKey'=>'string', 'key'=>'string', '...other_keys='=>'string'], -'Redis::select' => ['bool', 'dbindex'=>'int'], -'Redis::set' => ['bool', 'key'=>'string', 'value'=>'mixed', 'options='=>'array'], +'Redis::sDiff' => ['__benevolent|false>', 'key'=>'string', '...other_keys='=>'string'], +'Redis::sDiffStore' => ['__benevolent', 'dst'=>'string', 'key'=>'string', '...other_keys='=>'string'], +'Redis::select' => ['__benevolent', 'db'=>'int'], +'Redis::set' => ['__benevolent', 'key'=>'string', 'value'=>'mixed', 'options='=>'array'], 'Redis::set\'1' => ['bool', 'key'=>'string', 'value'=>'mixed', 'timeout='=>'int'], -'Redis::setBit' => ['int', 'key'=>'string', 'offset'=>'int', 'value'=>'int'], -'Redis::setEx' => ['bool', 'key'=>'string', 'ttl'=>'int', 'value'=>'string'], -'Redis::setex' => ['bool', 'key'=>'string', 'ttl'=>'int', 'value'=>'string'], -'Redis::setNx' => ['bool', 'key'=>'string', 'value'=>'string'], -'Redis::setnx' => ['bool', 'key'=>'string', 'value'=>'string'], -'Redis::setOption' => ['bool', 'name'=>'int', 'value'=>'mixed'], -'Redis::setRange' => ['int', 'key'=>'string', 'offset'=>'int', 'end'=>'string'], +'Redis::setBit' => ['__benevolent', 'key'=>'string', 'idx'=>'int', 'value'=>'bool'], +'Redis::setex' => ['__benevolent', 'key'=>'string', 'expire'=>'int', 'value'=>'mixed'], +'Redis::setnx' => ['__benevolent', 'key'=>'string', 'value'=>'mixed'], +'Redis::setOption' => ['bool', 'option'=>'int', 'value'=>'mixed'], +'Redis::setRange' => ['__benevolent', 'key'=>'string', 'index'=>'int', 'value'=>'string'], 'Redis::setTimeout' => ['bool', 'key'=>'string', 'ttl'=>'int'], 'Redis::sGetMembers' => ['array', 'key'=>'string'], -'Redis::sInter' => ['array|false', 'key'=>'string', '...other_keys='=>'string'], -'Redis::sInterStore' => ['int|false', 'dstKey'=>'string', 'key'=>'string', '...other_keys='=>'string'], -'Redis::sIsMember' => ['bool', 'key'=>'string', 'value'=>'string'], +'Redis::sInter' => ['__benevolent|false>', 'key'=>'string', '...other_keys='=>'string'], +'Redis::sintercard' => ['__benevolent', 'keys'=>'string[]', 'limit='=>'int'], +'Redis::sInterStore' => ['__benevolent', 'key'=>'string[]|string', '...other_keys='=>'string'], +'Redis::sismember' => ['__benevolent', 'key'=>'string', 'value'=>'mixed'], 'Redis::slave' => ['bool', 'host'=>'string', 'port'=>'int'], 'Redis::slave\'1' => ['bool', 'host'=>'string', 'port'=>'int'], -'Redis::slaveof' => ['bool', 'host='=>'string', 'port='=>'int'], -'Redis::slowLog' => ['mixed', 'operation'=>'string', 'length='=>'int'], -'Redis::sMembers' => ['array', 'key'=>'string'], -'Redis::sMove' => ['bool', 'srcKey'=>'string', 'dstKey'=>'string', 'member'=>'string'], -'Redis::sort' => ['array|int', 'key'=>'string', 'options='=>'array'], -'Redis::sPop' => ['string|false', 'key'=>'string'], -'Redis::sRandMember' => ['array|string|false', 'key'=>'string', 'count='=>'int'], -'Redis::sRem' => ['int', 'key'=>'string', 'member1'=>'string', '...other_members='=>'string'], +'Redis::slaveof' => ['__benevolent', 'host='=>'?string', 'port='=>'int'], +'Redis::slowlog' => ['mixed', 'operation'=>'string', 'length='=>'int'], +'Redis::sMembers' => ['__benevolent|false>', 'key'=>'string'], +'Redis::sMisMember' => ['__benevolent', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], +'Redis::sMove' => ['__benevolent', 'src'=>'string', 'dst'=>'string', 'value'=>'mixed'], +'Redis::sort' => ['array|int', 'key'=>'string', 'options='=>'?array{SORT?:string,ALPHA?:bool,LIMIT?:array{0:int,1:int},BY?:string,GET?:string}'], +'Redis::sort_ro' => ['array|int', 'key'=>'string', 'options='=>'?array{SORT?:string,ALPHA?:bool,LIMIT?:array{0:int,1:int},BY?:string,GET?:string}'], +'Redis::sPop' => ['__benevolent', 'key'=>'string', 'count='=>'int'], +'Redis::sRandMember' => ['__benevolent', 'key'=>'string', 'count='=>'int'], +'Redis::srem' => ['__benevolent', 'key'=>'string', 'value'=>'mixed', '...other_values='=>'mixed'], 'Redis::sRemove' => ['int', 'key'=>'string', 'member1'=>'string', '...other_members='=>'string'], -'Redis::sScan' => ['array|bool', 'key'=>'string', '&iterator'=>'int', 'pattern='=>'string', 'count='=>'int'], -'Redis::strLen' => ['0|positive-int', 'key'=>'string'], -'Redis::subscribe' => ['mixed|null', 'channels'=>'array', 'callback'=>'string|array'], +'Redis::sscan' => ['__benevolent', 'key'=>'string', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'int'], +'Redis::ssubscribe' => ['bool', 'channels'=>'string[]', 'cb'=>'callable'], +'Redis::strlen' => ['__benevolent', 'key'=>'string'], +'Redis::subscribe' => ['bool', 'channels'=>'string[]', 'cb'=>'callable'], 'Redis::substr' => ['', 'key'=>'string', 'start'=>'int', 'end'=>'int'], -'Redis::sUnion' => ['array', 'key'=>'string', '...other_keys='=>'string'], -'Redis::sUnionStore' => ['int', 'dstKey'=>'string', 'key'=>'string', '...other_keys='=>'string'], -'Redis::time' => ['array'], -'Redis::ttl' => ['int|false', 'key'=>'string'], -'Redis::type' => ['int', 'key'=>'string'], -'Redis::unlink' => ['int', 'key'=>'string', '...args'=>'string'], 'Redis::unlink\'1' => ['int', 'key'=>'string[]'], -'Redis::unsubscribe' => ['', 'channel'=>'string', '...other_channels='=>'string'], -'Redis::unwatch' => [''], -'Redis::wait' => ['int', 'numSlaves'=>'int', 'timeout'=>'int'], -'Redis::watch' => ['void', 'key'=>'string', '...other_keys='=>'string'], -'Redis::xack' => ['', 'str_key'=>'string', 'str_group'=>'string', 'arr_ids'=>'array'], -'Redis::xadd' => ['', 'str_key'=>'string', 'str_id'=>'string', 'arr_fields'=>'array', 'i_maxlen='=>'', 'boo_approximate='=>''], -'Redis::xclaim' => ['', 'str_key'=>'string', 'str_group'=>'string', 'str_consumer'=>'string', 'i_min_idle'=>'', 'arr_ids'=>'array', 'arr_opts='=>'array'], -'Redis::xdel' => ['', 'str_key'=>'string', 'arr_ids'=>'array'], -'Redis::xgroup' => ['', 'str_operation'=>'string', 'str_key='=>'string', 'str_arg1='=>'', 'str_arg2='=>'', 'str_arg3='=>''], -'Redis::xinfo' => ['', 'str_cmd'=>'string', 'str_key='=>'string', 'str_group='=>'string'], -'Redis::xlen' => ['', 'key'=>''], -'Redis::xpending' => ['', 'str_key'=>'string', 'str_group'=>'string', 'str_start='=>'', 'str_end='=>'', 'i_count='=>'', 'str_consumer='=>'string'], -'Redis::xrange' => ['', 'str_key'=>'string', 'str_start'=>'', 'str_end'=>'', 'i_count='=>''], -'Redis::xread' => ['', 'arr_streams'=>'array', 'i_count='=>'', 'i_block='=>''], -'Redis::xreadgroup' => ['', 'str_group'=>'string', 'str_consumer'=>'string', 'arr_streams'=>'array', 'i_count='=>'', 'i_block='=>''], -'Redis::xrevrange' => ['', 'str_key'=>'string', 'str_start'=>'', 'str_end'=>'', 'i_count='=>''], -'Redis::xtrim' => ['', 'str_key'=>'string', 'i_maxlen'=>'', 'boo_approximate='=>''], -'Redis::zAdd' => ['int', 'key'=>'string', 'score1'=>'float', 'value1'=>'string', 'score2='=>'float', 'value2='=>'string', 'scoreN='=>'float', 'valueN='=>'string'], +'Redis::sUnion' => ['__benevolent|false>', 'key'=>'string', '...other_keys='=>'string'], +'Redis::sUnionStore' => ['__benevolent', 'dst'=>'string', 'key'=>'string', '...other_keys='=>'string'], +'Redis::sunsubscribe' => ['__benevolent', 'channels'=>'string[]'], +'Redis::swapdb' => ['__benevolent', 'src'=>'int', 'dst'=>'int'], +'Redis::time' => ['__benevolent'], +'Redis::ttl' => ['__benevolent', 'key'=>'string'], +'Redis::type' => ['__benevolent', 'key'=>'string'], +'Redis::unlink' => ['__benevolent', 'key'=>'string[]|string', '...other_keys'=>'string'], +'Redis::unsubscribe' => ['__benevolent', 'channels'=>'string[]'], +'Redis::unwatch' => ['__benevolent'], +'Redis::wait' => ['int|false', 'numreplicas'=>'int', 'timeout'=>'int'], +'Redis::watch' => ['__benevolent', 'key'=>'string[]|string', '...other_keys='=>'string'], +'Redis::xack' => ['int|false', 'key'=>'string', 'group'=>'string', 'ids'=>'array'], +'Redis::xadd' => ['__benevolent', 'key'=>'string', 'id'=>'string', 'values'=>'array', 'maxlen='=>'int', 'approx='=>'bool', 'nomkstream='=>'bool'], +'Redis::xclaim' => ['__benevolent', 'key'=>'string', 'group'=>'string', 'consumer'=>'string', 'min_idle'=>'int', 'ids'=>'array', 'options='=>'array'], +'Redis::xdel' => ['__benevolent', 'key'=>'string', 'ids'=>'array'], +'Redis::xgroup' => ['mixed', 'operation'=>'string', 'key='=>'?string', 'group='=>'?string', 'id_or_consumer='=>'?string', 'mkstream='=>'bool', 'entries_read='=>'int'], +'Redis::xinfo' => ['mixed', 'operation'=>'string', 'arg1='=>'?string', 'arg2='=>'?string', 'count='=>'int'], +'Redis::xlen' => ['__benevolent', 'key'=>'string'], +'Redis::xpending' => ['__benevolent', 'key'=>'string', 'group'=>'string', 'start='=>'?string', 'end='=>'?string', 'count='=>'int', 'consumer='=>'?string'], +'Redis::xrange' => ['__benevolent', 'key'=>'string', 'start'=>'string', 'end'=>'string', 'count='=>'int'], +'Redis::xread' => ['__benevolent', 'streams'=>'array', 'count='=>'int', 'block='=>'int'], +'Redis::xreadgroup' => ['__benevolent', 'group'=>'string', 'consumer'=>'string', 'streams'=>'array', 'count='=>'int', 'block='=>'int'], +'Redis::xrevrange' => ['__benevolent', 'key'=>'string', 'end'=>'string', 'start'=>'string', 'count='=>'int'], +'Redis::xtrim' => ['__benevolent', 'key'=>'string', 'threshold'=>'string', 'approx='=>'bool', 'minid='=>'bool', 'limit='=>'int'], +'Redis::zAdd' => ['__benevolent', 'key'=>'string', 'score_or_options'=>'array|float', '...more_scores_and_mems='=>'mixed'], 'Redis::zAdd\'1' => ['int', 'key'=>'string', 'options'=>'array', 'score1'=>'float', 'value1'=>'string', 'score2='=>'float', 'value2='=>'string', 'scoreN='=>'float', 'valueN='=>'string'], -'Redis::zCard' => ['int', 'key'=>'string'], -'Redis::zCount' => ['int', 'key'=>'string', 'start'=>'string', 'end'=>'string'], +'Redis::zCard' => ['__benevolent', 'key'=>'string'], +'Redis::zCount' => ['__benevolent', 'key'=>'string', 'start'=>'string', 'end'=>'string'], 'Redis::zDelete' => ['int', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], 'Redis::zDeleteRangeByRank' => ['', 'key'=>'string', 'start'=>'int', 'end'=>'int'], 'Redis::zDeleteRangeByScore' => ['', 'key'=>'string', 'start'=>'float', 'end'=>'float'], -'Redis::zIncrBy' => ['float', 'key'=>'string', 'value'=>'float', 'member'=>'string'], -'Redis::zInter' => ['int', 'Output'=>'string', 'ZSetKeys'=>'array', 'Weights='=>'?array', 'aggregateFunction='=>'string'], -'Redis::zRange' => ['array', 'key'=>'string', 'start'=>'int', 'end'=>'int', 'withscores='=>'bool'], -'Redis::zRangeByLex' => ['array|false', 'key'=>'string', 'min'=>'int', 'max'=>'int', 'offset='=>'int', 'limit='=>'int'], -'Redis::zRangeByScore' => ['array', 'key'=>'string', 'start'=>'int|string', 'end'=>'int|string', 'options='=>'array'], -'Redis::zRank' => ['int', 'key'=>'string', 'member'=>'string'], -'Redis::zRem' => ['int', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], +'Redis::zIncrBy' => ['__benevolent', 'key'=>'string', 'value'=>'float', 'member'=>'mixed'], +'Redis::zInter' => ['__benevolent', 'keys'=>'string[]', 'weights='=>'?array', 'options='=>'?array'], +'Redis::zmpop' => ['__benevolent', 'keys'=>'string[]', 'from'=>'string', 'count='=>'int'], +'Redis::zRange' => ['__benevolent', 'key'=>'string', 'start'=>'string|int', 'end'=>'string|int', 'options='=>'array|bool|null'], +'Redis::zRangeByLex' => ['__benevolent', 'key'=>'string', 'min'=>'string', 'max'=>'string', 'offset='=>'int', 'limit='=>'int'], +'Redis::zRangeByScore' => ['__benevolent', 'key'=>'string', 'start'=>'string', 'end'=>'string', 'options='=>'array'], +'Redis::zRank' => ['__benevolent', 'key'=>'string', 'member'=>'mixed'], +'Redis::zRem' => ['__benevolent', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], 'Redis::zRemove' => ['int', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], -'Redis::zRemRangeByRank' => ['int', 'key'=>'string', 'start'=>'int', 'end'=>'int'], -'Redis::zRemRangeByScore' => ['int', 'key'=>'string', 'start'=>'float|string', 'end'=>'float|string'], -'Redis::zRevRange' => ['array', 'key'=>'string', 'start'=>'int', 'end'=>'int', 'withscore='=>'bool'], -'Redis::zRevRangeByLex' => ['array', 'key'=>'string', 'min'=>'int', 'max'=>'int', 'offset='=>'int', 'limit='=>'int'], -'Redis::zRevRangeByScore' => ['array', 'key'=>'string', 'start'=>'int', 'end'=>'int', 'options='=>'array'], -'Redis::zRevRank' => ['int', 'key'=>'string', 'member'=>'string'], -'Redis::zScan' => ['array|false', 'key'=>'string', '&iterator'=>'int', 'pattern='=>'string', 'count='=>'int'], -'Redis::zScore' => ['float|false', 'key'=>'string', 'member'=>'string'], +'Redis::zRemRangeByRank' => ['__benevolent', 'key'=>'string', 'start'=>'int', 'end'=>'int'], +'Redis::zRemRangeByScore' => ['__benevolent', 'key'=>'string', 'start'=>'string', 'end'=>'string'], +'Redis::zRevRange' => ['__benevolent', 'key'=>'string', 'start'=>'int', 'end'=>'int', 'scores='=>'bool|array{withscores:bool}|null'], +'Redis::zRevRangeByLex' => ['__benevolent', 'key'=>'string', 'max'=>'string', 'min'=>'string', 'offset='=>'int', 'limit='=>'int'], +'Redis::zRevRangeByScore' => ['__benevolent', 'key'=>'string', 'max'=>'string', 'min'=>'string', 'options='=>'array|bool'], +'Redis::zRevRank' => ['__benevolent', 'key'=>'string', 'member'=>'mixed'], +'Redis::zscan' => ['__benevolent', 'key'=>'string', '&iterator'=>'?int', 'pattern='=>'?string', 'count='=>'int'], +'Redis::zScore' => ['__benevolent', 'key'=>'string', 'member'=>'mixed'], 'Redis::zSize' => ['', 'key'=>'string'], -'Redis::zUnion' => ['int', 'Output'=>'string', 'ZSetKeys'=>'array', 'Weights='=>'?array', 'aggregateFunction='=>'string'], +'Redis::zUnion' => ['__benevolent', 'keys'=>'string[]', 'weights'=>'?array', 'options='=>'?array'], 'RedisArray::__construct' => ['void', 'name'=>'string'], 'RedisArray::__construct\'1' => ['void', 'hosts'=>'array', 'opts='=>'array'], 'RedisArray::_function' => ['string'], @@ -9574,7 +9859,7 @@ 'RedisCluster::scan' => ['array', '&iterator'=>'int', 'pattern='=>'string', 'count='=>'int'], 'RedisCluster::sCard' => ['int', 'key'=>'string'], 'RedisCluster::script' => ['mixed', 'nodeParams'=>'string', 'command'=>'string', 'script'=>'string'], -'RedisCluster::sDiff' => ['array', 'key1'=>'string', 'key2'=>'string', '...other_keys='=>'string'], +'RedisCluster::sDiff' => ['list', 'key1'=>'string', 'key2'=>'string', '...other_keys='=>'string'], 'RedisCluster::sDiffStore' => ['int', 'dstKey'=>'string', 'key1'=>'string', '...other_keys='=>'string'], 'RedisCluster::set' => ['bool', 'key'=>'string', 'value'=>'string', 'timeout='=>'array|int'], 'RedisCluster::setBit' => ['int', 'key'=>'string', 'offset'=>'int', 'value'=>'bool|int'], @@ -9582,11 +9867,11 @@ 'RedisCluster::setnx' => ['bool', 'key'=>'string', 'value'=>'string'], 'RedisCluster::setOption' => ['bool', 'name'=>'int', 'value'=>'mixed'], 'RedisCluster::setRange' => ['string', 'key'=>'string', 'offset'=>'int', 'value'=>'string'], -'RedisCluster::sInter' => ['array', 'key'=>'string', '...other_keys='=>'string'], +'RedisCluster::sInter' => ['list', 'key'=>'string', '...other_keys='=>'string'], 'RedisCluster::sInterStore' => ['int', 'dstKey'=>'string', 'key'=>'string', '...other_keys='=>'string'], 'RedisCluster::sIsMember' => ['bool', 'key'=>'string', 'value'=>'string'], 'RedisCluster::slowLog' => ['', 'nodeParams'=>'string', 'command'=>'string', 'argument'=>'mixed', '...other_arguments='=>'mixed'], -'RedisCluster::sMembers' => ['array', 'key'=>'string'], +'RedisCluster::sMembers' => ['list', 'key'=>'string'], 'RedisCluster::sMove' => ['bool', 'srcKey'=>'string', 'dstKey'=>'string', 'member'=>'string'], 'RedisCluster::sort' => ['array', 'key'=>'string', 'option='=>'array'], 'RedisCluster::sPop' => ['string', 'key'=>'string'], @@ -9653,24 +9938,24 @@ 'ReflectionClass::getExtension' => ['ReflectionExtension|null'], 'ReflectionClass::getExtensionName' => ['string|false'], 'ReflectionClass::getFileName' => ['string|false'], -'ReflectionClass::getInterfaceNames' => ['array'], +'ReflectionClass::getInterfaceNames' => ['list'], 'ReflectionClass::getInterfaces' => ['array'], 'ReflectionClass::getMethod' => ['ReflectionMethod', 'name'=>'string'], -'ReflectionClass::getMethods' => ['array', 'filter='=>'int'], +'ReflectionClass::getMethods' => ['list', 'filter='=>'int'], 'ReflectionClass::getModifiers' => ['int'], 'ReflectionClass::getName' => ['class-string'], 'ReflectionClass::getNamespaceName' => ['string'], 'ReflectionClass::getParentClass' => ['ReflectionClass|false'], -'ReflectionClass::getProperties' => ['array', 'filter='=>'int'], +'ReflectionClass::getProperties' => ['list', 'filter='=>'int'], 'ReflectionClass::getProperty' => ['ReflectionProperty', 'name'=>'string'], 'ReflectionClass::getReflectionConstant' => ['ReflectionClassConstant|false', 'name'=>'string'], -'ReflectionClass::getReflectionConstants' => ['array'], +'ReflectionClass::getReflectionConstants' => ['list'], 'ReflectionClass::getShortName' => ['string'], 'ReflectionClass::getStartLine' => ['int|false'], 'ReflectionClass::getStaticProperties' => ['array'], 'ReflectionClass::getStaticPropertyValue' => ['mixed', 'name'=>'string', 'default='=>'mixed'], 'ReflectionClass::getTraitAliases' => ['array'], -'ReflectionClass::getTraitNames' => ['array'], +'ReflectionClass::getTraitNames' => ['list'], 'ReflectionClass::getTraits' => ['array'], 'ReflectionClass::hasConstant' => ['bool', 'name'=>'string'], 'ReflectionClass::hasMethod' => ['bool', 'name'=>'string'], @@ -9710,7 +9995,7 @@ 'ReflectionExtension::__toString' => ['string'], 'ReflectionExtension::export' => ['string|null', 'name'=>'string', 'return='=>'bool'], 'ReflectionExtension::getClasses' => ['array'], -'ReflectionExtension::getClassNames' => ['array'], +'ReflectionExtension::getClassNames' => ['list'], 'ReflectionExtension::getConstants' => ['array'], 'ReflectionExtension::getDependencies' => ['array'], 'ReflectionExtension::getFunctions' => ['array'], @@ -9735,7 +10020,7 @@ 'ReflectionFunction::getNamespaceName' => ['string'], 'ReflectionFunction::getNumberOfParameters' => ['int'], 'ReflectionFunction::getNumberOfRequiredParameters' => ['int'], -'ReflectionFunction::getParameters' => ['array'], +'ReflectionFunction::getParameters' => ['list'], 'ReflectionFunction::getReturnType' => ['?ReflectionType'], 'ReflectionFunction::getShortName' => ['string'], 'ReflectionFunction::getStartLine' => ['int|false'], @@ -9757,14 +10042,14 @@ 'ReflectionFunctionAbstract::getClosureThis' => ['object|null'], 'ReflectionFunctionAbstract::getDocComment' => ['string|false'], 'ReflectionFunctionAbstract::getEndLine' => ['int|false'], -'ReflectionFunctionAbstract::getExtension' => ['ReflectionExtension'], -'ReflectionFunctionAbstract::getExtensionName' => ['string'], +'ReflectionFunctionAbstract::getExtension' => ['ReflectionExtension|null'], +'ReflectionFunctionAbstract::getExtensionName' => ['string|false'], 'ReflectionFunctionAbstract::getFileName' => ['string|false'], 'ReflectionFunctionAbstract::getName' => ['non-empty-string'], 'ReflectionFunctionAbstract::getNamespaceName' => ['string'], 'ReflectionFunctionAbstract::getNumberOfParameters' => ['int'], 'ReflectionFunctionAbstract::getNumberOfRequiredParameters' => ['int'], -'ReflectionFunctionAbstract::getParameters' => ['array'], +'ReflectionFunctionAbstract::getParameters' => ['list'], 'ReflectionFunctionAbstract::getReturnType' => ['?ReflectionType'], 'ReflectionFunctionAbstract::getShortName' => ['string'], 'ReflectionFunctionAbstract::getStartLine' => ['int|false'], @@ -9897,7 +10182,7 @@ 'rewind' => ['bool', 'fp'=>'resource'], 'rewinddir' => ['null|false', 'dir_handle='=>'resource'], 'rmdir' => ['bool', 'dirname'=>'string', 'context='=>'resource'], -'round' => ['float|false', 'number'=>'float', 'precision='=>'int', 'mode='=>'1|2|3|4'], +'round' => ['__benevolent', 'number'=>'float', 'precision='=>'int', 'mode='=>'1|2|3|4'], 'rpm_close' => ['bool', 'rpmr'=>'resource'], 'rpm_get_tag' => ['mixed', 'rpmr'=>'resource', 'tagnum'=>'int'], 'rpm_is_valid' => ['bool', 'filename'=>'string'], @@ -9998,7 +10283,7 @@ 'scalebarObj::set' => ['int', 'property_name'=>'string', 'new_value'=>''], 'scalebarObj::setImageColor' => ['int', 'red'=>'int', 'green'=>'int', 'blue'=>'int'], 'scalebarObj::updateFromString' => ['int', 'snippet'=>'string'], -'scandir' => ['array|false', 'dir'=>'string', 'sorting_order='=>'int', 'context='=>'resource'], +'scandir' => ['list|false', 'dir'=>'string', 'sorting_order='=>'int', 'context='=>'resource'], 'SDO_DAS_ChangeSummary::beginLogging' => [''], 'SDO_DAS_ChangeSummary::endLogging' => [''], 'SDO_DAS_ChangeSummary::getChangedDataObjects' => ['SDO_List'], @@ -10098,11 +10383,11 @@ 'session_destroy' => ['bool'], 'session_encode' => ['string|false'], 'session_gc' => ['int|false'], -'session_get_cookie_params' => ['array'], +'session_get_cookie_params' => ['array{lifetime:0|positive-int,path:non-falsy-string,domain:string,secure:bool,httponly:bool,samesite:string}'], 'session_id' => ['string|false', 'newid='=>'string'], 'session_is_registered' => ['bool', 'name'=>'string'], 'session_module_name' => ['string|false', 'newname='=>'string'], -'session_name' => ['string|false', 'newname='=>'string'], +'session_name' => ['non-falsy-string|false', 'newname='=>'string'], 'session_pgsql_add_error' => ['bool', 'error_level'=>'int', 'error_message='=>'string'], 'session_pgsql_get_error' => ['array', 'with_error_message='=>'bool'], 'session_pgsql_get_field' => ['string'], @@ -10119,7 +10404,7 @@ 'session_set_save_handler' => ['bool', 'open'=>'callable(string,string):bool', 'close'=>'callable():bool', 'read'=>'callable(string):string', 'write'=>'callable(string,string):bool', 'destroy'=>'callable(string):bool', 'gc'=>'callable(string):bool', 'create_sid='=>'callable():string', 'validate_sid='=>'callable(string):bool', 'update_timestamp='=>'callable(string):bool'], 'session_set_save_handler\'1' => ['bool', 'sessionhandler'=>'SessionHandlerInterface', 'register_shutdown='=>'bool'], 'session_start' => ['bool', 'options='=>'array'], -'session_status' => ['int'], +'session_status' => ['PHP_SESSION_NONE|PHP_SESSION_DISABLED|PHP_SESSION_ACTIVE'], 'session_unregister' => ['bool', 'name'=>'string'], 'session_unset' => ['bool'], 'session_write_close' => ['bool'], @@ -10149,20 +10434,20 @@ 'set_include_path' => ['string|false', 'new_include_path'=>'string'], 'set_magic_quotes_runtime' => ['bool', 'new_setting'=>'bool'], 'set_time_limit' => ['bool', 'seconds'=>'int'], -'setcookie' => ['bool', 'name'=>'string', 'value='=>'string', 'expires='=>'int', 'path='=>'string', 'domain='=>'string', 'secure='=>'bool', 'httponly='=>'bool', 'samesite='=>'\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'', 'url_encode='=>'int'], -'setcookie\'1' => ['bool', 'name'=>'string', 'value='=>'string', 'options='=>'array{ expires?:int, path?:string, domain?:string, secure?:bool, httponly?:bool, samesite?:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\', url_encode?:int}'], +'setcookie' => ['bool', 'name'=>'string', 'value='=>'string', 'expires='=>'int', 'path='=>'string', 'domain='=>'string', 'secure='=>'bool', 'httponly='=>'bool'], +'setcookie\'1' => ['bool', 'name'=>'string', 'value='=>'string', 'options='=>'array{ expires?:int, path?:string, domain?:string, secure?:bool, httponly?:bool, samesite?:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'}'], 'setLeftFill' => ['void', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'a='=>'int'], 'setLine' => ['void', 'width'=>'int', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'a='=>'int'], 'setlocale' => ['string|false', 'category'=>'int', 'locale'=>'string|null', '...args='=>'string'], 'setlocale\'1' => ['string|false', 'category'=>'int', 'locale'=>'?array'], 'setproctitle' => ['void', 'title'=>'string'], -'setrawcookie' => ['bool', 'name'=>'string', 'value='=>'string', 'expires='=>'int', 'path='=>'string', 'domain='=>'string', 'secure='=>'bool', 'httponly='=>'bool', 'samesite='=>'\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'', 'url_encode='=>'int'], -'setrawcookie\'1' => ['bool', 'name'=>'string', 'value='=>'string', 'options='=>'array{ expires?:int, path?:string, domain?:string, secure?:bool, httponly?:bool, samesite?:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\', url_encode?:int}'], +'setrawcookie' => ['bool', 'name'=>'string', 'value='=>'string', 'expires='=>'int', 'path='=>'string', 'domain='=>'string', 'secure='=>'bool', 'httponly='=>'bool'], +'setrawcookie\'1' => ['bool', 'name'=>'string', 'value='=>'string', 'options='=>'array{ expires?:int, path?:string, domain?:string, secure?:bool, httponly?:bool, samesite?:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'}'], 'setRightFill' => ['void', 'red'=>'int', 'green'=>'int', 'blue'=>'int', 'a='=>'int'], 'setthreadtitle' => ['bool', 'title'=>'string'], 'settype' => ['bool', '&rw_var'=>'mixed', 'type'=>'string'], -'sha1' => ['non-empty-string', 'str'=>'string', 'raw_output='=>'bool'], -'sha1_file' => ['non-empty-string|false', 'filename'=>'string', 'raw_output='=>'bool'], +'sha1' => ['non-falsy-string', 'str'=>'string', 'raw_output='=>'bool'], +'sha1_file' => ['non-falsy-string|false', 'filename'=>'string', 'raw_output='=>'bool'], 'sha256' => ['string', 'str'=>'string', 'raw_output='=>'bool'], 'sha256_file' => ['string', 'filename'=>'string', 'raw_output='=>'bool'], 'shapefileObj::__construct' => ['void', 'filename'=>'string', 'type'=>'int'], @@ -10232,9 +10517,9 @@ 'SimpleXMLElement::__get' => ['static', 'name'=>'string'], 'SimpleXMLElement::__toString' => ['string'], 'SimpleXMLElement::addAttribute' => ['void', 'name'=>'string', 'value='=>'string', 'ns='=>'string'], -'SimpleXMLElement::addChild' => ['static', 'name'=>'string', 'value='=>'string|null', 'ns='=>'string|null'], +'SimpleXMLElement::addChild' => ['__benevolent', 'name'=>'string', 'value='=>'string|null', 'ns='=>'string|null'], 'SimpleXMLElement::asXML' => ['string|bool', 'filename='=>'string'], -'SimpleXMLElement::attributes' => ['static|null', 'ns='=>'string', 'is_prefix='=>'bool'], +'SimpleXMLElement::attributes' => ['__benevolent', 'ns='=>'string', 'is_prefix='=>'bool'], 'SimpleXMLElement::children' => ['__benevolent', 'namespaceOrPrefix='=>'string|null', 'is_prefix='=>'bool'], 'SimpleXMLElement::count' => ['0|positive-int'], 'SimpleXMLElement::getDocNamespaces' => ['string[]|false', 'recursive='=>'bool', 'from_root='=>'bool'], @@ -10345,7 +10630,7 @@ 'socket_recv' => ['int|false', 'socket'=>'resource', '&w_buf'=>'string', 'len'=>'int', 'flags'=>'int'], 'socket_recvfrom' => ['int|false', 'socket'=>'resource', '&w_buf'=>'string', 'len'=>'int', 'flags'=>'int', '&w_name'=>'string', '&w_port='=>'int'], 'socket_recvmsg' => ['int|false', 'socket'=>'resource', '&w_message'=>'string', 'flags='=>'int'], -'socket_select' => ['int|false', '&rw_read_fds'=>'resource[]|null', '&rw_write_fds'=>'resource[]|null', '&rw_except_fds'=>'resource[]|null', 'tv_sec'=>'int|null', 'tv_usec='=>'int|null'], +'socket_select' => ['int|false', '&w_read_fds'=>'resource[]|null', '&w_write_fds'=>'resource[]|null', '&w_except_fds'=>'resource[]|null', 'tv_sec'=>'int|null', 'tv_usec='=>'int|null'], 'socket_send' => ['int|false', 'socket'=>'resource', 'buf'=>'string', 'len'=>'int', 'flags'=>'int'], 'socket_sendmsg' => ['int|false', 'socket'=>'resource', 'message'=>'array', 'flags'=>'int'], 'socket_sendto' => ['int|false', 'socket'=>'resource', 'buf'=>'string', 'len'=>'int', 'flags'=>'int', 'addr'=>'string', 'port='=>'int'], @@ -10449,11 +10734,11 @@ 'sodium_crypto_box_seal_open' => ['string|false', 'message'=>'string', 'recipient_keypair'=>'string'], 'sodium_crypto_box_secretkey' => ['string', 'keypair'=>'string'], 'sodium_crypto_box_seed_keypair' => ['string', 'seed'=>'string'], -'sodium_crypto_generichash' => ['string', 'msg'=>'string', 'key='=>'?string', 'length='=>'?int'], -'sodium_crypto_generichash_final' => ['string', 'state'=>'string', 'length='=>'?int'], -'sodium_crypto_generichash_init' => ['string', 'key='=>'?string', 'length='=>'?int'], -'sodium_crypto_generichash_keygen' => ['string'], -'sodium_crypto_generichash_update' => ['bool', 'state'=>'string', 'string'=>'string'], +'sodium_crypto_generichash' => ['non-empty-string', 'msg'=>'string', 'key='=>'?string', 'length='=>'?int'], +'sodium_crypto_generichash_final' => ['non-empty-string', 'state'=>'non-empty-string', 'length='=>'?int'], +'sodium_crypto_generichash_init' => ['non-empty-string', 'key='=>'?string', 'length='=>'?int'], +'sodium_crypto_generichash_keygen' => ['non-empty-string'], +'sodium_crypto_generichash_update' => ['bool', 'state'=>'non-empty-string', 'string'=>'string'], 'sodium_crypto_kdf_derive_from_key' => ['string', 'subkey_len'=>'int', 'subkey_id'=>'int', 'context'=>'string', 'key'=>'string'], 'sodium_crypto_kdf_keygen' => ['string'], 'sodium_crypto_kx' => ['string', 'secretkey'=>'string', 'publickey'=>'string', 'client_publickey'=>'string', 'server_publickey'=>'string'], @@ -10483,18 +10768,18 @@ 'sodium_crypto_secretstream_xchacha20poly1305_rekey' => ['void', 'state'=>'string'], 'sodium_crypto_shorthash' => ['string', 'message'=>'string', 'key'=>'string'], 'sodium_crypto_shorthash_keygen' => ['string'], -'sodium_crypto_sign' => ['string', 'message'=>'string', 'secretkey'=>'string'], -'sodium_crypto_sign_detached' => ['string', 'message'=>'string', 'secretkey'=>'string'], -'sodium_crypto_sign_ed25519_pk_to_curve25519' => ['string', 'ed25519pk'=>'string'], -'sodium_crypto_sign_ed25519_sk_to_curve25519' => ['string', 'ed25519sk'=>'string'], -'sodium_crypto_sign_keypair' => ['string'], -'sodium_crypto_sign_keypair_from_secretkey_and_publickey' => ['string', 'secret_key'=>'string', 'public_key'=>'string'], -'sodium_crypto_sign_open' => ['string|false', 'message'=>'string', 'publickey'=>'string'], -'sodium_crypto_sign_publickey' => ['string', 'keypair'=>'string'], -'sodium_crypto_sign_publickey_from_secretkey' => ['string', 'secretkey'=>'string'], -'sodium_crypto_sign_secretkey' => ['string', 'keypair'=>'string'], -'sodium_crypto_sign_seed_keypair' => ['string', 'seed'=>'string'], -'sodium_crypto_sign_verify_detached' => ['bool', 'signature'=>'string', 'message'=>'string', 'publickey'=>'string'], +'sodium_crypto_sign' => ['non-empty-string', 'message'=>'string', 'secretkey'=>'non-empty-string'], +'sodium_crypto_sign_detached' => ['non-empty-string', 'message'=>'string', 'secretkey'=>'non-empty-string'], +'sodium_crypto_sign_ed25519_pk_to_curve25519' => ['non-empty-string', 'ed25519pk'=>'non-empty-string'], +'sodium_crypto_sign_ed25519_sk_to_curve25519' => ['non-empty-string', 'ed25519sk'=>'non-empty-string'], +'sodium_crypto_sign_keypair' => ['non-empty-string'], +'sodium_crypto_sign_keypair_from_secretkey_and_publickey' => ['non-empty-string', 'secret_key'=>'non-empty-string', 'public_key'=>'non-empty-string'], +'sodium_crypto_sign_open' => ['string|false', 'message'=>'string', 'publickey'=>'non-empty-string'], +'sodium_crypto_sign_publickey' => ['non-empty-string', 'keypair'=>'non-empty-string'], +'sodium_crypto_sign_publickey_from_secretkey' => ['non-empty-string', 'secretkey'=>'non-empty-string'], +'sodium_crypto_sign_secretkey' => ['non-empty-string', 'keypair'=>'non-empty-string'], +'sodium_crypto_sign_seed_keypair' => ['non-empty-string', 'seed'=>'non-empty-string'], +'sodium_crypto_sign_verify_detached' => ['bool', 'signature'=>'non-empty-string', 'message'=>'string', 'publickey'=>'non-empty-string'], 'sodium_crypto_stream' => ['string', 'length'=>'int', 'nonce'=>'string', 'key'=>'string'], 'sodium_crypto_stream_keygen' => ['string'], 'sodium_crypto_stream_xor' => ['string', 'message'=>'string', 'nonce'=>'string', 'key'=>'string'], @@ -11226,7 +11511,7 @@ 'spl_autoload' => ['void', 'class_name'=>'string', 'file_extensions='=>'string'], 'spl_autoload_call' => ['void', 'class_name'=>'string'], 'spl_autoload_extensions' => ['string', 'file_extensions='=>'string'], -'spl_autoload_functions' => ['false|array'], +'spl_autoload_functions' => ['false|list'], 'spl_autoload_register' => ['bool', 'autoload_function='=>'callable(string):void', 'throw='=>'bool', 'prepend='=>'bool'], 'spl_autoload_unregister' => ['bool', 'autoload_function'=>'mixed'], 'spl_classes' => ['array'], @@ -11293,7 +11578,7 @@ 'SplFileObject::fflush' => ['bool'], 'SplFileObject::fgetc' => ['string|false'], // Do not believe https://www.php.net/manual/en/splfileobject.fgetcsv#refsect1-splfileobject.fgetcsv-returnvalues -'SplFileObject::fgetcsv' => ['array|false|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], +'SplFileObject::fgetcsv' => ['list|array{0: null}|false|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'SplFileObject::fgets' => ['string|false'], 'SplFileObject::fgetss' => ['string|false', 'allowable_tags='=>'string'], 'SplFileObject::flock' => ['bool', 'operation'=>'int', '&w_wouldblock='=>'int'], @@ -11404,7 +11689,7 @@ 'Spoofchecker::setAllowedLocales' => ['void', 'locale_list'=>'string'], 'Spoofchecker::setChecks' => ['void', 'checks'=>'long'], 'Spoofchecker::setRestrictionLevel' => ['void', 'restriction_level'=>'int'], -'sprintf' => ['string', 'format'=>'string', '...values='=>'string|int|float|bool'], +'sprintf' => ['string', 'format'=>'string', '...values='=>'__stringAndStringable|int|float|null|bool'], 'sql_regcase' => ['string', 'string'=>'string'], 'SQLite3::__construct' => ['void', 'filename'=>'string', 'flags='=>'int', 'encryption_key='=>'string|null'], 'SQLite3::busyTimeout' => ['bool', 'msecs'=>'int'], @@ -11421,7 +11706,7 @@ 'SQLite3::lastInsertRowID' => ['int'], 'SQLite3::loadExtension' => ['bool', 'shared_library'=>'string'], 'SQLite3::open' => ['void', 'filename'=>'string', 'flags='=>'int', 'encryption_key='=>'string|null'], -'SQLite3::openBlob' => ['resource', 'table'=>'string', 'column'=>'string', 'rowid'=>'int', 'dbname'=>'string', 'flags='=>'int'], +'SQLite3::openBlob' => ['resource|false', 'table'=>'string', 'column'=>'string', 'rowid'=>'int', 'dbname='=>'string', 'flags='=>'int'], 'SQLite3::prepare' => ['SQLite3Stmt|false', 'query'=>'string'], 'SQLite3::query' => ['SQLite3Result|false', 'query'=>'string'], 'SQLite3::querySingle' => ['array|int|string|bool|float|null|false', 'query'=>'string', 'entire_row='=>'bool'], @@ -11692,14 +11977,14 @@ 'stomp_version' => ['string'], 'StompException::getDetails' => ['string'], 'StompFrame::__construct' => ['void', 'command='=>'string', 'headers='=>'array', 'body='=>'string'], -'str_getcsv' => ['array', 'input'=>'string', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], +'str_getcsv' => ['non-empty-list', 'input'=>'string', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'str_ireplace' => ['string|string[]', 'search'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', '&w_replace_count='=>'int'], 'str_pad' => ['string', 'input'=>'string', 'pad_length'=>'int', 'pad_string='=>'string', 'pad_type='=>'int'], 'str_repeat' => ['string', 'input'=>'string', 'multiplier'=>'int'], 'str_replace' => ['string|array', 'search'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', '&w_replace_count='=>'int'], 'str_rot13' => ['string', 'str'=>'string'], 'str_shuffle' => ['string', 'str'=>'string'], -'str_split' => ['non-empty-array|false', 'str'=>'string', 'split_length='=>'positive-int'], +'str_split' => ['non-empty-list|false', 'str'=>'string', 'split_length='=>'positive-int'], 'str_word_count' => ['array|int|false', 'string'=>'string', 'format='=>'int', 'charlist='=>'string'], 'strcasecmp' => ['int<-1, 1>', 'str1'=>'string', 'str2'=>'string'], 'strchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], @@ -11727,7 +12012,7 @@ 'stream_get_contents' => ['string|false', 'source'=>'resource', 'maxlen='=>'int', 'offset='=>'int'], 'stream_get_filters' => ['list'], 'stream_get_line' => ['string|false', 'stream'=>'resource', 'maxlen'=>'int', 'ending='=>'string'], -'stream_get_meta_data' => ['array{timed_out:bool,blocked:bool,eof:bool,unread_bytes:int,stream_type:string,wrapper_type:string,wrapper_data:mixed,mode:string,seekable:bool,uri:string,mediatype?:string,base64?:bool}', 'fp'=>'resource'], +'stream_get_meta_data' => ['array{timed_out:bool,blocked:bool,eof:bool,unread_bytes:int,stream_type:string,wrapper_type:string,wrapper_data:mixed,mode:string,seekable:bool,uri?:string,mediatype?:string,base64?:bool}', 'fp'=>'resource'], 'stream_get_transports' => ['list'], 'stream_get_wrappers' => ['list'], 'stream_is_local' => ['bool', 'stream'=>'resource|string'], @@ -11742,7 +12027,7 @@ 'stream_set_write_buffer' => ['int', 'fp'=>'resource', 'buffer'=>'int'], 'stream_socket_accept' => ['resource|false', 'serverstream'=>'resource', 'timeout='=>'float', '&w_peername='=>'string'], 'stream_socket_client' => ['resource|false', 'remoteaddress'=>'string', '&w_errcode='=>'int', '&w_errstring='=>'string', 'timeout='=>'float', 'flags='=>'int', 'context='=>'resource'], -'stream_socket_enable_crypto' => ['int|bool', 'stream'=>'resource', 'enable'=>'bool', 'cryptokind='=>'int', 'sessionstream='=>'resource'], +'stream_socket_enable_crypto' => ['0|bool', 'stream'=>'resource', 'enable'=>'bool', 'cryptokind='=>'int', 'sessionstream='=>'resource'], 'stream_socket_get_name' => ['string|false', 'stream'=>'resource', 'want_peer'=>'bool'], 'stream_socket_pair' => ['resource[]|false', 'domain'=>'int', 'type'=>'int', 'protocol'=>'int'], 'stream_socket_recvfrom' => ['string|false', 'stream'=>'resource', 'amount'=>'int', 'flags='=>'int', '&w_remote_addr='=>'string'], @@ -11798,14 +12083,14 @@ 'strrpos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'strspn' => ['int', 'str'=>'string', 'mask'=>'string', 'start='=>'int', 'len='=>'int'], 'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'mixed', 'before_needle='=>'bool'], -'strtok' => ['string|false', 'str'=>'string', 'token'=>'string'], -'strtok\'1' => ['string|false', 'token'=>'string'], +'strtok' => ['non-empty-string|false', 'str'=>'string', 'token'=>'string'], +'strtok\'1' => ['non-empty-string|false', 'token'=>'string'], 'strtolower' => ['string', 'str'=>'string'], 'strtotime' => ['int|false', 'time'=>'string', 'now='=>'int'], 'strtoupper' => ['string', 'str'=>'string'], 'strtr' => ['string', 'str'=>'string', 'from'=>'string', 'to'=>'string'], 'strtr\'1' => ['string', 'str'=>'string', 'replace_pairs'=>'array'], -'strval' => ['string', 'var'=>'mixed'], +'strval' => ['string', 'var'=>'__stringAndStringable|int|float|bool|resource|null'], 'substr' => ['__benevolent', 'string'=>'string', 'start'=>'int', 'length='=>'int'], 'substr_compare' => ['int<-1, 1>|false', 'main_str'=>'string', 'str'=>'string', 'offset'=>'int', 'length='=>'int', 'case_sensitivity='=>'bool'], 'substr_count' => ['0|positive-int', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'length='=>'int'], @@ -12310,17 +12595,17 @@ 'time' => ['positive-int'], 'time_nanosleep' => ['array{seconds:0|positive-int,nanoseconds:0|positive-int}|bool', 'seconds'=>'int', 'nanoseconds'=>'int'], 'time_sleep_until' => ['bool', 'timestamp'=>'float'], -'timezone_abbreviations_list' => ['array'], -'timezone_identifiers_list' => ['array', 'what='=>'int', 'country='=>'?string'], +'timezone_abbreviations_list' => ['array>'], +'timezone_identifiers_list' => ['list', 'what='=>'int', 'country='=>'?string'], 'timezone_location_get' => ['array{country_code: string, latitude: float, longitude: float, comments: string}|false', 'object'=>'DateTimeZone'], 'timezone_name_from_abbr' => ['string|false', 'abbr'=>'string', 'gmtoffset='=>'int', 'isdst='=>'int'], 'timezone_name_get' => ['string', 'object'=>'DateTimeZone'], 'timezone_offset_get' => ['int', 'object'=>'DateTimeZone', 'datetime'=>'DateTime'], 'timezone_open' => ['DateTimeZone|false', 'timezone'=>'string'], -'timezone_transitions_get' => ['array|false', 'object'=>'DateTimeZone', 'timestamp_begin='=>'int', 'timestamp_end='=>'int'], +'timezone_transitions_get' => ['list|false', 'object'=>'DateTimeZone', 'timestamp_begin='=>'int', 'timestamp_end='=>'int'], 'timezone_version_get' => ['string'], 'tmpfile' => ['resource|false'], -'token_get_all' => ['array', 'source'=>'string', 'flags='=>'int'], +'token_get_all' => ['list', 'source'=>'string', 'flags='=>'int'], 'token_name' => ['string', 'type'=>'int'], 'TokyoTyrant::__construct' => ['void', 'host='=>'string', 'port='=>'int', 'options='=>'array'], 'TokyoTyrant::add' => ['int|float', 'key'=>'string', 'increment'=>'float', 'type='=>'int'], @@ -12544,17 +12829,17 @@ 'trait_exists' => ['bool', 'traitname'=>'string', 'autoload='=>'bool'], 'Transliterator::create' => ['?Transliterator', 'id'=>'string', 'direction='=>'int'], 'Transliterator::createFromRules' => ['?Transliterator', 'rules'=>'string', 'direction='=>'int'], -'Transliterator::createInverse' => ['Transliterator'], -'Transliterator::getErrorCode' => ['int'], -'Transliterator::getErrorMessage' => ['string'], -'Transliterator::listIDs' => ['array'], +'Transliterator::createInverse' => ['?Transliterator'], +'Transliterator::getErrorCode' => ['int|false'], +'Transliterator::getErrorMessage' => ['string|false'], +'Transliterator::listIDs' => ['list|false'], 'Transliterator::transliterate' => ['string|false', 'subject'=>'string', 'start='=>'int', 'end='=>'int'], 'transliterator_create' => ['?Transliterator', 'id'=>'string', 'direction='=>'int'], 'transliterator_create_from_rules' => ['?Transliterator', 'rules'=>'string', 'direction='=>'int'], -'transliterator_create_inverse' => ['Transliterator', 'obj'=>'Transliterator'], +'transliterator_create_inverse' => ['?Transliterator', 'obj'=>'Transliterator'], 'transliterator_get_error_code' => ['int|false', 'obj'=>'Transliterator'], 'transliterator_get_error_message' => ['string|false', 'obj'=>'Transliterator'], -'transliterator_list_ids' => ['array|false'], +'transliterator_list_ids' => ['list|false'], 'transliterator_transliterate' => ['string|false', 'obj'=>'Transliterator|string', 'subject'=>'string', 'start='=>'int', 'end='=>'int'], 'trigger_error' => ['bool', 'message'=>'string', 'error_type='=>'int'], 'trim' => ['string', 'str'=>'string', 'character_mask='=>'string'], @@ -12659,8 +12944,8 @@ 'uopz_delete' => ['void', 'class'=>'string', 'function'=>'string'], 'uopz_delete\'1' => ['void', 'function'=>'string'], 'uopz_extend' => ['void', 'class'=>'string', 'parent'=>'string'], -'uopz_flags' => ['int', 'class'=>'string', 'function'=>'string', 'flags'=>'int'], -'uopz_flags\'1' => ['int', 'function'=>'string', 'flags'=>'int'], +'uopz_flags' => ['int', 'class'=>'string', 'function'=>'string', 'flags='=>'int'], +'uopz_flags\'1' => ['int', 'function'=>'string', 'flags='=>'int'], 'uopz_function' => ['void', 'class'=>'string', 'function'=>'string', 'handler'=>'Closure', 'modifiers='=>'int'], 'uopz_function\'1' => ['void', 'function'=>'string', 'handler'=>'Closure', 'modifiers='=>'int'], 'uopz_get_exit_status' => ['mixed'], @@ -12789,7 +13074,7 @@ 'VarnishStat::getSnapshot' => ['array'], 'version_compare' => ['int', 'version1'=>'string', 'version2'=>'string'], 'version_compare\'1' => ['bool', 'version1'=>'string', 'version2'=>'string', 'operator'=>'string'], -'vfprintf' => ['int', 'stream'=>'resource', 'format'=>'string', 'args'=>'array'], +'vfprintf' => ['int', 'stream'=>'resource', 'format'=>'string', 'args'=>'array<__stringAndStringable|int|float|null|bool>'], 'virtual' => ['bool', 'uri'=>'string'], 'Volatile::__construct' => ['void'], 'Volatile::chunk' => ['array', 'size'=>'int', 'preserve'=>'bool'], @@ -12826,8 +13111,8 @@ 'vpopmail_error' => ['string'], 'vpopmail_passwd' => ['bool', 'user'=>'string', 'domain'=>'string', 'password'=>'string', 'apop='=>'bool'], 'vpopmail_set_user_quota' => ['bool', 'user'=>'string', 'domain'=>'string', 'quota'=>'string'], -'vprintf' => ['int', 'format'=>'string', 'args'=>'array'], -'vsprintf' => ['string', 'format'=>'string', 'args'=>'array'], +'vprintf' => ['int', 'format'=>'string', 'args'=>'array<__stringAndStringable|int|float|null|bool>'], +'vsprintf' => ['string', 'format'=>'string', 'args'=>'array<__stringAndStringable|int|float|null|bool>'], 'w32api_deftype' => ['bool', 'typename'=>'string', 'member1_type'=>'string', 'member1_name'=>'string', '...args='=>'string'], 'w32api_init_dtype' => ['resource', 'typename'=>'string', 'value'=>'', '...args='=>''], 'w32api_invoke_function' => ['', 'funcname'=>'string', 'argument'=>'', '...args='=>''], @@ -12969,10 +13254,10 @@ 'Xcom::send' => ['int', 'topic'=>'string', 'data'=>'mixed', 'json_schema='=>'string', 'http_headers='=>'array'], 'Xcom::sendAsync' => ['int', 'topic'=>'string', 'data'=>'mixed', 'json_schema='=>'string', 'http_headers='=>'array'], 'xdebug_break' => ['bool'], -'xdebug_call_class' => ['string', 'depth=' => 'int'], -'xdebug_call_file' => ['string', 'depth=' => 'int'], -'xdebug_call_function' => ['string', 'depth=' => 'int'], -'xdebug_call_line' => ['int', 'depth=' => 'int'], +'xdebug_call_class' => ['string', 'depth='=>'int'], +'xdebug_call_file' => ['string', 'depth='=>'int'], +'xdebug_call_function' => ['string', 'depth='=>'int'], +'xdebug_call_line' => ['int', 'depth='=>'int'], 'xdebug_clear_aggr_profiling_data' => ['bool'], 'xdebug_code_coverage_started' => ['bool'], 'xdebug_connect_to_client' => ['bool'], @@ -12996,10 +13281,10 @@ 'xdebug_is_debugger_active' => ['bool'], 'xdebug_is_enabled' => ['bool'], 'xdebug_memory_usage' => ['int'], -'xdebug_notify' => ['bool', 'data' => 'mixed'], +'xdebug_notify' => ['bool', 'data'=>'mixed'], 'xdebug_peak_memory_usage' => ['int'], -'xdebug_print_function_stack' => ['array', 'message='=>'string', 'options=' => 'int'], -'xdebug_set_filter' => ['void', 'group' => 'int', 'list_type' => 'int', 'configuration' => 'array'], +'xdebug_print_function_stack' => ['array', 'message='=>'string', 'options='=>'int'], +'xdebug_set_filter' => ['void', 'group'=>'int', 'list_type'=>'int', 'configuration'=>'array'], 'xdebug_start_code_coverage' => ['void', 'options='=>'int'], 'xdebug_start_error_collection' => ['void'], 'xdebug_start_function_monitor' => ['void', 'list_of_functions_to_monitor'=>'string[]'], @@ -13087,7 +13372,7 @@ 'XMLReader::setRelaxNGSchema' => ['bool', 'filename'=>'string'], 'XMLReader::setRelaxNGSchemaSource' => ['bool', 'source'=>'string'], 'XMLReader::setSchema' => ['bool', 'filename'=>'string'], -'XMLReader::XML' => ['bool', 'source'=>'string', 'encoding='=>'?string', 'options='=>'int'], +'XMLReader::XML' => ['bool|XMLReader', 'source'=>'string', 'encoding='=>'?string', 'options='=>'int'], 'xmlrpc_decode' => ['?array', 'xml'=>'string', 'encoding='=>'string'], 'xmlrpc_decode_request' => ['?array', 'xml'=>'string', '&w_method'=>'string', 'encoding='=>'string'], 'xmlrpc_encode' => ['string', 'value'=>'mixed'], @@ -13254,7 +13539,7 @@ 'Yaf_Application::__clone' => ['void'], 'Yaf_Application::__construct' => ['void', 'config'=>'mixed', 'envrion='=>'string'], 'Yaf_Application::__destruct' => ['void'], -'Yaf_Application::__sleep' => ['void'], +'Yaf_Application::__sleep' => ['list'], 'Yaf_Application::__wakeup' => ['void'], 'Yaf_Application::app' => ['void'], 'Yaf_Application::bootstrap' => ['void', 'bootstrap='=>'Yaf_Bootstrap_Abstract'], @@ -13269,93 +13554,113 @@ 'Yaf_Application::getModules' => ['array'], 'Yaf_Application::run' => ['void'], 'Yaf_Application::setAppDirectory' => ['Yaf_Application', 'directory'=>'string'], -'Yaf_Config_Abstract::get' => ['mixed', 'name'=>'string', 'value'=>'mixed'], +'Yaf_Config_Abstract::__get' => ['mixed', 'name'=>'string'], +'Yaf_Config_Abstract::__isset' => ['bool', 'name'=>'string'], +'Yaf_Config_Abstract::count' => ['0|positive-int'], +'Yaf_Config_Abstract::current' => ['mixed'], +'Yaf_Config_Abstract::get' => ['mixed', 'name'=>'?string'], +'Yaf_Config_Abstract::key' => ['int|string|null|bool'], +'Yaf_Config_Abstract::next' => ['void'], +'Yaf_Config_Abstract::offsetExists' => ['bool', 'name'=>'mixed'], +'Yaf_Config_Abstract::offsetGet' => ['mixed', 'name'=>'mixed'], +'Yaf_Config_Abstract::offsetSet' => ['void', 'name'=>'mixed', 'value'=>'mixed'], +'Yaf_Config_Abstract::offsetUnset' => ['void', 'name'=>'mixed'], 'Yaf_Config_Abstract::readonly' => ['bool'], -'Yaf_Config_Abstract::set' => ['Yaf_Config_Abstract'], +'Yaf_Config_Abstract::rewind' => ['void'], +'Yaf_Config_Abstract::set' => ['bool', 'name'=>'string', 'value'=>'mixed'], 'Yaf_Config_Abstract::toArray' => ['array'], -'Yaf_Config_Ini::__construct' => ['void', 'config_file'=>'string', 'section='=>'string'], -'Yaf_Config_Ini::__get' => ['void', 'name='=>'string'], -'Yaf_Config_Ini::__isset' => ['void', 'name'=>'string'], -'Yaf_Config_Ini::__set' => ['void', 'name'=>'string', 'value'=>'mixed'], +'Yaf_Config_Abstract::valid' => ['bool'], +'Yaf_Config_Ini::__construct' => ['void', 'config_file'=>'array|string', 'section='=>'?string'], +'Yaf_Config_Ini::__isset' => ['bool', 'name'=>'string'], +'Yaf_Config_Ini::__set' => ['void', 'name'=>'mixed', 'value'=>'mixed'], 'Yaf_Config_Ini::count' => ['0|positive-int'], -'Yaf_Config_Ini::current' => ['void'], -'Yaf_Config_Ini::get' => ['mixed', 'name='=>'mixed'], -'Yaf_Config_Ini::key' => ['void'], +'Yaf_Config_Ini::current' => ['mixed'], +'Yaf_Config_Ini::get' => ['mixed', 'name='=>'?string'], +'Yaf_Config_Ini::key' => ['int|string|null|bool'], 'Yaf_Config_Ini::next' => ['void'], -'Yaf_Config_Ini::offsetExists' => ['void', 'name'=>'string'], -'Yaf_Config_Ini::offsetGet' => ['void', 'name'=>'string'], -'Yaf_Config_Ini::offsetSet' => ['void', 'name'=>'string', 'value'=>'string'], -'Yaf_Config_Ini::offsetUnset' => ['void', 'name'=>'string'], -'Yaf_Config_Ini::readonly' => ['void'], +'Yaf_Config_Ini::offsetExists' => ['bool', 'name'=>'mixed'], +'Yaf_Config_Ini::offsetGet' => ['mixed', 'name'=>'mixed'], +'Yaf_Config_Ini::offsetSet' => ['void', 'name'=>'mixed', 'value'=>'mixed'], +'Yaf_Config_Ini::offsetUnset' => ['void', 'name'=>'mixed'], +'Yaf_Config_Ini::readonly' => ['bool'], 'Yaf_Config_Ini::rewind' => ['void'], -'Yaf_Config_Ini::set' => ['Yaf_Config_Abstract', 'name'=>'string', 'value'=>'mixed'], +'Yaf_Config_Ini::set' => ['bool', 'name'=>'string', 'value'=>'mixed'], 'Yaf_Config_Ini::toArray' => ['array'], -'Yaf_Config_Ini::valid' => ['void'], -'Yaf_Config_Simple::__construct' => ['void', 'config_file'=>'string', 'section='=>'string'], +'Yaf_Config_Ini::valid' => ['bool'], +'Yaf_Config_Simple::__construct' => ['void', 'config_file'=>'array|string', 'section='=>'string'], 'Yaf_Config_Simple::__get' => ['void', 'name='=>'string'], -'Yaf_Config_Simple::__isset' => ['void', 'name'=>'string'], -'Yaf_Config_Simple::__set' => ['void', 'name'=>'string', 'value'=>'string'], +'Yaf_Config_Simple::__isset' => ['bool', 'name'=>'string'], +'Yaf_Config_Simple::__set' => ['void', 'name'=>'string', 'value'=>'mixed'], 'Yaf_Config_Simple::count' => ['0|positive-int'], -'Yaf_Config_Simple::current' => ['void'], -'Yaf_Config_Simple::get' => ['mixed', 'name='=>'mixed'], -'Yaf_Config_Simple::key' => ['void'], +'Yaf_Config_Simple::current' => ['mixed'], +'Yaf_Config_Simple::get' => ['mixed', 'name='=>'?string'], +'Yaf_Config_Simple::key' => ['int|string|null|bool'], 'Yaf_Config_Simple::next' => ['void'], -'Yaf_Config_Simple::offsetExists' => ['void', 'name'=>'string'], -'Yaf_Config_Simple::offsetGet' => ['void', 'name'=>'string'], -'Yaf_Config_Simple::offsetSet' => ['void', 'name'=>'string', 'value'=>'string'], -'Yaf_Config_Simple::offsetUnset' => ['void', 'name'=>'string'], -'Yaf_Config_Simple::readonly' => ['void'], +'Yaf_Config_Simple::offsetExists' => ['bool', 'name'=>'mixed'], +'Yaf_Config_Simple::offsetGet' => ['mixed', 'name'=>'mixed'], +'Yaf_Config_Simple::offsetSet' => ['void', 'name'=>'mixed', 'value'=>'mixed'], +'Yaf_Config_Simple::offsetUnset' => ['void', 'name'=>'mixed'], +'Yaf_Config_Simple::readonly' => ['bool'], 'Yaf_Config_Simple::rewind' => ['void'], -'Yaf_Config_Simple::set' => ['Yaf_Config_Abstract', 'name'=>'string', 'value'=>'mixed'], +'Yaf_Config_Simple::set' => ['bool', 'name'=>'string', 'value'=>'mixed'], 'Yaf_Config_Simple::toArray' => ['array'], -'Yaf_Config_Simple::valid' => ['void'], +'Yaf_Config_Simple::valid' => ['bool'], 'Yaf_Controller_Abstract::__clone' => ['void'], 'Yaf_Controller_Abstract::__construct' => ['void'], -'Yaf_Controller_Abstract::display' => ['bool', 'tpl'=>'string', 'parameters='=>'array'], -'Yaf_Controller_Abstract::forward' => ['void', 'action'=>'string', 'parameters='=>'array'], -'Yaf_Controller_Abstract::forward\'1' => ['void', 'controller'=>'string', 'action'=>'string', 'parameters='=>'array'], -'Yaf_Controller_Abstract::forward\'2' => ['void', 'module'=>'string', 'controller'=>'string', 'action'=>'string', 'parameters='=>'array'], -'Yaf_Controller_Abstract::getInvokeArg' => ['void', 'name'=>'string'], -'Yaf_Controller_Abstract::getInvokeArgs' => ['void'], -'Yaf_Controller_Abstract::getModuleName' => ['string'], -'Yaf_Controller_Abstract::getRequest' => ['Yaf_Request_Abstract'], -'Yaf_Controller_Abstract::getResponse' => ['Yaf_Response_Abstract'], -'Yaf_Controller_Abstract::getView' => ['Yaf_View_Interface'], -'Yaf_Controller_Abstract::getViewpath' => ['void'], +'Yaf_Controller_Abstract::display' => ['?bool', 'tpl'=>'string', 'parameters='=>'?array'], +'Yaf_Controller_Abstract::forward' => ['?bool', 'action'=>'string'], +'Yaf_Controller_Abstract::forward\'1' => ['?bool', 'controller'=>'string', 'action'=>'string'], +'Yaf_Controller_Abstract::forward\'2' => ['?bool', 'action'=>'string', 'invoke_args'=>'array'], +'Yaf_Controller_Abstract::forward\'3' => ['?bool', 'module'=>'string', 'controller'=>'string', 'action'=>'string'], +'Yaf_Controller_Abstract::forward\'4' => ['?bool', 'controller'=>'string', 'action'=>'string', 'invoke_args'=>'array'], +'Yaf_Controller_Abstract::forward\'5' => ['?bool', 'module'=>'string', 'controller'=>'string', 'action'=>'string', 'invoke_args'=>'array'], +'Yaf_Controller_Abstract::getInvokeArg' => ['?string', 'name'=>'string'], +'Yaf_Controller_Abstract::getInvokeArgs' => ['?array'], +'Yaf_Controller_Abstract::getModuleName' => ['?string'], +'Yaf_Controller_Abstract::getName' => ['?string'], +'Yaf_Controller_Abstract::getRequest' => ['?Yaf_Request_Abstract'], +'Yaf_Controller_Abstract::getResponse' => ['?Yaf_Response_Abstract'], +'Yaf_Controller_Abstract::getView' => ['?Yaf_View_Interface'], +'Yaf_Controller_Abstract::getViewpath' => ['?string'], 'Yaf_Controller_Abstract::init' => ['void'], -'Yaf_Controller_Abstract::initView' => ['void', 'options='=>'array'], -'Yaf_Controller_Abstract::redirect' => ['bool', 'url'=>'string'], -'Yaf_Controller_Abstract::render' => ['string', 'tpl'=>'string', 'parameters='=>'array'], -'Yaf_Controller_Abstract::setViewpath' => ['void', 'view_directory'=>'string'], +'Yaf_Controller_Abstract::initView' => ['?Yaf_View_Interface', 'options='=>'?array'], +'Yaf_Controller_Abstract::redirect' => ['?bool', 'url'=>'string'], +'Yaf_Controller_Abstract::render' => ['string|null|bool', 'tpl'=>'string', 'parameters='=>'?array'], +'Yaf_Controller_Abstract::setViewpath' => ['?bool', 'view_directory'=>'string'], 'Yaf_Dispatcher::__clone' => ['void'], 'Yaf_Dispatcher::__construct' => ['void'], -'Yaf_Dispatcher::__sleep' => ['void'], +'Yaf_Dispatcher::__sleep' => ['list'], 'Yaf_Dispatcher::__wakeup' => ['void'], -'Yaf_Dispatcher::autoRender' => ['Yaf_Dispatcher', 'flag='=>'bool'], -'Yaf_Dispatcher::catchException' => ['Yaf_Dispatcher', 'flag='=>'bool'], -'Yaf_Dispatcher::disableView' => ['bool'], -'Yaf_Dispatcher::dispatch' => ['Yaf_Response_Abstract', 'request'=>'Yaf_Request_Abstract'], -'Yaf_Dispatcher::enableView' => ['Yaf_Dispatcher'], -'Yaf_Dispatcher::flushInstantly' => ['Yaf_Dispatcher', 'flag='=>'bool'], -'Yaf_Dispatcher::getApplication' => ['Yaf_Application'], -'Yaf_Dispatcher::getInstance' => ['Yaf_Dispatcher'], -'Yaf_Dispatcher::getRequest' => ['Yaf_Request_Abstract'], -'Yaf_Dispatcher::getRouter' => ['Yaf_Router'], -'Yaf_Dispatcher::initView' => ['Yaf_View_Interface', 'templates_dir'=>'string', 'options='=>'array'], -'Yaf_Dispatcher::registerPlugin' => ['Yaf_Dispatcher', 'plugin'=>'Yaf_Plugin_Abstract'], -'Yaf_Dispatcher::returnResponse' => ['Yaf_Dispatcher', 'flag'=>'bool'], -'Yaf_Dispatcher::setDefaultAction' => ['Yaf_Dispatcher', 'action'=>'string'], -'Yaf_Dispatcher::setDefaultController' => ['Yaf_Dispatcher', 'controller'=>'string'], -'Yaf_Dispatcher::setDefaultModule' => ['Yaf_Dispatcher', 'module'=>'string'], -'Yaf_Dispatcher::setErrorHandler' => ['Yaf_Dispatcher', 'callback'=>'call', 'error_types'=>'int'], -'Yaf_Dispatcher::setRequest' => ['Yaf_Dispatcher', 'request'=>'Yaf_Request_Abstract'], -'Yaf_Dispatcher::setView' => ['Yaf_Dispatcher', 'view'=>'Yaf_View_Interface'], -'Yaf_Dispatcher::throwException' => ['Yaf_Dispatcher', 'flag='=>'bool'], +'Yaf_Dispatcher::autoRender' => ['Yaf_Dispatcher|false|null', 'flag='=>'?bool'], +'Yaf_Dispatcher::catchException' => ['Yaf_Dispatcher|false|null', 'flag='=>'?bool'], +'Yaf_Dispatcher::disableView' => ['?Yaf_Dispatcher'], +'Yaf_Dispatcher::dispatch' => ['Yaf_Response_Abstract|false|null', 'request'=>'Yaf_Request_Abstract'], +'Yaf_Dispatcher::enableView' => ['?Yaf_Dispatcher'], +'Yaf_Dispatcher::flushInstantly' => ['Yaf_Dispatcher|false|null', 'flag='=>'?bool'], +'Yaf_Dispatcher::getApplication' => ['?Yaf_Application'], +'Yaf_Dispatcher::getDefaultAction' => ['?string'], +'Yaf_Dispatcher::getDefaultController' => ['?string'], +'Yaf_Dispatcher::getDefaultModule' => ['?string'], +'Yaf_Dispatcher::getInstance' => ['?Yaf_Dispatcher'], +'Yaf_Dispatcher::getRequest' => ['?Yaf_Request_Abstract'], +'Yaf_Dispatcher::getResponse' => ['?Yaf_Response_Abstract'], +'Yaf_Dispatcher::getRouter' => ['?Yaf_Router'], +'Yaf_Dispatcher::initView' => ['Yaf_View_Interface|null|false', 'templates_dir'=>'string', 'options='=>'?array'], +'Yaf_Dispatcher::registerPlugin' => ['Yaf_Dispatcher|false|null', 'plugin'=>'Yaf_Plugin_Abstract'], +'Yaf_Dispatcher::returnResponse' => ['Yaf_Dispatcher|false|null', 'flag='=>'bool'], +'Yaf_Dispatcher::setDefaultAction' => ['Yaf_Dispatcher|false|null', 'action'=>'string'], +'Yaf_Dispatcher::setDefaultController' => ['Yaf_Dispatcher|false|null', 'controller'=>'string'], +'Yaf_Dispatcher::setDefaultModule' => ['Yaf_Dispatcher|false|null', 'module'=>'string'], +'Yaf_Dispatcher::setErrorHandler' => ['Yaf_Dispatcher|false|null', 'callback'=>'mixed', 'error_types'=>'int'], +'Yaf_Dispatcher::setRequest' => ['?Yaf_Dispatcher', 'request'=>'Yaf_Request_Abstract'], +'Yaf_Dispatcher::setResponse' => ['?Yaf_Dispatcher', 'response'=>'Yaf_Response_Abstract'], +'Yaf_Dispatcher::setView' => ['?Yaf_Dispatcher', 'view'=>'Yaf_View_Interface'], +'Yaf_Dispatcher::throwException' => ['Yaf_Dispatcher|false|null', 'flag='=>'?bool'], 'Yaf_Exception::__construct' => ['void'], 'Yaf_Exception::getPrevious' => ['void'], 'Yaf_Loader::__clone' => ['void'], 'Yaf_Loader::__construct' => ['void'], -'Yaf_Loader::__sleep' => ['void'], +'Yaf_Loader::__sleep' => ['list'], 'Yaf_Loader::__wakeup' => ['void'], 'Yaf_Loader::autoload' => ['void'], 'Yaf_Loader::clearLocalNamespace' => ['void'], @@ -13379,149 +13684,163 @@ 'Yaf_Registry::get' => ['mixed', 'name'=>'string'], 'Yaf_Registry::has' => ['bool', 'name'=>'string'], 'Yaf_Registry::set' => ['bool', 'name'=>'string', 'value'=>'string'], -'Yaf_Request_Abstract::getActionName' => ['void'], -'Yaf_Request_Abstract::getBaseUri' => ['void'], -'Yaf_Request_Abstract::getControllerName' => ['void'], -'Yaf_Request_Abstract::getEnv' => ['void', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Abstract::getException' => ['void'], -'Yaf_Request_Abstract::getLanguage' => ['void'], -'Yaf_Request_Abstract::getMethod' => ['void'], -'Yaf_Request_Abstract::getModuleName' => ['void'], -'Yaf_Request_Abstract::getParam' => ['void', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Abstract::getParams' => ['void'], -'Yaf_Request_Abstract::getRequestUri' => ['void'], -'Yaf_Request_Abstract::getServer' => ['void', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Abstract::isCli' => ['void'], -'Yaf_Request_Abstract::isDispatched' => ['void'], -'Yaf_Request_Abstract::isGet' => ['void'], -'Yaf_Request_Abstract::isHead' => ['void'], -'Yaf_Request_Abstract::isOptions' => ['void'], -'Yaf_Request_Abstract::isPost' => ['void'], -'Yaf_Request_Abstract::isPut' => ['void'], -'Yaf_Request_Abstract::isRouted' => ['void'], -'Yaf_Request_Abstract::isXmlHttpRequest' => ['void'], -'Yaf_Request_Abstract::setActionName' => ['void', 'action'=>'string'], -'Yaf_Request_Abstract::setBaseUri' => ['bool', 'uir'=>'string'], -'Yaf_Request_Abstract::setControllerName' => ['void', 'controller'=>'string'], -'Yaf_Request_Abstract::setDispatched' => ['void'], -'Yaf_Request_Abstract::setModuleName' => ['void', 'module'=>'string'], -'Yaf_Request_Abstract::setParam' => ['void', 'name'=>'string', 'value='=>'string'], -'Yaf_Request_Abstract::setRequestUri' => ['void', 'uir'=>'string'], -'Yaf_Request_Abstract::setRouted' => ['void', 'flag='=>'string'], +'Yaf_Request_Abstract::get' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getActionName' => ['?string'], +'Yaf_Request_Abstract::getBaseUri' => ['?string'], +'Yaf_Request_Abstract::getCookie' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getControllerName' => ['?string'], +'Yaf_Request_Abstract::getEnv' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getException' => ['?Exception'], +'Yaf_Request_Abstract::getFiles' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getLanguage' => ['?string'], +'Yaf_Request_Abstract::getMethod' => ['?string'], +'Yaf_Request_Abstract::getModuleName' => ['?string'], +'Yaf_Request_Abstract::getParam' => ['mixed', 'name'=>'string', 'default='=>'mixed'], +'Yaf_Request_Abstract::getParams' => ['?array'], +'Yaf_Request_Abstract::getPost' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getQuery' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getRaw' => ['?string'], +'Yaf_Request_Abstract::getRequest' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::getRequestUri' => ['?string'], +'Yaf_Request_Abstract::getServer' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Abstract::cleanParams' => ['?Yaf_Request_Abstract'], +'Yaf_Request_Abstract::isCli' => ['bool'], +'Yaf_Request_Abstract::isDelete' => ['bool'], +'Yaf_Request_Abstract::isDispatched' => ['bool'], +'Yaf_Request_Abstract::isGet' => ['bool'], +'Yaf_Request_Abstract::isHead' => ['bool'], +'Yaf_Request_Abstract::isOptions' => ['bool'], +'Yaf_Request_Abstract::isPatch' => ['bool'], +'Yaf_Request_Abstract::isPost' => ['bool'], +'Yaf_Request_Abstract::isPut' => ['bool'], +'Yaf_Request_Abstract::isRouted' => ['bool'], +'Yaf_Request_Abstract::isXmlHttpRequest' => ['bool'], +'Yaf_Request_Abstract::setActionName' => ['?Yaf_Request_Abstract', 'action'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Abstract::setBaseUri' => ['Yaf_Request_Abstract|false', 'uir'=>'string'], +'Yaf_Request_Abstract::setControllerName' => ['?Yaf_Request_Abstract', 'controller'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Abstract::setDispatched' => ['?Yaf_Request_Abstract', 'flag='=>'bool|true'], +'Yaf_Request_Abstract::setModuleName' => ['?Yaf_Request_Abstract', 'module'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Abstract::setParam' => ['Yaf_Request_Abstract|false|null', 'name'=>'mixed', 'value='=>'?mixed'], +'Yaf_Request_Abstract::setRequestUri' => ['?Yaf_Request_Abstract', 'uir'=>'string'], +'Yaf_Request_Abstract::setRouted' => ['?Yaf_Request_Abstract', 'flag='=>'bool|true'], 'Yaf_Request_Http::__clone' => ['void'], -'Yaf_Request_Http::__construct' => ['void'], -'Yaf_Request_Http::get' => ['mixed', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Http::getActionName' => ['string'], -'Yaf_Request_Http::getBaseUri' => ['string'], -'Yaf_Request_Http::getControllerName' => ['string'], -'Yaf_Request_Http::getCookie' => ['mixed', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Http::getEnv' => ['mixed', 'name='=>'string', 'default='=>'mixed'], -'Yaf_Request_Http::getException' => ['Yaf_Exception'], -'Yaf_Request_Http::getFiles' => ['void'], -'Yaf_Request_Http::getLanguage' => ['string'], -'Yaf_Request_Http::getMethod' => ['string'], -'Yaf_Request_Http::getModuleName' => ['string'], +'Yaf_Request_Http::__construct' => ['void', 'requestUri='=>'?string', 'baseUri='=>'?string'], +'Yaf_Request_Http::get' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getActionName' => ['?string'], +'Yaf_Request_Http::getBaseUri' => ['?string'], +'Yaf_Request_Http::getCookie' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getControllerName' => ['?string'], +'Yaf_Request_Http::getEnv' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getException' => ['?Exception'], +'Yaf_Request_Http::getFiles' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getLanguage' => ['?string'], +'Yaf_Request_Http::getMethod' => ['?string'], +'Yaf_Request_Http::getModuleName' => ['?string'], 'Yaf_Request_Http::getParam' => ['mixed', 'name'=>'string', 'default='=>'mixed'], -'Yaf_Request_Http::getParams' => ['array'], -'Yaf_Request_Http::getPost' => ['mixed', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Http::getQuery' => ['mixed', 'name'=>'string', 'default='=>'string'], -'Yaf_Request_Http::getRaw' => ['mixed'], -'Yaf_Request_Http::getRequest' => ['void'], -'Yaf_Request_Http::getRequestUri' => ['string'], -'Yaf_Request_Http::getServer' => ['mixed', 'name='=>'string', 'default='=>'mixed'], +'Yaf_Request_Http::getParams' => ['?array'], +'Yaf_Request_Http::getPost' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getQuery' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getRaw' => ['?string'], +'Yaf_Request_Http::getRequest' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::getRequestUri' => ['?string'], +'Yaf_Request_Http::getServer' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Http::cleanParams' => ['?Yaf_Request_Http'], 'Yaf_Request_Http::isCli' => ['bool'], +'Yaf_Request_Http::isDelete' => ['bool'], 'Yaf_Request_Http::isDispatched' => ['bool'], 'Yaf_Request_Http::isGet' => ['bool'], 'Yaf_Request_Http::isHead' => ['bool'], 'Yaf_Request_Http::isOptions' => ['bool'], +'Yaf_Request_Http::isPatch' => ['bool'], 'Yaf_Request_Http::isPost' => ['bool'], 'Yaf_Request_Http::isPut' => ['bool'], 'Yaf_Request_Http::isRouted' => ['bool'], 'Yaf_Request_Http::isXmlHttpRequest' => ['bool'], -'Yaf_Request_Http::setActionName' => ['Yaf_Request_Abstract|bool', 'action'=>'string'], -'Yaf_Request_Http::setBaseUri' => ['bool', 'uri'=>'string'], -'Yaf_Request_Http::setControllerName' => ['Yaf_Request_Abstract|bool', 'controller'=>'string'], -'Yaf_Request_Http::setDispatched' => ['bool'], -'Yaf_Request_Http::setModuleName' => ['Yaf_Request_Abstract|bool', 'module'=>'string'], -'Yaf_Request_Http::setParam' => ['Yaf_Request_Abstract|bool', 'name'=>'array|string', 'value='=>'string'], -'Yaf_Request_Http::setRequestUri' => ['', 'uri'=>'string'], -'Yaf_Request_Http::setRouted' => ['Yaf_Request_Abstract|bool'], -'Yaf_Request_Simple::__clone' => ['void'], -'Yaf_Request_Simple::__construct' => ['void'], -'Yaf_Request_Simple::get' => ['void'], -'Yaf_Request_Simple::getActionName' => ['string'], -'Yaf_Request_Simple::getBaseUri' => ['string'], -'Yaf_Request_Simple::getControllerName' => ['string'], -'Yaf_Request_Simple::getCookie' => ['void'], -'Yaf_Request_Simple::getEnv' => ['mixed', 'name='=>'string', 'default='=>'mixed'], -'Yaf_Request_Simple::getException' => ['Yaf_Exception'], -'Yaf_Request_Simple::getFiles' => ['void'], -'Yaf_Request_Simple::getLanguage' => ['string'], -'Yaf_Request_Simple::getMethod' => ['string'], -'Yaf_Request_Simple::getModuleName' => ['string'], +'Yaf_Request_Http::setActionName' => ['?Yaf_Request_Http', 'action'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Http::setBaseUri' => ['Yaf_Request_Http|false', 'uir'=>'string'], +'Yaf_Request_Http::setControllerName' => ['?Yaf_Request_Http', 'controller'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Http::setDispatched' => ['?Yaf_Request_Http', 'flag='=>'bool|true'], +'Yaf_Request_Http::setModuleName' => ['?Yaf_Request_Http', 'module'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Http::setParam' => ['Yaf_Request_Http|false|null', 'name'=>'mixed', 'value='=>'?mixed'], +'Yaf_Request_Http::setRequestUri' => ['?Yaf_Request_Http', 'uir'=>'string'], +'Yaf_Request_Http::setRouted' => ['?Yaf_Request_Http', 'flag='=>'bool|true'], +'Yaf_Request_Simple::__construct' => ['void', 'method='=>'?string', 'module='=>'?string', 'controller='=>'?string', 'action='=>'?string', 'params='=>'?array'], +'Yaf_Request_Simple::get' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getActionName' => ['?string'], +'Yaf_Request_Simple::getBaseUri' => ['?string'], +'Yaf_Request_Simple::getCookie' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getControllerName' => ['?string'], +'Yaf_Request_Simple::getEnv' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getException' => ['?Exception'], +'Yaf_Request_Simple::getFiles' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getLanguage' => ['?string'], +'Yaf_Request_Simple::getMethod' => ['?string'], +'Yaf_Request_Simple::getModuleName' => ['?string'], 'Yaf_Request_Simple::getParam' => ['mixed', 'name'=>'string', 'default='=>'mixed'], -'Yaf_Request_Simple::getParams' => ['array'], -'Yaf_Request_Simple::getPost' => ['void'], -'Yaf_Request_Simple::getQuery' => ['void'], -'Yaf_Request_Simple::getRequest' => ['void'], -'Yaf_Request_Simple::getRequestUri' => ['string'], -'Yaf_Request_Simple::getServer' => ['mixed', 'name='=>'string', 'default='=>'mixed'], +'Yaf_Request_Simple::getParams' => ['?array'], +'Yaf_Request_Simple::getPost' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getQuery' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getRaw' => ['?string'], +'Yaf_Request_Simple::getRequest' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::getRequestUri' => ['?string'], +'Yaf_Request_Simple::getServer' => ['mixed', 'name='=>'?string', 'default='=>'?mixed'], +'Yaf_Request_Simple::cleanParams' => ['?Yaf_Request_Simple'], 'Yaf_Request_Simple::isCli' => ['bool'], +'Yaf_Request_Simple::isDelete' => ['bool'], 'Yaf_Request_Simple::isDispatched' => ['bool'], 'Yaf_Request_Simple::isGet' => ['bool'], 'Yaf_Request_Simple::isHead' => ['bool'], 'Yaf_Request_Simple::isOptions' => ['bool'], +'Yaf_Request_Simple::isPatch' => ['bool'], 'Yaf_Request_Simple::isPost' => ['bool'], 'Yaf_Request_Simple::isPut' => ['bool'], 'Yaf_Request_Simple::isRouted' => ['bool'], -'Yaf_Request_Simple::isXmlHttpRequest' => ['void'], -'Yaf_Request_Simple::setActionName' => ['Yaf_Request_Abstract|bool', 'action'=>'string'], -'Yaf_Request_Simple::setBaseUri' => ['bool', 'uri'=>'string'], -'Yaf_Request_Simple::setControllerName' => ['Yaf_Request_Abstract|bool', 'controller'=>'string'], -'Yaf_Request_Simple::setDispatched' => ['bool'], -'Yaf_Request_Simple::setModuleName' => ['Yaf_Request_Abstract|bool', 'module'=>'string'], -'Yaf_Request_Simple::setParam' => ['Yaf_Request_Abstract|bool', 'name'=>'array|string', 'value='=>'string'], -'Yaf_Request_Simple::setRequestUri' => ['', 'uri'=>'string'], -'Yaf_Request_Simple::setRouted' => ['Yaf_Request_Abstract|bool'], +'Yaf_Request_Simple::isXmlHttpRequest' => ['bool'], +'Yaf_Request_Simple::setActionName' => ['?Yaf_Request_Simple', 'action'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Simple::setBaseUri' => ['Yaf_Request_Simple|false', 'uir'=>'string'], +'Yaf_Request_Simple::setControllerName' => ['?Yaf_Request_Simple', 'controller'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Simple::setDispatched' => ['?Yaf_Request_Simple', 'flag='=>'bool|true'], +'Yaf_Request_Simple::setModuleName' => ['?Yaf_Request_Simple', 'module'=>'string', 'format_name='=>'bool|true'], +'Yaf_Request_Simple::setParam' => ['Yaf_Request_Simple|bool|null', 'name'=>'mixed', 'value='=>'?mixed'], +'Yaf_Request_Simple::setRequestUri' => ['?Yaf_Request_Simple', 'uir'=>'string'], +'Yaf_Request_Simple::setRouted' => ['?Yaf_Request_Simple', 'flag='=>'bool|true'], 'Yaf_Response_Abstract::__clone' => ['void'], 'Yaf_Response_Abstract::__construct' => ['void'], 'Yaf_Response_Abstract::__destruct' => ['void'], 'Yaf_Response_Abstract::__toString' => ['string'], -'Yaf_Response_Abstract::appendBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Abstract::clearBody' => ['bool', 'key='=>'string'], -'Yaf_Response_Abstract::clearHeaders' => ['void'], -'Yaf_Response_Abstract::getBody' => ['mixed', 'key='=>'string'], -'Yaf_Response_Abstract::getHeader' => ['void'], -'Yaf_Response_Abstract::prependBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Abstract::response' => ['void'], -'Yaf_Response_Abstract::setAllHeaders' => ['void'], -'Yaf_Response_Abstract::setBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Abstract::setHeader' => ['void'], -'Yaf_Response_Abstract::setRedirect' => ['void'], -'Yaf_Response_Cli::__clone' => [''], +'Yaf_Response_Abstract::appendBody' => ['Yaf_Response_Abstract|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Abstract::clearBody' => ['?Yaf_Response_Abstract', 'name='=>'?string'], +'Yaf_Response_Abstract::getBody' => ['mixed', 'name='=>'string'], +'Yaf_Response_Abstract::prependBody' => ['Yaf_Response_Abstract|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Abstract::response' => ['bool'], +'Yaf_Response_Abstract::setBody' => ['Yaf_Response_Abstract|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Abstract::setRedirect' => ['?bool', 'url'=>'string'], +'Yaf_Response_Cli::__clone' => ['void'], 'Yaf_Response_Cli::__construct' => ['void'], -'Yaf_Response_Cli::__destruct' => [''], +'Yaf_Response_Cli::__destruct' => ['void'], 'Yaf_Response_Cli::__toString' => ['string'], -'Yaf_Response_Cli::appendBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Cli::clearBody' => ['bool', 'key='=>'string'], -'Yaf_Response_Cli::getBody' => ['mixed', 'key='=>'null|string'], -'Yaf_Response_Cli::prependBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Cli::setBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Http::__clone' => [''], +'Yaf_Response_Cli::appendBody' => ['Yaf_Response_Cli|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Cli::clearBody' => ['?Yaf_Response_Cli', 'name='=>'?string'], +'Yaf_Response_Cli::getBody' => ['mixed', 'name='=>'string'], +'Yaf_Response_Cli::prependBody' => ['Yaf_Response_Cli|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Cli::response' => ['bool'], +'Yaf_Response_Cli::setBody' => ['Yaf_Response_Cli|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Cli::setRedirect' => ['?bool', 'url'=>'string'], +'Yaf_Response_Http::__clone' => ['void'], 'Yaf_Response_Http::__construct' => ['void'], -'Yaf_Response_Http::__destruct' => [''], +'Yaf_Response_Http::__destruct' => ['void'], 'Yaf_Response_Http::__toString' => ['string'], -'Yaf_Response_Http::appendBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Http::clearBody' => ['bool', 'key='=>'string'], -'Yaf_Response_Http::clearHeaders' => ['Yaf_Response_Abstract|false', 'name='=>'string'], -'Yaf_Response_Http::getBody' => ['mixed', 'key='=>'null|string'], +'Yaf_Response_Http::appendBody' => ['Yaf_Response_Http|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Http::clearHeaders' => ['Yaf_Response_Http|false|null'], +'Yaf_Response_Http::clearBody' => ['?Yaf_Response_Http', 'name='=>'?string'], +'Yaf_Response_Http::getBody' => ['mixed', 'name='=>'string'], 'Yaf_Response_Http::getHeader' => ['mixed', 'name='=>'string'], -'Yaf_Response_Http::prependBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Http::response' => ['bool'], +'Yaf_Response_Http::prependBody' => ['Yaf_Response_Http|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Http::response' => ['?bool'], 'Yaf_Response_Http::setAllHeaders' => ['bool', 'headers'=>'array'], -'Yaf_Response_Http::setBody' => ['bool', 'content'=>'string', 'key='=>'string'], -'Yaf_Response_Http::setHeader' => ['bool', 'name'=>'string', 'value'=>'string', 'replace='=>'bool|false', 'response_code='=>'int'], -'Yaf_Response_Http::setRedirect' => ['bool', 'url'=>'string'], +'Yaf_Response_Http::setBody' => ['Yaf_Response_Http|false|null', 'body'=>'string', 'name='=>'?string'], +'Yaf_Response_Http::setHeader' => ['?bool', 'name'=>'string', 'value'=>'string', 'replace='=>'bool|false', 'response_code='=>'int'], +'Yaf_Response_Http::setRedirect' => ['?bool', 'url'=>'string'], 'Yaf_Route_Interface::__construct' => ['void'], 'Yaf_Route_Interface::assemble' => ['string', 'info'=>'array', 'query='=>'array'], 'Yaf_Route_Interface::route' => ['bool', 'request'=>'Yaf_Request_Abstract'], @@ -13565,7 +13884,7 @@ 'Yaf_Session::__get' => ['void', 'name'=>'string'], 'Yaf_Session::__isset' => ['void', 'name'=>'string'], 'Yaf_Session::__set' => ['void', 'name'=>'string', 'value'=>'string'], -'Yaf_Session::__sleep' => ['void'], +'Yaf_Session::__sleep' => ['list'], 'Yaf_Session::__unset' => ['void', 'name'=>'string'], 'Yaf_Session::__wakeup' => ['void'], 'Yaf_Session::count' => ['0|positive-int'], @@ -13584,23 +13903,24 @@ 'Yaf_Session::set' => ['Yaf_Session|bool', 'name'=>'string', 'value'=>'mixed'], 'Yaf_Session::start' => ['void'], 'Yaf_Session::valid' => ['void'], -'Yaf_View_Interface::assign' => ['bool', 'name'=>'string', 'value='=>'string'], -'Yaf_View_Interface::display' => ['bool', 'tpl'=>'string', 'tpl_vars='=>'array'], -'Yaf_View_Interface::getScriptPath' => ['void'], -'Yaf_View_Interface::render' => ['string', 'tpl'=>'string', 'tpl_vars='=>'array'], -'Yaf_View_Interface::setScriptPath' => ['void', 'template_dir'=>'string'], -'Yaf_View_Simple::__construct' => ['void', 'tempalte_dir'=>'string', 'options='=>'array'], -'Yaf_View_Simple::__get' => ['void', 'name='=>'string'], -'Yaf_View_Simple::__isset' => ['void', 'name'=>'string'], +'Yaf_View_Interface::assign' => ['Yaf_View_Interface|bool', 'name'=>'string', 'value='=>'?mixed'], +'Yaf_View_Interface::display' => ['Yaf_View_Interface|bool', 'tpl'=>'string', 'tpl_vars='=>'?array'], +'Yaf_View_Interface::getScriptPath' => ['string'], +'Yaf_View_Interface::render' => ['string|bool', 'tpl'=>'string', 'tpl_vars='=>'?array'], +'Yaf_View_Interface::setScriptPath' => ['bool', 'template_dir'=>'string'], +'Yaf_View_Simple::__construct' => ['void', 'tempalte_dir'=>'string', 'options='=>'?array'], +'Yaf_View_Simple::__get' => ['mixed', 'name='=>'?string'], +'Yaf_View_Simple::__isset' => ['bool', 'name'=>'string'], 'Yaf_View_Simple::__set' => ['void', 'name'=>'string', 'value'=>'mixed'], -'Yaf_View_Simple::assign' => ['bool', 'name'=>'string', 'value='=>'mixed'], -'Yaf_View_Simple::assignRef' => ['bool', 'name'=>'string', '&rw_value'=>'mixed'], -'Yaf_View_Simple::clear' => ['bool', 'name='=>'string'], -'Yaf_View_Simple::display' => ['bool', 'tpl'=>'string', 'tpl_vars='=>'array'], -'Yaf_View_Simple::eval' => ['string', 'tpl_content'=>'string', 'tpl_vars='=>'array'], -'Yaf_View_Simple::getScriptPath' => ['string'], -'Yaf_View_Simple::render' => ['string', 'tpl'=>'string', 'tpl_vars='=>'array'], -'Yaf_View_Simple::setScriptPath' => ['bool', 'template_dir'=>'string'], +'Yaf_View_Simple::assign' => ['Yaf_View_Simple|false|null', 'name='=>'?mixed', 'default='=>'?mixed'], +'Yaf_View_Simple::assignRef' => ['?Yaf_View_Simple', 'name'=>'string', '&value'=>'mixed'], +'Yaf_View_Simple::clear' => ['?Yaf_View_Simple', 'name='=>'string'], +'Yaf_View_Simple::display' => ['?bool', 'tpl'=>'string', 'tpl_vars='=>'?array'], +'Yaf_View_Simple::eval' => ['string|null|false', 'tpl_str'=>'string', 'vars='=>'?array'], +'Yaf_View_Simple::get' => ['mixed', 'name='=>'?string'], +'Yaf_View_Simple::getScriptPath' => ['?string'], +'Yaf_View_Simple::render' => ['string|null|false', 'tpl'=>'string', 'tpl_vars='=>'?array'], +'Yaf_View_Simple::setScriptPath' => ['Yaf_View_Simple|false|null', 'template_dir'=>'string'], 'yaml_emit' => ['string', 'data'=>'mixed', 'encoding='=>'int', 'linebreak='=>'int'], 'yaml_emit_file' => ['bool', 'filename'=>'string', 'data'=>'mixed', 'encoding='=>'int', 'linebreak='=>'int'], 'yaml_parse' => ['mixed', 'input'=>'string', 'pos='=>'int', '&w_ndocs='=>'int', 'callbacks='=>'array'], diff --git a/resources/functionMap_bleedingEdge.php b/resources/functionMap_bleedingEdge.php new file mode 100644 index 0000000000..7c54feb8a6 --- /dev/null +++ b/resources/functionMap_bleedingEdge.php @@ -0,0 +1,134 @@ + [ + 'Closure::bind' => ['Closure', 'old'=>'Closure', 'to'=>'?object', 'scope='=>'object|class-string|\'static\'|null'], + 'Closure::bindTo' => ['Closure', 'new'=>'?object', 'newscope='=>'object|class-string|\'static\'|null'], + 'error_log' => ['bool', 'message'=>'string', 'message_type='=>'0|1|2|3|4', 'destination='=>'string', 'extra_headers='=>'string'], + 'SplFileObject::flock' => ['bool', 'operation'=>'int-mask', '&w_wouldblock='=>'0|1'], + 'Imagick::adaptiveBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::adaptiveSharpenImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::addNoiseImage' => ['bool', 'noise_type'=>'Imagick::NOISE_*', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::autoGammaImage' => ['bool', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::autoLevelImage' => ['bool', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::blurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::brightnessContrastImage' => ['bool', 'brightness'=>'float', 'contrast'=>'float', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::clampImage' => ['bool', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::combineImages' => ['Imagick', 'channeltype'=>'Imagick::CHANNEL_*'], + 'Imagick::compareImageChannels' => ['array{Imagick,float}', 'image'=>'imagick', 'channeltype'=>'Imagick::CHANNEL_*', 'metrictype'=>'Imagick::METRIC_*'], + 'Imagick::compareImageLayers' => ['Imagick', 'method'=>'Imagick::LAYERMETHOD_*'], + 'Imagick::compareImages' => ['array{Imagick,float}', 'compare'=>'imagick', 'metric'=>'Imagick::METRIC_*'], + 'Imagick::compositeImage' => ['bool', 'composite_object'=>'imagick', 'composite'=>'Imagick::COMPOSITE_*', 'x'=>'int', 'y'=>'int', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::contrastStretchImage' => ['bool', 'black_point'=>'float', 'white_point'=>'float', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::convolveImage' => ['bool', 'kernel'=>'array', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::distortImage' => ['bool', 'method'=>'Imagick::DISTORTION_*', 'arguments'=>'array', 'bestfit'=>'bool'], + 'Imagick::evaluateImage' => ['bool', 'op'=>'Imagick::EVALUATE_*', 'constant'=>'float', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::exportImagePixels' => ['list', 'x'=>'int', 'y'=>'int', 'width'=>'int', 'height'=>'int', 'map'=>'string', 'storage'=>'Imagick::PIXEL_*'], + 'Imagick::floodFillPaintImage' => ['bool', 'fill'=>'mixed', 'fuzz'=>'float', 'target'=>'mixed', 'x'=>'int', 'y'=>'int', 'invert'=>'bool', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::functionImage' => ['bool', 'function'=>'Imagick::FUNCTION_*', 'arguments'=>'array', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::fxImage' => ['Imagick', 'expression'=>'string', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::gammaImage' => ['bool', 'gamma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::gaussianBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::getImageChannelDepth' => ['int', 'channel'=>'Imagick::CHANNEL_*'], + 'Imagick::getImageChannelDistortion' => ['float', 'reference'=>'imagick', 'channel'=>'Imagick::CHANNEL_*', 'metric'=>'Imagick::METRIC_*'], + 'Imagick::getImageChannelDistortions' => ['float', 'reference'=>'imagick', 'metric'=>'Imagick::METRIC_*', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::getImageChannelExtrema' => ['array{minima:0|positive-int,maxima:0|positive-int}', 'channel'=>'Imagick::CHANNEL_*'], + 'Imagick::getImageChannelKurtosis' => ['array{kurtosis:float,skewness:float}', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::getImageChannelMean' => ['array{mean:float,standardDeviation:float}', 'channel'=>'Imagick::CHANNEL_*'], + 'Imagick::getImageChannelRange' => ['array{minima:float,maxima:float}', 'channel'=>'Imagick::CHANNEL_*'], + 'Imagick::getImageDistortion' => ['float', 'reference'=>'magickwand', 'metric'=>'Imagick::METRIC_*'], + 'Imagick::getResource' => ['int', 'type'=>'Imagick::RESOURCETYPE_*'], + 'Imagick::getResourceLimit' => ['int', 'type'=>'Imagick::RESOURCETYPE_*'], + 'Imagick::importImagePixels' => ['bool', 'x'=>'int', 'y'=>'int', 'width'=>'int', 'height'=>'int', 'map'=>'string', 'storage'=>'Imagick::PIXEL_*', 'pixels'=>'array'], + 'Imagick::levelImage' => ['bool', 'blackpoint'=>'float', 'gamma'=>'float', 'whitepoint'=>'float', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::mergeImageLayers' => ['Imagick', 'layer_method'=>'Imagick::LAYERMETHOD_*'], + 'Imagick::montageImage' => ['Imagick', 'draw'=>'imagickdraw', 'tile_geometry'=>'string', 'thumbnail_geometry'=>'string', 'mode'=>'Imagick::MONTAGEMODE_*', 'frame'=>'string'], + 'Imagick::morphology' => ['bool', 'morphologyMethod'=>'Imagick::MORPHOLOGY_*', 'iterations'=>'int', 'ImagickKernel'=>'ImagickKernel', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::motionBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'angle'=>'float', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::negateImage' => ['bool', 'gray'=>'bool', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::normalizeImage' => ['bool', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::opaquePaintImage' => ['bool', 'target'=>'mixed', 'fill'=>'mixed', 'fuzz'=>'float', 'invert'=>'bool', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::orderedPosterizeImage' => ['bool', 'threshold_map'=>'string', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::paintFloodfillImage' => ['bool', 'fill'=>'mixed', 'fuzz'=>'float', 'bordercolor'=>'mixed', 'x'=>'int', 'y'=>'int', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::paintOpaqueImage' => ['bool', 'target'=>'mixed', 'fill'=>'mixed', 'fuzz'=>'float', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::radialBlurImage' => ['bool', 'angle'=>'float', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::randomThresholdImage' => ['bool', 'low'=>'float', 'high'=>'float', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::remapImage' => ['bool', 'replacement'=>'imagick', 'dither'=>'Imagick::DITHERMETHOD_*'], + 'Imagick::rotationalBlurImage' => ['bool', 'float'=>'string', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::segmentImage' => ['bool', 'colorspace'=>'Imagick::COLORSPACE_*', 'cluster_threshold'=>'float', 'smooth_threshold'=>'float', 'verbose='=>'bool'], + 'Imagick::selectiveBlurImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'threshold'=>'float', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::separateImageChannel' => ['bool', 'channel'=>'Imagick::CHANNEL_*'], + 'Imagick::setColorspace' => ['bool', 'colorspace'=>'Imagick::COLORSPACE_*'], + 'Imagick::setCompression' => ['bool', 'compression'=>'Imagick::COMPRESSION_*'], + 'Imagick::setGravity' => ['bool', 'gravity'=>'Imagick::GRAVITY_*'], + 'Imagick::setImageAlphaChannel' => ['bool', 'mode'=>'Imagick::ALPHACHANNEL_*'], + 'Imagick::setImageChannelDepth' => ['bool', 'channel'=>'Imagick::CHANNEL_*', 'depth'=>'int'], + 'Imagick::setImageChannelMask' => ['', 'channel'=>'Imagick::CHANNEL_*'], + 'Imagick::setImageClipMask' => ['bool', 'clip_mask'=>'imagick'], + 'Imagick::setImageColormapColor' => ['bool', 'index'=>'int', 'color'=>'imagickpixel'], + 'Imagick::setImageColorspace' => ['bool', 'colorspace'=>'Imagick::COLORSPACE_*'], + 'Imagick::setImageCompose' => ['bool', 'compose'=>'Imagick::COMPOSITE_*'], + 'Imagick::setImageCompression' => ['bool', 'compression'=>'Imagick::COMPRESSION_*'], + 'Imagick::setImageDispose' => ['bool', 'dispose'=>'Imagick::DISPOSE_*'], + 'Imagick::setImageGravity' => ['bool', 'gravity'=>'Imagick::GRAVITY_*'], + 'Imagick::setImageInterlaceScheme' => ['bool', 'interlace_scheme'=>'Imagick::INTERLACE_*'], + 'Imagick::setImageInterpolateMethod' => ['bool', 'method'=>'Imagick::INTERPOLATE_*'], + 'Imagick::setImageOrientation' => ['bool', 'orientation'=>'Imagick::ORIENTATION_*'], + 'Imagick::setImageRenderingIntent' => ['bool', 'rendering_intent'=>'Imagick::RENDERINGINTENT_*'], + 'Imagick::setImageType' => ['bool', 'image_type'=>'Imagick::IMGTYPE_*'], + 'Imagick::setType' => ['bool', 'image_type'=>'Imagick::IMGTYPE_*'], + 'Imagick::sharpenImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::sigmoidalContrastImage' => ['bool', 'sharpen'=>'bool', 'alpha'=>'float', 'beta'=>'float', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::similarityImage' => ['Imagick', 'imagick'=>'Imagick', '&bestMatch'=>'array', '&similarity'=>'float', 'similarity_threshold'=>'float', 'metric'=>'Imagick::METRIC_*'], + 'Imagick::sparseColorImage' => ['bool', 'sparse_method'=>'Imagick::SPARSECOLORMETHOD_*', 'arguments'=>'array', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::statisticImage' => ['bool', 'type'=>'Imagick::STATISTIC_*', 'width'=>'int', 'height'=>'int', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::thresholdImage' => ['bool', 'threshold'=>'float', 'channel='=>'Imagick::CHANNEL_*'], + 'Imagick::transformImageColorspace' => ['bool', 'colorspace'=>'Imagick::COLORSPACE_*'], + 'Imagick::unsharpMaskImage' => ['bool', 'radius'=>'float', 'sigma'=>'float', 'amount'=>'float', 'threshold'=>'float', 'channel='=>'Imagick::CHANNEL_*'], + 'ImagickDraw::color' => ['bool', 'x'=>'float', 'y'=>'float', 'paintmethod'=>'Imagick::PAINT_*'], + 'ImagickDraw::composite' => ['bool', 'compose'=>'Imagick::COMPOSITE_*', 'x'=>'float', 'y'=>'float', 'width'=>'float', 'height'=>'float', 'compositewand'=>'imagick'], + 'ImagickDraw::getFillRule' => ['Imagick::FILLRULE_*'], + 'ImagickDraw::getFontStretch' => ['Imagick::STRETCH_*'], + 'ImagickDraw::getFontStyle' => ['Imagick::STYLE_*'], + 'ImagickDraw::getGravity' => ['Imagick::GRAVITY_*'], + 'ImagickDraw::getStrokeLineCap' => ['Imagick::LINECAP_*'], + 'ImagickDraw::getStrokeLineJoin' => ['Imagick::LINEJOIN_*'], + 'ImagickDraw::getTextAlignment' => ['Imagick::ALIGN_*'], + 'ImagickDraw::getTextDecoration' => ['Imagick::DECORATION_*'], + 'ImagickDraw::matte' => ['bool', 'x'=>'float', 'y'=>'float', 'paintmethod'=>'Imagick::PAINT_*'], + 'ImagickDraw::setClipRule' => ['bool', 'fill_rule'=>'Imagick::FILLRULE_*'], + 'ImagickDraw::setFillRule' => ['bool', 'fill_rule'=>'Imagick::FILLRULE_*'], + 'ImagickDraw::setFontStretch' => ['bool', 'fontstretch'=>'Imagick::STRETCH_*'], + 'ImagickDraw::setFontStyle' => ['bool', 'style'=>'Imagick::STYLE_*'], + 'ImagickDraw::setGravity' => ['bool', 'gravity'=>'Imagick::GRAVITY_*'], + 'ImagickDraw::setStrokeLineCap' => ['bool', 'linecap'=>'Imagick::LINECAP_*'], + 'ImagickDraw::setStrokeLineJoin' => ['bool', 'linejoin'=>'Imagick::LINEJOIN_*'], + 'ImagickDraw::setTextAlignment' => ['bool', 'alignment'=>'Imagick::ALIGN_*'], + 'ImagickDraw::setTextAntialias' => ['bool', 'antialias'=>'bool'], + 'ImagickDraw::setTextDecoration' => ['bool', 'decoration'=>'Imagick::DECORATION_*'], + 'ImagickKernel::fromBuiltin' => ['ImagickKernel', 'kernelType'=>'Imagick::KERNEL_*', 'kernelString'=>'string'], + 'ImagickKernel::scale' => ['void', 'scale'=>'float', 'normalizeFlag'=>'Imagick::NORMALIZE_KERNEL_*'], + 'max' => ['', '...arg1'=>'non-empty-array'], + 'mb_detect_order' => ['bool|list', 'encoding_list='=>'non-empty-list|non-falsy-string'], + 'min' => ['', '...arg1'=>'non-empty-array'], + 'file' => ['list|false', 'filename'=>'string', 'flags='=>'int-mask', 'context='=>'resource'], + 'flock' => ['bool', 'fp'=>'resource', 'operation'=>'int-mask', '&w_wouldblock='=>'0|1'], + 'ftp_append' => ['bool', 'ftp'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY'], + 'ftp_fget' => ['bool', 'stream'=>'resource', 'fp'=>'resource', 'remote_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'resumepos='=>'int'], + 'ftp_fput' => ['bool', 'stream'=>'resource', 'remote_file'=>'string', 'fp'=>'resource', 'mode='=>'FTP_ASCII|FTP_BINARY', 'startpos='=>'int'], + 'ftp_get' => ['bool', 'stream'=>'resource', 'local_file'=>'string', 'remote_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'resume_pos='=>'int'], + 'ftp_nb_fget' => ['int', 'stream'=>'resource', 'fp'=>'resource', 'remote_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'resumepos='=>'int'], + 'ftp_nb_fput' => ['int', 'stream'=>'resource', 'remote_file'=>'string', 'fp'=>'resource', 'mode='=>'FTP_ASCII|FTP_BINARY', 'startpos='=>'int'], + 'ftp_nb_get' => ['int|false', 'stream'=>'resource', 'local_file'=>'string', 'remote_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'resume_pos='=>'int'], + 'ftp_nb_put' => ['int|false', 'stream'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'startpos='=>'int'], + 'ftp_put' => ['bool', 'stream'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode='=>'FTP_ASCII|FTP_BINARY', 'startpos='=>'int'], + 'scandir' => ['list|false', 'dir'=>'string', 'sorting_order='=>'SCANDIR_SORT_ASCENDING|SCANDIR_SORT_DESCENDING| SCANDIR_SORT_NONE', 'context='=>'resource'], + 'stream_socket_client' => ['resource|false', 'remoteaddress'=>'string', '&w_errcode='=>'int', '&w_errstring='=>'string', 'timeout='=>'float', 'flags='=>'int-mask', 'context='=>'resource'], + 'stream_socket_enable_crypto' => ['0|bool', 'stream'=>'resource', 'enable'=>'bool', 'crypto_method='=>'STREAM_CRYPTO_METHOD_SSLv2_CLIENT|STREAM_CRYPTO_METHOD_SSLv3_CLIENT|STREAM_CRYPTO_METHOD_SSLv23_CLIENT|STREAM_CRYPTO_METHOD_ANY_CLIENT|STREAM_CRYPTO_METHOD_TLS_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT|STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT|STREAM_CRYPTO_METHOD_SSLv2_SERVER|STREAM_CRYPTO_METHOD_SSLv3_SERVER|STREAM_CRYPTO_METHOD_SSLv23_SERVER|STREAM_CRYPTO_METHOD_ANY_SERVER|STREAM_CRYPTO_METHOD_TLS_SERVER|STREAM_CRYPTO_METHOD_TLSv1_0_SERVER|STREAM_CRYPTO_METHOD_TLSv1_1_SERVER|STREAM_CRYPTO_METHOD_TLSv1_2_SERVER|STREAM_CRYPTO_METHOD_TLSv1_3_SERVER', 'session_stream='=>'resource'], + 'extract' => ['0|positive-int', '&rw_var_array'=>'array', 'extract_type='=>'EXTR_OVERWRITE|EXTR_SKIP|EXTR_PREFIX_SAME|EXTR_PREFIX_ALL|EXTR_PREFIX_INVALID|EXTR_IF_EXISTS|EXTR_PREFIX_IF_EXISTS|EXTR_REFS', 'prefix='=>'string|null'], + 'RecursiveIteratorIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator|IteratorAggregate', 'mode='=>'RecursiveIteratorIterator::LEAVES_ONLY|RecursiveIteratorIterator::SELF_FIRST|RecursiveIteratorIterator::CHILD_FIRST', 'flags='=>'0|RecursiveIteratorIterator::CATCH_GET_CHILD'], + ], + 'old' => [ + + ] +]; diff --git a/resources/functionMap_php74delta.php b/resources/functionMap_php74delta.php index f86a9c1db2..ec52b32bb1 100644 --- a/resources/functionMap_php74delta.php +++ b/resources/functionMap_php74delta.php @@ -39,9 +39,8 @@ 'FFI::typeof' => ['FFI\CType', '&ptr'=>'FFI\CData'], 'FFI::type' => ['FFI\CType', 'type'=>'string'], 'get_mangled_object_vars' => ['array', 'obj'=>'object'], - 'mb_str_split' => ['array|false', 'str'=>'string', 'split_length='=>'int', 'encoding='=>'string'], - 'password_algos' => ['array'], - 'password_hash' => ['string|false', 'password'=>'string', 'algo'=>'string|null', 'options='=>'array'], + 'mb_str_split' => ['list|false', 'str'=>'string', 'split_length='=>'int', 'encoding='=>'string'], + 'password_algos' => ['list'], 'password_needs_rehash' => ['bool', 'hash'=>'string', 'algo'=>'string|null', 'options='=>'array'], 'preg_replace_callback' => ['string|array|null', 'regex'=>'string|array', 'callback'=>'callable(array):string', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int', 'flags='=>'int'], 'preg_replace_callback_array' => ['string|array|null', 'pattern'=>'array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int', 'flags='=>'int'], diff --git a/resources/functionMap_php80delta.php b/resources/functionMap_php80delta.php index e249cd7e84..fd85f6750c 100644 --- a/resources/functionMap_php80delta.php +++ b/resources/functionMap_php80delta.php @@ -22,10 +22,13 @@ return [ 'new' => [ 'array_combine' => ['associative-array', 'keys'=>'string[]|int[]', 'values'=>'array'], + 'base64_decode' => ['string', 'string'=>'string', 'strict='=>'false'], + 'base64_decode\'1' => ['string|false', 'string'=>'string', 'strict='=>'true'], 'bcdiv' => ['string', 'dividend'=>'string', 'divisor'=>'string', 'scale='=>'int'], 'bcmod' => ['string', 'dividend'=>'string', 'divisor'=>'string', 'scale='=>'int'], 'bcpowmod' => ['string', 'base'=>'string', 'exponent'=>'string', 'modulus'=>'string', 'scale='=>'int'], 'call_user_func_array' => ['mixed', 'function'=>'callable', 'parameters'=>'array'], + 'ceil' => ['float', 'number'=>'float'], 'com_load_typelib' => ['bool', 'typelib_name'=>'string', 'case_insensitive='=>'true'], 'count_chars' => ['array|string', 'input'=>'string', 'mode='=>'int'], 'date_add' => ['DateTime', 'object'=>'DateTime', 'interval'=>'DateInterval'], @@ -35,18 +38,20 @@ 'date_isodate_set' => ['DateTime', 'object'=>'DateTime', 'year'=>'int', 'week'=>'int', 'day='=>'int|mixed'], 'date_parse' => ['array', 'date'=>'string'], 'date_sub' => ['DateTime', 'object'=>'DateTime', 'interval'=>'DateInterval'], - 'date_sun_info' => ['array', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], + 'date_sun_info' => ['array{sunrise: int|bool,sunset: int|bool,transit: int|bool,civil_twilight_begin: int|bool,civil_twilight_end: int|bool,nautical_twilight_begin: int|bool,nautical_twilight_end: int|bool,astronomical_twilight_begin: int|bool,astronomical_twilight_end: int|bool}', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], 'date_time_set' => ['DateTime', 'object'=>'DateTime', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], 'date_timestamp_set' => ['DateTime', 'object'=>'DateTime', 'unixtimestamp'=>'int'], 'date_timezone_set' => ['DateTime', 'object'=>'DateTime', 'timezone'=>'DateTimeZone'], - 'explode' => ['non-empty-array', 'separator'=>'non-empty-string', 'str'=>'string', 'limit='=>'int'], + 'explode' => ['list', 'separator'=>'non-empty-string', 'str'=>'string', 'limit='=>'int'], 'fdiv' => ['float', 'dividend'=>'float', 'divisor'=>'float'], + 'floor' => ['float', 'number'=>'float'], + 'forward_static_call_array' => ['mixed', 'function'=>'callable', 'parameters'=>'array'], 'get_debug_type' => ['string', 'var'=>'mixed'], 'get_resource_id' => ['int', 'res'=>'resource'], 'gmdate' => ['string', 'format'=>'string', 'timestamp='=>'int'], 'gmmktime' => ['int|false', 'hour'=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], - 'hash' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], - 'hash_hkdf' => ['non-empty-string', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], + 'hash' => ['non-falsy-string', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], + 'hash_hkdf' => ['non-falsy-string', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], 'hash_hmac' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], 'hash_pbkdf2' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], 'imageaffine' => ['false|object', 'src'=>'resource', 'affine'=>'array', 'clip='=>'array'], @@ -74,12 +79,13 @@ 'imagescale' => ['false|object', 'im'=>'resource', 'new_width'=>'int', 'new_height='=>'int', 'method='=>'int'], 'ldap_set_rebind_proc' => ['bool', 'ldap'=>'resource', 'callback'=>'?callable'], 'mb_decode_numericentity' => ['string|false', 'string'=>'string', 'convmap'=>'array', 'encoding='=>'string'], - 'mb_str_split' => ['array', 'str'=>'string', 'split_length='=>'positive-int', 'encoding='=>'string'], + 'mb_encoding_aliases' => ['list', 'encoding'=>'string'], + 'mb_str_split' => ['list', 'str'=>'string', 'split_length='=>'positive-int', 'encoding='=>'string'], 'mb_strlen' => ['0|positive-int', 'str'=>'string', 'encoding='=>'string'], 'mktime' => ['int|false', 'hour'=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], 'odbc_exec' => ['resource|false', 'connection_id'=>'resource', 'query'=>'string'], 'parse_str' => ['void', 'encoded_string'=>'string', '&w_result'=>'array'], - 'password_hash' => ['string', 'password'=>'string', 'algo'=>'string|int|null', 'options='=>'array'], + 'password_hash' => ['non-empty-string', 'password'=>'string', 'algo'=>'string|int|null', 'options='=>'array'], 'PDOStatement::fetchAll' => ['array', 'how='=>'int', 'fetch_argument='=>'int|string|callable', 'ctor_args='=>'?array'], 'PhpToken::tokenize' => ['list', 'code'=>'string', 'flags='=>'int'], 'PhpToken::is' => ['bool', 'kind'=>'string|int|string[]|int[]'], @@ -89,14 +95,14 @@ 'proc_get_status' => ['array{command: string, pid: int, running: bool, signaled: bool, stopped: bool, exitcode: int, termsig: int, stopsig: int}', 'process'=>'resource'], 'set_error_handler' => ['?callable', 'callback'=>'null|callable(int,string,string,int):bool', 'error_types='=>'int'], 'socket_addrinfo_lookup' => ['AddressInfo[]', 'node'=>'string', 'service='=>'mixed', 'hints='=>'array'], - 'socket_select' => ['int|false', '&rw_read'=>'Socket[]|null', '&rw_write'=>'Socket[]|null', '&rw_except'=>'Socket[]|null', 'seconds'=>'int|null', 'microseconds='=>'int'], + 'socket_select' => ['int|false', '&w_read'=>'Socket[]|null', '&w_write'=>'Socket[]|null', '&w_except'=>'Socket[]|null', 'seconds'=>'int|null', 'microseconds='=>'int'], 'sodium_crypto_aead_chacha20poly1305_ietf_decrypt' => ['string|false', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], 'str_contains' => ['bool', 'haystack'=>'string', 'needle'=>'string'], - 'str_split' => ['non-empty-array', 'str'=>'string', 'split_length='=>'positive-int'], + 'str_split' => ['non-empty-list', 'str'=>'string', 'split_length='=>'positive-int'], 'str_ends_with' => ['bool', 'haystack'=>'string', 'needle'=>'string'], 'str_starts_with' => ['bool', 'haystack'=>'string', 'needle'=>'string'], 'strchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], - 'stripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], + 'stripos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'stristr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], 'strpos' => ['positive-int|0|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'strrchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string'], @@ -104,6 +110,7 @@ 'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], 'substr' => ['string', 'string'=>'string', 'start'=>'int', 'length='=>'int'], + 'round' => ['float', 'number'=>'float', 'precision='=>'int', 'mode='=>'1|2|3|4'], 'version_compare' => ['int|bool', 'version1'=>'string', 'version2'=>'string', 'operator='=>'string'], 'xml_parser_create' => ['XMLParser', 'encoding='=>'string'], 'xml_parser_create_ns' => ['XMLParser', 'encoding='=>'string', 'sep='=>'string'], @@ -154,11 +161,12 @@ 'xmlwriter_write_raw' => ['bool', 'xmlwriter'=>'XMLWriter', 'content'=>'string'], ], 'old' => [ - 'array_combine' => ['associative-array|false', 'keys'=>'string[]|int[]', 'values'=>'array'], 'bcdiv' => ['?string', 'dividend'=>'string', 'divisor'=>'string', 'scale='=>'int'], 'bcmod' => ['?string', 'dividend'=>'string', 'divisor'=>'string', 'scale='=>'int'], 'bcpowmod' => ['?string', 'base'=>'string', 'exponent'=>'string', 'modulus'=>'string', 'scale='=>'int'], + 'ceil' => ['__benevolent', 'number'=>'float'], + 'convert_cyr_string' => ['string', 'str'=>'string', 'from'=>'string', 'to'=>'string'], 'com_load_typelib' => ['bool', 'typelib_name'=>'string', 'case_insensitive='=>'bool'], 'count_chars' => ['array|false|string', 'input'=>'string', 'mode='=>'int'], 'date_add' => ['DateTime|false', 'object'=>'DateTime', 'interval'=>'DateInterval'], @@ -168,11 +176,15 @@ 'date_isodate_set' => ['DateTime|false', 'object'=>'DateTime', 'year'=>'int', 'week'=>'int', 'day='=>'int|mixed'], 'date_parse' => ['array|false', 'date'=>'string'], 'date_sub' => ['DateTime|false', 'object'=>'DateTime', 'interval'=>'DateInterval'], - 'date_sun_info' => ['array|false', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], + 'date_sun_info' => ['__benevolent', 'time'=>'int', 'latitude'=>'float', 'longitude'=>'float'], 'date_time_set' => ['DateTime|false', 'object'=>'DateTime', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], 'date_timestamp_set' => ['DateTime|false', 'object'=>'DateTime', 'unixtimestamp'=>'int'], 'date_timezone_set' => ['DateTime|false', 'object'=>'DateTime', 'timezone'=>'DateTimeZone'], 'each' => ['array{0:int|string,key:int|string,1:mixed,value:mixed}', '&r_arr'=>'array'], + 'ezmlm_hash' => ['int', 'addr'=>'string'], + 'fgetss' => ['string|false', 'fp'=>'resource', 'length='=>'0|positive-int', 'allowable_tags='=>'string'], + 'floor' => ['__benevolent', 'number'=>'float'], + 'get_magic_quotes_gpc' => ['false'], 'gmdate' => ['string|false', 'format'=>'string', 'timestamp='=>'int'], 'gmmktime' => ['int|false', 'hour='=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], 'gmp_random' => ['GMP', 'limiter='=>'int'], @@ -181,6 +193,7 @@ 'hash_hkdf' => ['non-empty-string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], 'hash_hmac' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], 'hash_pbkdf2' => ['non-empty-string|false', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], + 'hebrevc' => ['string', 'str'=>'string', 'max_chars_per_line='=>'int'], 'image2wbmp' => ['bool', 'im'=>'resource', 'filename='=>'?string', 'threshold='=>'int'], 'imageaffine' => ['resource|false', 'src'=>'resource', 'affine'=>'array', 'clip='=>'array'], 'imagecreate' => ['resource|false', 'x_size'=>'int', 'y_size'=>'int'], @@ -205,24 +218,30 @@ 'imagejpeg' => ['bool', 'im'=>'resource', 'filename='=>'string|resource|null', 'quality='=>'int'], 'imagerotate' => ['resource|false', 'src_im'=>'resource', 'angle'=>'float', 'bgdcolor'=>'int', 'ignoretransparent='=>'int'], 'imagescale' => ['resource|false', 'im'=>'resource', 'new_width'=>'int', 'new_height='=>'int', 'method='=>'int'], + 'imap_header' => ['stdClass|false', 'stream_id'=>'resource', 'msg_no'=>'int', 'from_length='=>'int', 'subject_length='=>'int', 'default_host='=>'string'], 'implode\'1' => ['string', 'pieces'=>'array'], 'jpeg2wbmp' => ['bool', 'jpegname'=>'string', 'wbmpname'=>'string', 'dest_height'=>'int', 'dest_width'=>'int', 'threshold'=>'int'], + 'ldap_control_paged_result' => ['bool', 'link_identifier'=>'resource', 'pagesize'=>'int', 'iscritical='=>'bool', 'cookie='=>'string'], + 'ldap_control_paged_result_response' => ['bool', 'link_identifier'=>'resource', 'result_identifier'=>'resource', '&w_cookie='=>'string', '&w_estimated='=>'int'], 'ldap_set_rebind_proc' => ['bool', 'link_identifier'=>'resource', 'callback'=>'callable'], 'ldap_sort' => ['bool', 'link_identifier'=>'resource', 'result_identifier'=>'resource', 'sortfilter'=>'string'], 'mb_decode_numericentity' => ['string|false', 'string'=>'string', 'convmap'=>'array', 'encoding='=>'string', 'is_hex='=>'bool'], - 'mktime' => ['int|false', 'hour='=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], 'mb_strlen' => ['0|positive-int', 'str'=>'string', 'encoding='=>'string'], + 'mktime' => ['int|false', 'hour='=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], + 'money_format' => ['string', 'format'=>'string', 'value'=>'float'], 'odbc_exec' => ['resource|false', 'connection_id'=>'resource', 'query'=>'string', 'flags='=>'int'], 'parse_str' => ['void', 'encoded_string'=>'string', '&w_result='=>'array'], - 'password_hash' => ['string|false|null', 'password'=>'string', 'algo'=>'?string|?int', 'options='=>'array'], + 'password_hash' => ['__benevolent', 'password'=>'string', 'algo'=>'string|int', 'options='=>'array'], 'png2wbmp' => ['bool', 'pngname'=>'string', 'wbmpname'=>'string', 'dest_height'=>'int', 'dest_width'=>'int', 'threshold'=>'int'], 'proc_get_status' => ['array{command: string, pid: int, running: bool, signaled: bool, stopped: bool, exitcode: int, termsig: int, stopsig: int}|false', 'process'=>'resource'], 'read_exif_data' => ['array', 'filename'=>'string', 'sections_needed='=>'string', 'sub_arrays='=>'bool', 'read_thumbnail='=>'bool'], - 'socket_select' => ['int|false', '&rw_read_fds'=>'resource[]|null', '&rw_write_fds'=>'resource[]|null', '&rw_except_fds'=>'resource[]|null', 'tv_sec'=>'int|null', 'tv_usec='=>'int|null'], + 'restore_include_path' => ['void'], + 'round' => ['__benevolent', 'number'=>'float', 'precision='=>'int', 'mode='=>'1|2|3|4'], + 'socket_select' => ['int|false', '&w_read_fds'=>'resource[]|null', '&w_write_fds'=>'resource[]|null', '&w_except_fds'=>'resource[]|null', 'tv_sec'=>'int|null', 'tv_usec='=>'int|null'], 'sodium_crypto_aead_chacha20poly1305_ietf_decrypt' => ['?string|?false', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], 'SplFileObject::fgetss' => ['string|false', 'allowable_tags='=>'string'], 'strchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int', 'before_needle='=>'bool'], - 'stripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], + 'stripos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'stristr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int', 'before_needle='=>'bool'], 'strpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'strrchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int'], diff --git a/resources/functionMap_php80delta_bleedingEdge.php b/resources/functionMap_php80delta_bleedingEdge.php new file mode 100644 index 0000000000..990aafe691 --- /dev/null +++ b/resources/functionMap_php80delta_bleedingEdge.php @@ -0,0 +1,15 @@ + [ + 'error_log' => ['bool', 'message'=>'string', 'message_type='=>'0|1|3|4', 'destination='=>'string', 'extra_headers='=>'string'], + 'filter_input' => ['mixed', 'type'=>'INPUT_GET|INPUT_POST|INPUT_COOKIE|INPUT_SERVER|INPUT_ENV', 'variable_name'=>'string', 'filter='=>'int', 'options='=>'array|int'], + 'filter_input_array' => ['array|false|null', 'type'=>'INPUT_GET|INPUT_POST|INPUT_COOKIE|INPUT_SERVER|INPUT_ENV', 'definition='=>'int|array', 'add_empty='=>'bool'], + 'hash_hkdf' => ['non-empty-string', 'algo'=>'non-falsy-string', 'key'=>'string', 'length='=>'0|positive-int', 'info='=>'string', 'salt='=>'string'], + 'hash_pbkdf2' => ['non-empty-string', 'algo'=>'non-falsy-string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'positive-int', 'length='=>'0|positive-int', 'raw_output='=>'bool'], + 'mb_detect_order' => ['bool|list', 'encoding_list='=>'non-empty-list|non-falsy-string|null'], + ], + 'old' => [ + + ] +]; diff --git a/resources/functionMap_php81delta.php b/resources/functionMap_php81delta.php index 6d55567897..48f1f8decf 100644 --- a/resources/functionMap_php81delta.php +++ b/resources/functionMap_php81delta.php @@ -21,7 +21,12 @@ */ return [ 'new' => [ - + 'mysqli_fetch_field' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: 0, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'result'=>'mysqli_result'], + 'mysqli_fetch_field_direct' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: 0, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'result'=>'mysqli_result', 'fieldnr'=>'int'], + 'mysqli_fetch_fields' => ['list', 'result'=>'mysqli_result'], + 'mysqli_result::fetch_field' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: 0, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false'], + 'mysqli_result::fetch_field_direct' => ['(stdClass&object{name: string, orgname: string, table: string, orgtable: string, def: string, db: string, catalog: "def", max_length: 0, length: int, charsetnr: string, flags: int, type: int, decimals: int})|false', 'fieldnr'=>'int'], + 'mysqli_result::fetch_fields' => ['list'], ], 'old' => [ 'pg_escape_bytea' => ['string', 'connection'=>'resource', 'data'=>'string'], diff --git a/resources/functionMap_php82delta.php b/resources/functionMap_php82delta.php index 72c613a4d8..6054b7a9ce 100644 --- a/resources/functionMap_php82delta.php +++ b/resources/functionMap_php82delta.php @@ -21,7 +21,9 @@ */ return [ 'new' => [ - 'str_split' => ['array', 'str'=>'string', 'split_length='=>'positive-int'], + 'iterator_count' => ['0|positive-int', 'iterator'=>'iterable'], + 'iterator_to_array' => ['array', 'iterator'=>'iterable', 'use_keys='=>'bool'], + 'str_split' => ['list', 'str'=>'string', 'split_length='=>'positive-int'], ], 'old' => [ diff --git a/resources/functionMap_php83delta.php b/resources/functionMap_php83delta.php new file mode 100644 index 0000000000..c783a703d2 --- /dev/null +++ b/resources/functionMap_php83delta.php @@ -0,0 +1,31 @@ + [ + 'str_decrement' => ['non-empty-string', 'string'=>'non-empty-string'], + 'str_increment' => ['non-falsy-string', 'string'=>'non-empty-string'], + 'gc_status' => ['array{running:bool,protected:bool,full:bool,runs:int,collected:int,threshold:int,buffer_size:int,roots:int,application_time:float,collector_time:float,destructor_time:float,free_time:float}'], + ], + 'old' => [ + + ] +]; diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index e04a708bf1..08a416412c 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -44,6 +44,8 @@ 'Cassandra\\Exception\\UnpreparedException::__construct' => ['hasSideEffects' => false], 'Cassandra\\Exception\\ValidationException::__construct' => ['hasSideEffects' => false], 'Cassandra\\Exception\\WriteTimeoutException::__construct' => ['hasSideEffects' => false], + 'Closure::bind' => ['hasSideEffects' => false], + 'Closure::bindTo' => ['hasSideEffects' => false], 'Collator::__construct' => ['hasSideEffects' => false], 'Collator::compare' => ['hasSideEffects' => false], 'Collator::getAttribute' => ['hasSideEffects' => false], @@ -501,6 +503,9 @@ 'ReflectionClassConstant::isPrivate' => ['hasSideEffects' => false], 'ReflectionClassConstant::isProtected' => ['hasSideEffects' => false], 'ReflectionClassConstant::isPublic' => ['hasSideEffects' => false], + 'ReflectionEnumBackedCase::getBackingValue' => ['hasSideEffects' => false], + 'ReflectionEnumUnitCase::getEnum' => ['hasSideEffects' => false], + 'ReflectionEnumUnitCase::getValue' => ['hasSideEffects' => false], 'ReflectionExtension::getClassNames' => ['hasSideEffects' => false], 'ReflectionExtension::getClasses' => ['hasSideEffects' => false], 'ReflectionExtension::getConstants' => ['hasSideEffects' => false], @@ -514,6 +519,7 @@ 'ReflectionFunction::getClosure' => ['hasSideEffects' => false], 'ReflectionFunction::isDisabled' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::getAttributes' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::getClosureCalledClass' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::getClosureScopeClass' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::getClosureThis' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::getClosureUsedVariables' => ['hasSideEffects' => false], @@ -652,6 +658,12 @@ 'UConverter::getSubstChars' => ['hasSideEffects' => false], 'UConverter::reasonText' => ['hasSideEffects' => false], 'UnitEnum::cases' => ['hasSideEffects' => false], + 'WeakMap::count' => ['hasSideEffects' => false], + 'WeakMap::getIterator' => ['hasSideEffects' => false], + 'WeakMap::offsetExists' => ['hasSideEffects' => false], + 'WeakMap::offsetGet' => ['hasSideEffects' => false], + 'WeakReference::create' => ['hasSideEffects' => false], + 'WeakReference::get' => ['hasSideEffects' => false], 'XmlReader::next' => ['hasSideEffects' => true], 'XmlReader::read' => ['hasSideEffects' => true], 'Zookeeper::getAcl' => ['hasSideEffects' => false], @@ -695,12 +707,15 @@ 'array_merge' => ['hasSideEffects' => false], 'array_merge_recursive' => ['hasSideEffects' => false], 'array_pad' => ['hasSideEffects' => false], + 'array_pop' => ['hasSideEffects' => true], 'array_product' => ['hasSideEffects' => false], + 'array_push' => ['hasSideEffects' => true], 'array_rand' => ['hasSideEffects' => false], 'array_replace' => ['hasSideEffects' => false], 'array_replace_recursive' => ['hasSideEffects' => false], 'array_reverse' => ['hasSideEffects' => false], 'array_search' => ['hasSideEffects' => false], + 'array_shift' => ['hasSideEffects' => true], 'array_slice' => ['hasSideEffects' => false], 'array_sum' => ['hasSideEffects' => false], 'array_udiff' => ['hasSideEffects' => false], @@ -710,6 +725,7 @@ 'array_uintersect_assoc' => ['hasSideEffects' => false], 'array_uintersect_uassoc' => ['hasSideEffects' => false], 'array_unique' => ['hasSideEffects' => false], + 'array_unshift' => ['hasSideEffects' => true], 'array_values' => ['hasSideEffects' => false], 'asin' => ['hasSideEffects' => false], 'asinh' => ['hasSideEffects' => false], @@ -759,8 +775,8 @@ 'collator_get_sort_key' => ['hasSideEffects' => false], 'collator_get_strength' => ['hasSideEffects' => false], 'compact' => ['hasSideEffects' => false], - 'connection_aborted' => ['hasSideEffects' => false], - 'connection_status' => ['hasSideEffects' => false], + 'connection_aborted' => ['hasSideEffects' => true], + 'connection_status' => ['hasSideEffects' => true], 'constant' => ['hasSideEffects' => false], 'convert_cyr_string' => ['hasSideEffects' => false], 'convert_uudecode' => ['hasSideEffects' => false], @@ -845,6 +861,7 @@ 'dngettext' => ['hasSideEffects' => false], 'doubleval' => ['hasSideEffects' => false], 'error_get_last' => ['hasSideEffects' => false], + 'error_log' => ['hasSideEffects' => true], 'escapeshellarg' => ['hasSideEffects' => false], 'escapeshellcmd' => ['hasSideEffects' => false], 'exp' => ['hasSideEffects' => false], @@ -861,7 +878,7 @@ 'fgetss' => ['hasSideEffects' => true], 'file' => ['hasSideEffects' => false], 'file_exists' => ['hasSideEffects' => false], - 'file_get_contents' => ['hasSideEffects' => false], + 'file_get_contents' => ['hasSideEffects' => true], 'file_put_contents' => ['hasSideEffects' => true], 'fileatime' => ['hasSideEffects' => false], 'filectime' => ['hasSideEffects' => false], @@ -1224,6 +1241,7 @@ 'join' => ['hasSideEffects' => false], 'json_last_error' => ['hasSideEffects' => false], 'json_last_error_msg' => ['hasSideEffects' => false], + 'json_validate' => ['hasSideEffects' => false], 'key' => ['hasSideEffects' => false], 'key_exists' => ['hasSideEffects' => false], 'lcfirst' => ['hasSideEffects' => false], @@ -1287,6 +1305,7 @@ 'mb_preferred_mime_name' => ['hasSideEffects' => false], 'mb_scrub' => ['hasSideEffects' => false], 'mb_split' => ['hasSideEffects' => false], + 'mb_str_pad' => ['hasSideEffects' => false], 'mb_str_split' => ['hasSideEffects' => false], 'mb_strcut' => ['hasSideEffects' => false], 'mb_strimwidth' => ['hasSideEffects' => false], @@ -1357,6 +1376,8 @@ 'octdec' => ['hasSideEffects' => false], 'ord' => ['hasSideEffects' => false], 'pack' => ['hasSideEffects' => false], + 'pam_auth' => ['hasSideEffects' => false], + 'pam_chpass' => ['hasSideEffects' => false], 'parse_ini_file' => ['hasSideEffects' => false], 'parse_ini_string' => ['hasSideEffects' => false], 'parse_url' => ['hasSideEffects' => false], @@ -1455,8 +1476,10 @@ 'sqrt' => ['hasSideEffects' => false], 'stat' => ['hasSideEffects' => false], 'str_contains' => ['hasSideEffects' => false], + 'str_decrement' => ['hasSideEffects' => false], 'str_ends_with' => ['hasSideEffects' => false], 'str_getcsv' => ['hasSideEffects' => false], + 'str_increment' => ['hasSideEffects' => false], 'str_pad' => ['hasSideEffects' => false], 'str_repeat' => ['hasSideEffects' => false], 'str_rot13' => ['hasSideEffects' => false], diff --git a/src/Analyser/Analyser.php b/src/Analyser/Analyser.php index 3757c5a9cb..fe3545bdd5 100644 --- a/src/Analyser/Analyser.php +++ b/src/Analyser/Analyser.php @@ -10,6 +10,7 @@ use function array_fill_keys; use function array_merge; use function count; +use function memory_get_peak_usage; use function sprintf; class Analyser @@ -46,10 +47,20 @@ public function analyse( $this->nodeScopeResolver->setAnalysedFiles($allAnalysedFiles); $allAnalysedFiles = array_fill_keys($allAnalysedFiles, true); - /** @var Error[] $errors */ + /** @var list $errors */ $errors = []; + /** @var list $filteredPhpErrors */ + $filteredPhpErrors = []; + /** @var list $allPhpErrors */ + $allPhpErrors = []; - /** @var CollectedData[] $collectedData */ + /** @var list $locallyIgnoredErrors */ + $locallyIgnoredErrors = []; + + $linesToIgnore = []; + $unmatchedLineIgnores = []; + + /** @var list $collectedData */ $collectedData = []; $internalErrorsCount = 0; @@ -70,6 +81,12 @@ public function analyse( null, ); $errors = array_merge($errors, $fileAnalyserResult->getErrors()); + $filteredPhpErrors = array_merge($filteredPhpErrors, $fileAnalyserResult->getFilteredPhpErrors()); + $allPhpErrors = array_merge($allPhpErrors, $fileAnalyserResult->getAllPhpErrors()); + + $locallyIgnoredErrors = array_merge($locallyIgnoredErrors, $fileAnalyserResult->getLocallyIgnoredErrors()); + $linesToIgnore[$file] = $fileAnalyserResult->getLinesToIgnore(); + $unmatchedLineIgnores[$file] = $fileAnalyserResult->getUnmatchedLineIgnores(); $collectedData = array_merge($collectedData, $fileAnalyserResult->getCollectedData()); $dependencies[$file] = $fileAnalyserResult->getDependencies(); @@ -87,9 +104,9 @@ public function analyse( '%sRun PHPStan with --debug option and post the stack trace to:%s%s', "\n", "\n", - 'https://github.com/phpstan/phpstan/issues/new?template=Bug_report.md', + 'https://github.com/phpstan/phpstan/issues/new?template=Bug_report.yaml', ); - $errors[] = new Error($internalErrorMessage, $file, null, $t); + $errors[] = (new Error($internalErrorMessage, $file, null, $t))->withIdentifier('phpstan.internal'); if ($internalErrorsCount >= $this->internalErrorsCountLimit) { $reachedInternalErrorsCountLimit = true; break; @@ -105,11 +122,17 @@ public function analyse( return new AnalyserResult( $errors, + $filteredPhpErrors, + $allPhpErrors, + $locallyIgnoredErrors, + $linesToIgnore, + $unmatchedLineIgnores, [], $collectedData, $internalErrorsCount === 0 ? $dependencies : null, $exportedNodes, $reachedInternalErrorsCountLimit, + memory_get_peak_usage(true), ); } diff --git a/src/Analyser/AnalyserResult.php b/src/Analyser/AnalyserResult.php index 347ee1e150..56cafb327d 100644 --- a/src/Analyser/AnalyserResult.php +++ b/src/Analyser/AnalyserResult.php @@ -6,46 +6,46 @@ use PHPStan\Dependency\RootExportedNode; use function usort; +/** + * @phpstan-import-type LinesToIgnore from FileAnalyserResult + */ class AnalyserResult { - /** @var Error[] */ - private array $unorderedErrors; + /** @var list|null */ + private ?array $errors = null; /** - * @param Error[] $errors - * @param CollectedData[] $collectedData - * @param string[] $internalErrors + * @param list $unorderedErrors + * @param list $filteredPhpErrors + * @param list $allPhpErrors + * @param list $locallyIgnoredErrors + * @param array $linesToIgnore + * @param array $unmatchedLineIgnores + * @param list $collectedData + * @param list $internalErrors * @param array>|null $dependencies * @param array> $exportedNodes */ public function __construct( - private array $errors, + private array $unorderedErrors, + private array $filteredPhpErrors, + private array $allPhpErrors, + private array $locallyIgnoredErrors, + private array $linesToIgnore, + private array $unmatchedLineIgnores, private array $internalErrors, private array $collectedData, private ?array $dependencies, private array $exportedNodes, private bool $reachedInternalErrorsCountLimit, + private int $peakMemoryUsageBytes, ) { - $this->unorderedErrors = $errors; - - usort( - $this->errors, - static fn (Error $a, Error $b): int => [ - $a->getFile(), - $a->getLine(), - $a->getMessage(), - ] <=> [ - $b->getFile(), - $b->getLine(), - $b->getMessage(), - ], - ); } /** - * @return Error[] + * @return list */ public function getUnorderedErrors(): array { @@ -53,15 +53,71 @@ public function getUnorderedErrors(): array } /** - * @return Error[] + * @return list */ public function getErrors(): array { + if (!isset($this->errors)) { + $this->errors = $this->unorderedErrors; + usort( + $this->errors, + static fn (Error $a, Error $b): int => [ + $a->getFile(), + $a->getLine(), + $a->getMessage(), + ] <=> [ + $b->getFile(), + $b->getLine(), + $b->getMessage(), + ], + ); + } + return $this->errors; } /** - * @return string[] + * @return list + */ + public function getFilteredPhpErrors(): array + { + return $this->filteredPhpErrors; + } + + /** + * @return list + */ + public function getAllPhpErrors(): array + { + return $this->allPhpErrors; + } + + /** + * @return list + */ + public function getLocallyIgnoredErrors(): array + { + return $this->locallyIgnoredErrors; + } + + /** + * @return array + */ + public function getLinesToIgnore(): array + { + return $this->linesToIgnore; + } + + /** + * @return array + */ + public function getUnmatchedLineIgnores(): array + { + return $this->unmatchedLineIgnores; + } + + /** + * @return list */ public function getInternalErrors(): array { @@ -69,7 +125,7 @@ public function getInternalErrors(): array } /** - * @return CollectedData[] + * @return list */ public function getCollectedData(): array { @@ -97,4 +153,9 @@ public function hasReachedInternalErrorsCountLimit(): bool return $this->reachedInternalErrorsCountLimit; } + public function getPeakMemoryUsageBytes(): int + { + return $this->peakMemoryUsageBytes; + } + } diff --git a/src/Analyser/AnalyserResultFinalizer.php b/src/Analyser/AnalyserResultFinalizer.php new file mode 100644 index 0000000000..4f09cfbfa6 --- /dev/null +++ b/src/Analyser/AnalyserResultFinalizer.php @@ -0,0 +1,191 @@ +getCollectedData()) === 0) { + return $this->addUnmatchedIgnoredErrors($this->mergeFilteredPhpErrors($analyserResult), [], []); + } + + $hasInternalErrors = count($analyserResult->getInternalErrors()) > 0 || $analyserResult->hasReachedInternalErrorsCountLimit(); + if ($hasInternalErrors) { + return $this->addUnmatchedIgnoredErrors($this->mergeFilteredPhpErrors($analyserResult), [], []); + } + + $nodeType = CollectedDataNode::class; + $node = new CollectedDataNode($analyserResult->getCollectedData(), $onlyFiles); + + $file = 'N/A'; + $scope = $this->scopeFactory->create(ScopeContext::create($file)); + $tempCollectorErrors = []; + foreach ($this->ruleRegistry->getRules($nodeType) as $rule) { + try { + $ruleErrors = $rule->processNode($node, $scope); + } catch (AnalysedCodeException $e) { + $tempCollectorErrors[] = (new Error($e->getMessage(), $file, $node->getStartLine(), $e, null, null, $e->getTip()))->withIdentifier('phpstan.internal'); + continue; + } catch (IdentifierNotFound $e) { + $tempCollectorErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getStartLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols'))->withIdentifier('phpstan.reflection'); + continue; + } catch (UnableToCompileNode | CircularReference $e) { + $tempCollectorErrors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getStartLine(), $e))->withIdentifier('phpstan.reflection'); + continue; + } + + foreach ($ruleErrors as $ruleError) { + $tempCollectorErrors[] = $this->ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getStartLine()); + } + } + + $errors = $analyserResult->getUnorderedErrors(); + $locallyIgnoredErrors = $analyserResult->getLocallyIgnoredErrors(); + $allLinesToIgnore = $analyserResult->getLinesToIgnore(); + $allUnmatchedLineIgnores = $analyserResult->getUnmatchedLineIgnores(); + $collectorErrors = []; + $locallyIgnoredCollectorErrors = []; + foreach ($tempCollectorErrors as $tempCollectorError) { + $file = $tempCollectorError->getFilePath(); + $linesToIgnore = $allLinesToIgnore[$file] ?? []; + $unmatchedLineIgnores = $allUnmatchedLineIgnores[$file] ?? []; + $localIgnoresProcessorResult = $this->localIgnoresProcessor->process( + [$tempCollectorError], + $linesToIgnore, + $unmatchedLineIgnores, + ); + foreach ($localIgnoresProcessorResult->getFileErrors() as $error) { + $errors[] = $error; + $collectorErrors[] = $error; + } + foreach ($localIgnoresProcessorResult->getLocallyIgnoredErrors() as $locallyIgnoredError) { + $locallyIgnoredErrors[] = $locallyIgnoredError; + $locallyIgnoredCollectorErrors[] = $locallyIgnoredError; + } + $allLinesToIgnore[$file] = $localIgnoresProcessorResult->getLinesToIgnore(); + $allUnmatchedLineIgnores[$file] = $localIgnoresProcessorResult->getUnmatchedLineIgnores(); + } + + return $this->addUnmatchedIgnoredErrors(new AnalyserResult( + array_merge($errors, $analyserResult->getFilteredPhpErrors()), + [], + $analyserResult->getAllPhpErrors(), + $locallyIgnoredErrors, + $allLinesToIgnore, + $allUnmatchedLineIgnores, + $analyserResult->getInternalErrors(), + $analyserResult->getCollectedData(), + $analyserResult->getDependencies(), + $analyserResult->getExportedNodes(), + $analyserResult->hasReachedInternalErrorsCountLimit(), + $analyserResult->getPeakMemoryUsageBytes(), + ), $collectorErrors, $locallyIgnoredCollectorErrors); + } + + private function mergeFilteredPhpErrors(AnalyserResult $analyserResult): AnalyserResult + { + return new AnalyserResult( + array_merge($analyserResult->getUnorderedErrors(), $analyserResult->getFilteredPhpErrors()), + [], + $analyserResult->getAllPhpErrors(), + $analyserResult->getLocallyIgnoredErrors(), + $analyserResult->getLinesToIgnore(), + $analyserResult->getUnmatchedLineIgnores(), + $analyserResult->getInternalErrors(), + $analyserResult->getCollectedData(), + $analyserResult->getDependencies(), + $analyserResult->getExportedNodes(), + $analyserResult->hasReachedInternalErrorsCountLimit(), + $analyserResult->getPeakMemoryUsageBytes(), + ); + } + + /** + * @param list $collectorErrors + * @param list $locallyIgnoredCollectorErrors + */ + private function addUnmatchedIgnoredErrors( + AnalyserResult $analyserResult, + array $collectorErrors, + array $locallyIgnoredCollectorErrors, + ): FinalizerResult + { + if (!$this->reportUnmatchedIgnoredErrors) { + return new FinalizerResult($analyserResult, $collectorErrors, $locallyIgnoredCollectorErrors); + } + + $errors = $analyserResult->getUnorderedErrors(); + foreach ($analyserResult->getUnmatchedLineIgnores() as $file => $data) { + foreach ($data as $ignoredFile => $lines) { + if ($ignoredFile !== $file) { + continue; + } + + foreach ($lines as $line => $identifiers) { + if ($identifiers === null) { + $errors[] = (new Error( + sprintf('No error to ignore is reported on line %d.', $line), + $file, + $line, + false, + $file, + ))->withIdentifier('ignore.unmatchedLine'); + continue; + } + + foreach ($identifiers as $identifier) { + $errors[] = (new Error( + sprintf('No error with identifier %s is reported on line %d.', $identifier, $line), + $file, + $line, + false, + $file, + ))->withIdentifier('ignore.unmatchedIdentifier'); + } + } + } + } + + return new FinalizerResult( + new AnalyserResult( + $errors, + $analyserResult->getFilteredPhpErrors(), + $analyserResult->getAllPhpErrors(), + $analyserResult->getLocallyIgnoredErrors(), + $analyserResult->getLinesToIgnore(), + $analyserResult->getUnmatchedLineIgnores(), + $analyserResult->getInternalErrors(), + $analyserResult->getCollectedData(), + $analyserResult->getDependencies(), + $analyserResult->getExportedNodes(), + $analyserResult->hasReachedInternalErrorsCountLimit(), + $analyserResult->getPeakMemoryUsageBytes(), + ), + $collectorErrors, + $locallyIgnoredCollectorErrors, + ); + } + +} diff --git a/src/Analyser/ArgumentsNormalizer.php b/src/Analyser/ArgumentsNormalizer.php index eeab6bf83c..4da74a681e 100644 --- a/src/Analyser/ArgumentsNormalizer.php +++ b/src/Analyser/ArgumentsNormalizer.php @@ -10,6 +10,7 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantArrayType; use function array_key_exists; @@ -18,11 +19,67 @@ use function ksort; use function max; +/** + * @api + */ final class ArgumentsNormalizer { public const ORIGINAL_ARG_ATTRIBUTE = 'originalArg'; + /** + * @return array{ParametersAcceptor, FuncCall}|null + */ + public static function reorderCallUserFuncArguments( + FuncCall $callUserFuncCall, + Scope $scope, + ): ?array + { + $args = $callUserFuncCall->getArgs(); + if (count($args) < 1) { + return null; + } + + $passThruArgs = []; + $callbackArg = null; + foreach ($args as $i => $arg) { + if ($callbackArg === null) { + if ($arg->name === null && $i === 0) { + $callbackArg = $arg; + continue; + } + if ($arg->name !== null && $arg->name->toString() === 'callback') { + $callbackArg = $arg; + continue; + } + } + + $passThruArgs[] = $arg; + } + + if ($callbackArg === null) { + return null; + } + + $calledOnType = $scope->getType($callbackArg->value); + if (!$calledOnType->isCallable()->yes()) { + return null; + } + + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $passThruArgs, + $calledOnType->getCallableParametersAcceptors($scope), + null, + ); + + return [$parametersAcceptor, new FuncCall( + $callbackArg->value, + $passThruArgs, + $callUserFuncCall->getAttributes(), + )]; + } + public static function reorderFuncArguments( ParametersAcceptor $parametersAcceptor, FuncCall $functionCall, @@ -134,6 +191,7 @@ private static function reorderArgs(ParametersAcceptor $parametersAcceptor, Call $reorderedArgs = []; $additionalNamedArgs = []; + $appendArgs = []; foreach ($callArgs as $i => $arg) { if ($arg->name === null) { // add regular args as is @@ -152,7 +210,16 @@ private static function reorderArgs(ParametersAcceptor $parametersAcceptor, Call ); } else { if (!$hasVariadic) { - return null; + $attributes = $arg->getAttributes(); + $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg; + $appendArgs[] = new Arg( + $arg->value, + $arg->byRef, + $arg->unpack, + $attributes, + null, + ); + continue; } $attributes = $arg->getAttributes(); @@ -178,10 +245,13 @@ private static function reorderArgs(ParametersAcceptor $parametersAcceptor, Call } if (count($reorderedArgs) === 0) { - return []; + foreach ($appendArgs as $arg) { + $reorderedArgs[] = $arg; + } + return $reorderedArgs; } - // fill up all wholes with default values until the last given argument + // fill up all holes with default values until the last given argument for ($j = 0; $j < max(array_keys($reorderedArgs)); $j++) { if (array_key_exists($j, $reorderedArgs)) { continue; @@ -212,6 +282,10 @@ private static function reorderArgs(ParametersAcceptor $parametersAcceptor, Call ksort($reorderedArgs); + foreach ($appendArgs as $arg) { + $reorderedArgs[] = $arg; + } + return $reorderedArgs; } diff --git a/src/Analyser/ConditionalExpressionHolder.php b/src/Analyser/ConditionalExpressionHolder.php index b36e35bc26..561feac059 100644 --- a/src/Analyser/ConditionalExpressionHolder.php +++ b/src/Analyser/ConditionalExpressionHolder.php @@ -3,7 +3,6 @@ namespace PHPStan\Analyser; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function count; use function implode; @@ -13,27 +12,27 @@ class ConditionalExpressionHolder { /** - * @param array $conditionExpressionTypes + * @param array $conditionExpressionTypeHolders */ public function __construct( - private array $conditionExpressionTypes, - private VariableTypeHolder $typeHolder, + private array $conditionExpressionTypeHolders, + private ExpressionTypeHolder $typeHolder, ) { - if (count($conditionExpressionTypes) === 0) { + if (count($conditionExpressionTypeHolders) === 0) { throw new ShouldNotHappenException(); } } /** - * @return array + * @return array */ - public function getConditionExpressionTypes(): array + public function getConditionExpressionTypeHolders(): array { - return $this->conditionExpressionTypes; + return $this->conditionExpressionTypeHolders; } - public function getTypeHolder(): VariableTypeHolder + public function getTypeHolder(): ExpressionTypeHolder { return $this->typeHolder; } @@ -41,8 +40,8 @@ public function getTypeHolder(): VariableTypeHolder public function getKey(): string { $parts = []; - foreach ($this->conditionExpressionTypes as $exprString => $type) { - $parts[] = $exprString . '=' . $type->describe(VerbosityLevel::precise()); + foreach ($this->conditionExpressionTypeHolders as $exprString => $typeHolder) { + $parts[] = $exprString . '=' . $typeHolder->getType()->describe(VerbosityLevel::precise()); } return sprintf( diff --git a/src/Analyser/ConstantResolver.php b/src/Analyser/ConstantResolver.php index 9dc79e20d4..19cf21bbf2 100644 --- a/src/Analyser/ConstantResolver.php +++ b/src/Analyser/ConstantResolver.php @@ -10,7 +10,6 @@ use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\ConstantType; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; @@ -21,6 +20,7 @@ use PHPStan\Type\UnionType; use function array_key_exists; use function in_array; +use function sprintf; use const INF; use const NAN; use const PHP_INT_SIZE; @@ -293,13 +293,29 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type public function resolveConstantType(string $constantName, Type $constantType): Type { - if ($constantType instanceof ConstantType && in_array($constantName, $this->dynamicConstantNames, true)) { + if ($constantType->isConstantValue()->yes() && in_array($constantName, $this->dynamicConstantNames, true)) { return $constantType->generalize(GeneralizePrecision::lessSpecific()); } return $constantType; } + public function resolveClassConstantType(string $className, string $constantName, Type $constantType, ?Type $nativeType): Type + { + $lookupConstantName = sprintf('%s::%s', $className, $constantName); + if (in_array($lookupConstantName, $this->dynamicConstantNames, true)) { + if ($nativeType !== null) { + return $nativeType; + } + + if ($constantType->isConstantValue()->yes()) { + return $constantType->generalize(GeneralizePrecision::lessSpecific()); + } + } + + return $constantType; + } + private function getReflectionProvider(): ReflectionProvider { return $this->reflectionProviderProvider->getReflectionProvider(); diff --git a/src/Analyser/DirectScopeFactory.php b/src/Analyser/DirectInternalScopeFactory.php similarity index 78% rename from src/Analyser/DirectScopeFactory.php rename to src/Analyser/DirectInternalScopeFactory.php index eb1df50afa..07d7fbe09c 100644 --- a/src/Analyser/DirectScopeFactory.php +++ b/src/Analyser/DirectInternalScopeFactory.php @@ -3,23 +3,21 @@ namespace PHPStan\Analyser; use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider; +use PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Parser\Parser; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\Type; use function is_a; -/** - * @internal - */ -class DirectScopeFactory implements ScopeFactory +class DirectInternalScopeFactory implements InternalScopeFactory { /** @@ -30,12 +28,12 @@ public function __construct( private ReflectionProvider $reflectionProvider, private InitializerExprTypeResolver $initializerExprTypeResolver, private DynamicReturnTypeExtensionRegistryProvider $dynamicReturnTypeExtensionRegistryProvider, + private ExpressionTypeResolverExtensionRegistryProvider $expressionTypeResolverExtensionRegistryProvider, private ExprPrinter $exprPrinter, private TypeSpecifier $typeSpecifier, private PropertyReflectionFinder $propertyReflectionFinder, private Parser $parser, private NodeScopeResolver $nodeScopeResolver, - private bool $treatPhpDocTypesAsCertain, private PhpVersion $phpVersion, private bool $explicitMixedInUnknownGenericNew, private bool $explicitMixedForGlobalVariables, @@ -45,34 +43,30 @@ public function __construct( } /** - * @param array $constantTypes - * @param VariableTypeHolder[] $variablesTypes - * @param VariableTypeHolder[] $moreSpecificTypes + * @param array $expressionTypes + * @param array $nativeExpressionTypes * @param array $conditionalExpressions + * @param list $inFunctionCallsStack * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions - * @param array $nativeExpressionTypes - * @param array<(FunctionReflection|MethodReflection)> $inFunctionCallsStack - * */ public function create( ScopeContext $context, bool $declareStrictTypes = false, - array $constantTypes = [], FunctionReflection|MethodReflection|null $function = null, ?string $namespace = null, - array $variablesTypes = [], - array $moreSpecificTypes = [], + array $expressionTypes = [], + array $nativeExpressionTypes = [], array $conditionalExpressions = [], - ?string $inClosureBindScopeClass = null, + array $inClosureBindScopeClasses = [], ?ParametersAcceptor $anonymousFunctionReflection = null, bool $inFirstLevelStatement = true, array $currentlyAssignedExpressions = [], array $currentlyAllowedUndefinedExpressions = [], - array $nativeExpressionTypes = [], array $inFunctionCallsStack = [], bool $afterExtractCall = false, ?Scope $parentScope = null, + bool $nativeTypesPromoted = false, ): MutatingScope { $scopeClass = $this->scopeClass; @@ -85,6 +79,7 @@ public function create( $this->reflectionProvider, $this->initializerExprTypeResolver, $this->dynamicReturnTypeExtensionRegistryProvider->getRegistry(), + $this->expressionTypeResolverExtensionRegistryProvider->getRegistry(), $this->exprPrinter, $this->typeSpecifier, $this->propertyReflectionFinder, @@ -94,22 +89,20 @@ public function create( $context, $this->phpVersion, $declareStrictTypes, - $constantTypes, $function, $namespace, - $variablesTypes, - $moreSpecificTypes, + $expressionTypes, + $nativeExpressionTypes, $conditionalExpressions, - $inClosureBindScopeClass, + $inClosureBindScopeClasses, $anonymousFunctionReflection, $inFirstLevelStatement, $currentlyAssignedExpressions, $currentlyAllowedUndefinedExpressions, - $nativeExpressionTypes, $inFunctionCallsStack, - $this->treatPhpDocTypesAsCertain, $afterExtractCall, $parentScope, + $nativeTypesPromoted, $this->explicitMixedInUnknownGenericNew, $this->explicitMixedForGlobalVariables, ); diff --git a/src/Analyser/EnsuredNonNullabilityResultExpression.php b/src/Analyser/EnsuredNonNullabilityResultExpression.php index a7ed1572f8..33f94341e6 100644 --- a/src/Analyser/EnsuredNonNullabilityResultExpression.php +++ b/src/Analyser/EnsuredNonNullabilityResultExpression.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser; use PhpParser\Node\Expr; +use PHPStan\TrinaryLogic; use PHPStan\Type\Type; class EnsuredNonNullabilityResultExpression @@ -12,6 +13,7 @@ public function __construct( private Expr $expression, private Type $originalType, private Type $originalNativeType, + private TrinaryLogic $certainty, ) { } @@ -31,4 +33,9 @@ public function getOriginalNativeType(): Type return $this->originalNativeType; } + public function getCertainty(): TrinaryLogic + { + return $this->certainty; + } + } diff --git a/src/Analyser/Error.php b/src/Analyser/Error.php index f36ba89284..35b567fbca 100644 --- a/src/Analyser/Error.php +++ b/src/Analyser/Error.php @@ -4,16 +4,20 @@ use Exception; use JsonSerializable; +use Nette\Utils\Strings; use PhpParser\Node; use PHPStan\ShouldNotHappenException; use ReturnTypeWillChange; use Throwable; use function is_bool; +use function sprintf; /** @api */ class Error implements JsonSerializable { + public const PATTERN_IDENTIFIER = '[a-zA-Z0-9](?:[a-zA-Z0-9\\.]*[a-zA-Z0-9])?'; + /** * Error constructor. * @@ -34,6 +38,9 @@ public function __construct( private array $metadata = [], ) { + if ($this->identifier !== null && !self::validateIdentifier($this->identifier)) { + throw new ShouldNotHappenException(sprintf('Invalid identifier: %s', $this->identifier)); + } } public function getMessage(): string @@ -156,6 +163,27 @@ public function doNotIgnore(): self ); } + public function withIdentifier(string $identifier): self + { + if ($this->identifier !== null) { + throw new ShouldNotHappenException(sprintf('Error already has an identifier: %s', $this->identifier)); + } + + return new self( + $this->message, + $this->file, + $this->line, + $this->canBeIgnored, + $this->filePath, + $this->traitFilePath, + $this->tip, + $this->nodeLine, + $this->nodeType, + $identifier, + $this->metadata, + ); + } + public function getNodeLine(): ?int { return $this->nodeLine; @@ -169,6 +197,11 @@ public function getNodeType(): ?string return $this->nodeType; } + /** + * Error identifier set via `RuleErrorBuilder::identifier()`. + * + * List of all current error identifiers in PHPStan: https://phpstan.org/error-identifiers + */ public function getIdentifier(): ?string { return $this->identifier; @@ -243,4 +276,9 @@ public static function __set_state(array $properties): self ); } + public static function validateIdentifier(string $identifier): bool + { + return Strings::match($identifier, '~^' . self::PATTERN_IDENTIFIER . '$~') !== null; + } + } diff --git a/src/Analyser/ExpressionContext.php b/src/Analyser/ExpressionContext.php index df1f55d284..c2a5a6c78a 100644 --- a/src/Analyser/ExpressionContext.php +++ b/src/Analyser/ExpressionContext.php @@ -11,18 +11,19 @@ private function __construct( private bool $isDeep, private ?string $inAssignRightSideVariableName, private ?Type $inAssignRightSideType, + private ?Type $inAssignRightSideNativeType, ) { } public static function createTopLevel(): self { - return new self(false, null, null); + return new self(false, null, null, null); } public static function createDeep(): self { - return new self(true, null, null); + return new self(true, null, null, null); } public function enterDeep(): self @@ -31,7 +32,7 @@ public function enterDeep(): self return $this; } - return new self(true, $this->inAssignRightSideVariableName, $this->inAssignRightSideType); + return new self(true, $this->inAssignRightSideVariableName, $this->inAssignRightSideType, $this->inAssignRightSideNativeType); } public function isDeep(): bool @@ -39,9 +40,9 @@ public function isDeep(): bool return $this->isDeep; } - public function enterRightSideAssign(string $variableName, Type $type): self + public function enterRightSideAssign(string $variableName, Type $type, Type $nativeType): self { - return new self($this->isDeep, $variableName, $type); + return new self($this->isDeep, $variableName, $type, $nativeType); } public function getInAssignRightSideVariableName(): ?string @@ -54,4 +55,9 @@ public function getInAssignRightSideType(): ?Type return $this->inAssignRightSideType; } + public function getInAssignRightSideNativeType(): ?Type + { + return $this->inAssignRightSideNativeType; + } + } diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 93a3bdca25..cae75b1609 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -17,6 +17,7 @@ class ExpressionResult /** * @param ThrowPoint[] $throwPoints + * @param ImpurePoint[] $impurePoints * @param (callable(): MutatingScope)|null $truthyScopeCallback * @param (callable(): MutatingScope)|null $falseyScopeCallback */ @@ -24,6 +25,7 @@ public function __construct( private MutatingScope $scope, private bool $hasYield, private array $throwPoints, + private array $impurePoints, ?callable $truthyScopeCallback = null, ?callable $falseyScopeCallback = null, ) @@ -50,6 +52,14 @@ public function getThrowPoints(): array return $this->throwPoints; } + /** + * @return ImpurePoint[] + */ + public function getImpurePoints(): array + { + return $this->impurePoints; + } + public function getTruthyScope(): MutatingScope { if ($this->truthyScopeCallback === null) { diff --git a/src/Analyser/VariableTypeHolder.php b/src/Analyser/ExpressionTypeHolder.php similarity index 61% rename from src/Analyser/VariableTypeHolder.php rename to src/Analyser/ExpressionTypeHolder.php index 70273de965..6211e211c8 100644 --- a/src/Analyser/VariableTypeHolder.php +++ b/src/Analyser/ExpressionTypeHolder.php @@ -2,25 +2,26 @@ namespace PHPStan\Analyser; +use PhpParser\Node\Expr; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class VariableTypeHolder +class ExpressionTypeHolder { - public function __construct(private Type $type, private TrinaryLogic $certainty) + public function __construct(private Expr $expr, private Type $type, private TrinaryLogic $certainty) { } - public static function createYes(Type $type): self + public static function createYes(Expr $expr, Type $type): self { - return new self($type, TrinaryLogic::createYes()); + return new self($expr, $type, TrinaryLogic::createYes()); } - public static function createMaybe(Type $type): self + public static function createMaybe(Expr $expr, Type $type): self { - return new self($type, TrinaryLogic::createMaybe()); + return new self($expr, $type, TrinaryLogic::createMaybe()); } public function equals(self $other): bool @@ -40,11 +41,17 @@ public function and(self $other): self $type = TypeCombinator::union($this->getType(), $other->getType()); } return new self( + $this->expr, $type, $this->getCertainty()->and($other->getCertainty()), ); } + public function getExpr(): Expr + { + return $this->expr; + } + public function getType(): Type { return $this->type; diff --git a/src/Analyser/FileAnalyser.php b/src/Analyser/FileAnalyser.php index 75a0c06e15..0e6c861d94 100644 --- a/src/Analyser/FileAnalyser.php +++ b/src/Analyser/FileAnalyser.php @@ -2,26 +2,23 @@ namespace PHPStan\Analyser; -use PhpParser\Comment; use PhpParser\Node; use PHPStan\AnalysedCodeException; use PHPStan\BetterReflection\NodeCompiler\Exception\UnableToCompileNode; use PHPStan\BetterReflection\Reflection\Exception\CircularReference; -use PHPStan\BetterReflection\Reflection\Exception\NotAClassReflection; -use PHPStan\BetterReflection\Reflection\Exception\NotAnInterfaceReflection; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; use PHPStan\Collectors\CollectedData; use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; use PHPStan\Node\FileNode; +use PHPStan\Node\InTraitNode; use PHPStan\Parser\Parser; use PHPStan\Parser\ParserErrorsException; use PHPStan\Rules\Registry as RuleRegistry; -use function array_key_exists; use function array_keys; -use function array_merge; use function array_unique; use function array_values; +use function count; use function error_reporting; use function get_class; use function is_dir; @@ -29,14 +26,24 @@ use function restore_error_handler; use function set_error_handler; use function sprintf; -use function strpos; use const E_DEPRECATED; +use const E_ERROR; +use const E_NOTICE; +use const E_PARSE; +use const E_STRICT; +use const E_USER_ERROR; +use const E_USER_NOTICE; +use const E_USER_WARNING; +use const E_WARNING; class FileAnalyser { - /** @var Error[] */ - private array $collectedErrors = []; + /** @var list */ + private array $allPhpErrors = []; + + /** @var list */ + private array $filteredPhpErrors = []; public function __construct( private ScopeFactory $scopeFactory, @@ -44,7 +51,7 @@ public function __construct( private Parser $parser, private DependencyResolver $dependencyResolver, private RuleErrorTransformer $ruleErrorTransformer, - private bool $reportUnmatchedIgnoredErrors, + private LocalIgnoresProcessor $localIgnoresProcessor, ) { } @@ -61,30 +68,39 @@ public function analyseFile( ?callable $outerNodeCallback, ): FileAnalyserResult { - /** @var Error[] $fileErrors */ + /** @var list $fileErrors */ $fileErrors = []; - /** @var CollectedData[] $fileCollectedData */ + /** @var list $locallyIgnoredErrors */ + $locallyIgnoredErrors = []; + + /** @var list $fileCollectedData */ $fileCollectedData = []; $fileDependencies = []; $exportedNodes = []; + $linesToIgnore = []; + $unmatchedLineIgnores = []; if (is_file($file)) { try { $this->collectErrors($analysedFiles); $parserNodes = $this->parser->parseFile($file); - $linesToIgnore = $this->getLinesToIgnoreFromTokens($file, $parserNodes); + $linesToIgnore = $unmatchedLineIgnores = [$file => $this->getLinesToIgnoreFromTokens($parserNodes)]; $temporaryFileErrors = []; - $nodeCallback = function (Node $node, Scope $scope) use (&$fileErrors, &$fileCollectedData, &$fileDependencies, &$exportedNodes, $file, $ruleRegistry, $collectorRegistry, $outerNodeCallback, $analysedFiles, &$linesToIgnore, &$temporaryFileErrors): void { + $nodeCallback = function (Node $node, Scope $scope) use (&$fileErrors, &$fileCollectedData, &$fileDependencies, &$exportedNodes, $file, $ruleRegistry, $collectorRegistry, $outerNodeCallback, $analysedFiles, &$linesToIgnore, &$unmatchedLineIgnores, &$temporaryFileErrors): void { if ($node instanceof Node\Stmt\Trait_) { foreach (array_keys($linesToIgnore[$file] ?? []) as $lineToIgnore) { if ($lineToIgnore < $node->getStartLine() || $lineToIgnore > $node->getEndLine()) { continue; } - unset($linesToIgnore[$file][$lineToIgnore]); + unset($unmatchedLineIgnores[$file][$lineToIgnore]); } } + if ($node instanceof InTraitNode) { + $traitNode = $node->getOriginalNode(); + $linesToIgnore[$scope->getFileDescription()] = $this->getLinesToIgnoreFromTokens([$traitNode]); + } if ($outerNodeCallback !== null) { $outerNodeCallback($node, $scope); } @@ -99,30 +115,18 @@ public function analyseFile( } $uniquedAnalysedCodeExceptionMessages[$e->getMessage()] = true; - $fileErrors[] = new Error($e->getMessage(), $file, $node->getLine(), $e, null, null, $e->getTip()); + $fileErrors[] = (new Error($e->getMessage(), $file, $node->getStartLine(), $e, null, null, $e->getTip()))->withIdentifier('phpstan.internal'); continue; } catch (IdentifierNotFound $e) { - $fileErrors[] = new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols'); + $fileErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getStartLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols'))->withIdentifier('phpstan.reflection'); continue; - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection | CircularReference $e) { - $fileErrors[] = new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getLine(), $e); + } catch (UnableToCompileNode | CircularReference $e) { + $fileErrors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getStartLine(), $e))->withIdentifier('phpstan.reflection'); continue; } foreach ($ruleErrors as $ruleError) { - $temporaryFileErrors[] = $this->ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getLine()); - } - } - - if ($scope->isInTrait()) { - $sameTraitFile = $file === $scope->getTraitReflection()->getFileName(); - foreach ($this->getLinesToIgnore($node) as $lineToIgnore) { - $linesToIgnore[$scope->getFileDescription()][$lineToIgnore] = true; - if (!$sameTraitFile) { - continue; - } - - unset($linesToIgnore[$file][$lineToIgnore]); + $temporaryFileErrors[] = $this->ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getStartLine()); } } @@ -135,13 +139,13 @@ public function analyseFile( } $uniquedAnalysedCodeExceptionMessages[$e->getMessage()] = true; - $fileErrors[] = new Error($e->getMessage(), $file, $node->getLine(), $e, null, null, $e->getTip()); + $fileErrors[] = (new Error($e->getMessage(), $file, $node->getStartLine(), $e, null, null, $e->getTip()))->withIdentifier('phpstan.internal'); continue; } catch (IdentifierNotFound $e) { - $fileErrors[] = new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols'); + $fileErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getStartLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols'))->withIdentifier('phpstan.reflection'); continue; - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection | CircularReference $e) { - $fileErrors[] = new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getLine(), $e); + } catch (UnableToCompileNode | CircularReference $e) { + $fileErrors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getStartLine(), $e))->withIdentifier('phpstan.reflection'); continue; } @@ -168,7 +172,7 @@ public function analyseFile( // pass } catch (IdentifierNotFound) { // pass - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection) { + } catch (UnableToCompileNode) { // pass } }; @@ -180,136 +184,82 @@ public function analyseFile( $scope, $nodeCallback, ); - $unmatchedLineIgnores = $linesToIgnore; - foreach ($temporaryFileErrors as $tmpFileError) { - $line = $tmpFileError->getLine(); - if ( - $line !== null - && $tmpFileError->canBeIgnored() - && array_key_exists($tmpFileError->getFile(), $linesToIgnore) - && array_key_exists($line, $linesToIgnore[$tmpFileError->getFile()]) - ) { - unset($unmatchedLineIgnores[$tmpFileError->getFile()][$line]); - continue; - } - $fileErrors[] = $tmpFileError; + $localIgnoresProcessorResult = $this->localIgnoresProcessor->process( + $temporaryFileErrors, + $linesToIgnore, + $unmatchedLineIgnores, + ); + foreach ($localIgnoresProcessorResult->getFileErrors() as $fileError) { + $fileErrors[] = $fileError; } - - if ($this->reportUnmatchedIgnoredErrors) { - foreach ($unmatchedLineIgnores as $ignoredFile => $lines) { - if ($ignoredFile !== $file) { - continue; - } - - foreach (array_keys($lines) as $line) { - $fileErrors[] = new Error( - sprintf('No error to ignore is reported on line %d.', $line), - $scope->getFileDescription(), - $line, - false, - $scope->getFile(), - null, - null, - null, - null, - 'ignoredError.unmatchedOnLine', - ); - } - } + foreach ($localIgnoresProcessorResult->getLocallyIgnoredErrors() as $locallyIgnoredError) { + $locallyIgnoredErrors[] = $locallyIgnoredError; } + $linesToIgnore = $localIgnoresProcessorResult->getLinesToIgnore(); + $unmatchedLineIgnores = $localIgnoresProcessorResult->getUnmatchedLineIgnores(); } catch (\PhpParser\Error $e) { - $fileErrors[] = new Error($e->getMessage(), $file, $e->getStartLine() !== -1 ? $e->getStartLine() : null, $e); + $fileErrors[] = (new Error($e->getRawMessage(), $file, $e->getStartLine() !== -1 ? $e->getStartLine() : null, $e))->withIdentifier('phpstan.parse'); } catch (ParserErrorsException $e) { foreach ($e->getErrors() as $error) { - $fileErrors[] = new Error($error->getMessage(), $e->getParsedFile() ?? $file, $error->getStartLine() !== -1 ? $error->getStartLine() : null, $e); + $fileErrors[] = (new Error($error->getMessage(), $e->getParsedFile() ?? $file, $error->getLine() !== -1 ? $error->getStartLine() : null, $e))->withIdentifier('phpstan.parse'); } } catch (AnalysedCodeException $e) { - $fileErrors[] = new Error($e->getMessage(), $file, null, $e, null, null, $e->getTip()); + $fileErrors[] = (new Error($e->getMessage(), $file, null, $e, null, null, $e->getTip()))->withIdentifier('phpstan.internal'); } catch (IdentifierNotFound $e) { - $fileErrors[] = new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, null, $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols'); - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection | CircularReference $e) { - $fileErrors[] = new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, null, $e); + $fileErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, null, $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols'))->withIdentifier('phpstan.reflection'); + } catch (UnableToCompileNode | CircularReference $e) { + $fileErrors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, null, $e))->withIdentifier('phpstan.reflection'); } } elseif (is_dir($file)) { - $fileErrors[] = new Error(sprintf('File %s is a directory.', $file), $file, null, false); + $fileErrors[] = (new Error(sprintf('File %s is a directory.', $file), $file, null, false))->withIdentifier('phpstan.path'); } else { - $fileErrors[] = new Error(sprintf('File %s does not exist.', $file), $file, null, false); + $fileErrors[] = (new Error(sprintf('File %s does not exist.', $file), $file, null, false))->withIdentifier('phpstan.path'); } $this->restoreCollectErrorsHandler(); - $fileErrors = array_merge($fileErrors, $this->collectedErrors); - - return new FileAnalyserResult($fileErrors, $fileCollectedData, array_values(array_unique($fileDependencies)), $exportedNodes); - } - - /** - * @return int[] - */ - private function getLinesToIgnore(Node $node): array - { - $lines = []; - if ($node->getDocComment() !== null) { - $line = $this->findLineToIgnoreComment($node->getDocComment()); - if ($line !== null) { - $lines[] = $line; + foreach ($linesToIgnore as $fileKey => $lines) { + if (count($lines) > 0) { + continue; } + + unset($linesToIgnore[$fileKey]); } - foreach ($node->getComments() as $comment) { - $line = $this->findLineToIgnoreComment($comment); - if ($line === null) { + foreach ($unmatchedLineIgnores as $fileKey => $lines) { + if (count($lines) > 0) { continue; } - $lines[] = $line; + unset($unmatchedLineIgnores[$fileKey]); } - return $lines; + return new FileAnalyserResult( + $fileErrors, + $this->filteredPhpErrors, + $this->allPhpErrors, + $locallyIgnoredErrors, + $fileCollectedData, + array_values(array_unique($fileDependencies)), + $exportedNodes, + $linesToIgnore, + $unmatchedLineIgnores, + ); } /** * @param Node[] $nodes - * @return array> + * @return array|null> */ - private function getLinesToIgnoreFromTokens(string $file, array $nodes): array + private function getLinesToIgnoreFromTokens(array $nodes): array { if (!isset($nodes[0])) { return []; } - /** @var int[] $tokenLines */ - $tokenLines = $nodes[0]->getAttribute('linesToIgnore', []); - $lines = []; - foreach ($tokenLines as $tokenLine) { - $lines[$file][$tokenLine] = true; - } - - return $lines; - } - - private function findLineToIgnoreComment(Comment $comment): ?int - { - $text = $comment->getText(); - if ($comment instanceof Comment\Doc) { - $line = $comment->getEndLine(); - } else { - if (strpos($text, "\n") === false || strpos($text, '//') === 0) { - $line = $comment->getStartLine(); - } else { - $line = $comment->getEndLine(); - } - } - if (strpos($text, '@phpstan-ignore-next-line') !== false) { - return $line + 1; - } - - if (strpos($text, '@phpstan-ignore-line') !== false) { - return $line; - } - - return null; + /** @var array|null> */ + return $nodes[0]->getAttribute('linesToIgnore', []); } /** @@ -317,13 +267,18 @@ private function findLineToIgnoreComment(Comment $comment): ?int */ private function collectErrors(array $analysedFiles): void { - $this->collectedErrors = []; + $this->filteredPhpErrors = []; + $this->allPhpErrors = []; set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use ($analysedFiles): bool { if ((error_reporting() & $errno) === 0) { // silence @ operator return true; } + $errorMessage = sprintf('%s: %s', $this->getErrorLabel($errno), $errstr); + + $this->allPhpErrors[] = (new Error($errorMessage, $errfile, $errline, true))->withIdentifier('phpstan.php'); + if ($errno === E_DEPRECATED) { return true; } @@ -332,7 +287,7 @@ private function collectErrors(array $analysedFiles): void return true; } - $this->collectedErrors[] = new Error($errstr, $errfile, $errline, true); + $this->filteredPhpErrors[] = (new Error($errorMessage, $errfile, $errline, true))->withIdentifier('phpstan.php'); return true; }); @@ -343,4 +298,28 @@ private function restoreCollectErrorsHandler(): void restore_error_handler(); } + private function getErrorLabel(int $errno): string + { + switch ($errno) { + case E_ERROR: + return 'Fatal error'; + case E_WARNING: + return 'Warning'; + case E_PARSE: + return 'Parse error'; + case E_NOTICE: + return 'Notice'; + case E_USER_ERROR: + return 'User error (E_USER_ERROR)'; + case E_USER_WARNING: + return 'User warning (E_USER_WARNING)'; + case E_USER_NOTICE: + return 'User notice (E_USER_NOTICE)'; + case E_STRICT: + return 'Strict error (E_STRICT)'; + } + + return 'Unknown PHP error'; + } + } diff --git a/src/Analyser/FileAnalyserResult.php b/src/Analyser/FileAnalyserResult.php index d585faa803..23e34a9278 100644 --- a/src/Analyser/FileAnalyserResult.php +++ b/src/Analyser/FileAnalyserResult.php @@ -5,26 +5,39 @@ use PHPStan\Collectors\CollectedData; use PHPStan\Dependency\RootExportedNode; +/** + * @phpstan-type LinesToIgnore = array|null>> + */ class FileAnalyserResult { /** - * @param Error[] $errors - * @param CollectedData[] $collectedData - * @param array $dependencies - * @param array $exportedNodes + * @param list $errors + * @param list $filteredPhpErrors + * @param list $allPhpErrors + * @param list $locallyIgnoredErrors + * @param list $collectedData + * @param list $dependencies + * @param list $exportedNodes + * @param LinesToIgnore $linesToIgnore + * @param LinesToIgnore $unmatchedLineIgnores */ public function __construct( private array $errors, + private array $filteredPhpErrors, + private array $allPhpErrors, + private array $locallyIgnoredErrors, private array $collectedData, private array $dependencies, private array $exportedNodes, + private array $linesToIgnore, + private array $unmatchedLineIgnores, ) { } /** - * @return Error[] + * @return list */ public function getErrors(): array { @@ -32,7 +45,31 @@ public function getErrors(): array } /** - * @return CollectedData[] + * @return list + */ + public function getFilteredPhpErrors(): array + { + return $this->filteredPhpErrors; + } + + /** + * @return list + */ + public function getAllPhpErrors(): array + { + return $this->allPhpErrors; + } + + /** + * @return list + */ + public function getLocallyIgnoredErrors(): array + { + return $this->locallyIgnoredErrors; + } + + /** + * @return list */ public function getCollectedData(): array { @@ -40,7 +77,7 @@ public function getCollectedData(): array } /** - * @return array + * @return list */ public function getDependencies(): array { @@ -48,11 +85,27 @@ public function getDependencies(): array } /** - * @return array + * @return list */ public function getExportedNodes(): array { return $this->exportedNodes; } + /** + * @return LinesToIgnore + */ + public function getLinesToIgnore(): array + { + return $this->linesToIgnore; + } + + /** + * @return LinesToIgnore + */ + public function getUnmatchedLineIgnores(): array + { + return $this->unmatchedLineIgnores; + } + } diff --git a/src/Analyser/FinalizerResult.php b/src/Analyser/FinalizerResult.php new file mode 100644 index 0000000000..dfc7761743 --- /dev/null +++ b/src/Analyser/FinalizerResult.php @@ -0,0 +1,49 @@ + $collectorErrors + * @param list $locallyIgnoredCollectorErrors + */ + public function __construct( + private AnalyserResult $analyserResult, + private array $collectorErrors, + private array $locallyIgnoredCollectorErrors, + ) + { + } + + /** + * @return list + */ + public function getErrors(): array + { + return $this->analyserResult->getErrors(); + } + + public function getAnalyserResult(): AnalyserResult + { + return $this->analyserResult; + } + + /** + * @return list + */ + public function getCollectorErrors(): array + { + return $this->collectorErrors; + } + + /** + * @return list + */ + public function getLocallyIgnoredCollectorErrors(): array + { + return $this->locallyIgnoredCollectorErrors; + } + +} diff --git a/src/Analyser/Ignore/IgnoreLexer.php b/src/Analyser/Ignore/IgnoreLexer.php new file mode 100644 index 0000000000..baa0cf7af4 --- /dev/null +++ b/src/Analyser/Ignore/IgnoreLexer.php @@ -0,0 +1,93 @@ + 'T_WHITESPACE', + self::TOKEN_EOL => 'T_EOL', + self::TOKEN_IDENTIFIER => 'T_IDENTIFIER', + self::TOKEN_COMMA => 'T_COMMA', + self::TOKEN_OPEN_PARENTHESIS => 'T_OPEN_PARENTHESIS', + self::TOKEN_CLOSE_PARENTHESIS => 'T_CLOSE_PARENTHESIS', + self::TOKEN_OTHER => 'T_OTHER', + ]; + + public const VALUE_OFFSET = 0; + public const TYPE_OFFSET = 1; + public const LINE_OFFSET = 2; + + private ?string $regexp = null; + + /** + * @return list + */ + public function tokenize(string $input): array + { + if ($this->regexp === null) { + $this->regexp = $this->generateRegexp(); + } + + $matches = Strings::matchAll($input, $this->regexp, PREG_SET_ORDER); + + $tokens = []; + $line = 1; + foreach ($matches as $match) { + /** @var self::TOKEN_* $type */ + $type = (int) $match['MARK']; + $tokens[] = [$match[0], $type, $line]; + if ($type !== self::TOKEN_EOL) { + continue; + } + + $line++; + } + + return $tokens; + } + + /** + * @param self::TOKEN_* $type + */ + public function getLabel(int $type): string + { + return self::LABELS[$type]; + } + + private function generateRegexp(): string + { + $patterns = [ + self::TOKEN_WHITESPACE => '[\\x09\\x20]++', + self::TOKEN_EOL => '\\r?+\\n[\\x09\\x20]*+(?:\\*(?!/)\\x20?+)?', + self::TOKEN_IDENTIFIER => Error::PATTERN_IDENTIFIER, + self::TOKEN_COMMA => ',', + self::TOKEN_OPEN_PARENTHESIS => '\\(', + self::TOKEN_CLOSE_PARENTHESIS => '\\)', + + // everything except whitespaces, TOKEN_CLOSE_PARENTHESIS + self::TOKEN_OTHER => '(?:(?!\\))[^\\s])++', + ]; + + foreach ($patterns as $type => &$pattern) { + $pattern = '(?:' . $pattern . ')(*MARK:' . $type . ')'; + } + + return '~' . implode('|', $patterns) . '~Asi'; + } + +} diff --git a/src/Analyser/Ignore/IgnoreParseException.php b/src/Analyser/Ignore/IgnoreParseException.php new file mode 100644 index 0000000000..fc8eec134c --- /dev/null +++ b/src/Analyser/Ignore/IgnoreParseException.php @@ -0,0 +1,20 @@ +phpDocLine; + } + +} diff --git a/src/Analyser/Ignore/IgnoredError.php b/src/Analyser/Ignore/IgnoredError.php new file mode 100644 index 0000000000..00cc1e7df8 --- /dev/null +++ b/src/Analyser/Ignore/IgnoredError.php @@ -0,0 +1,100 @@ +getIdentifier() !== $identifier) { + return false; + } + } + + if ($ignoredErrorPattern !== null) { + // normalize newlines to allow working with ignore-patterns independent of used OS newline-format + $errorMessage = $error->getMessage(); + $errorMessage = str_replace(['\r\n', '\r'], '\n', $errorMessage); + $ignoredErrorPattern = str_replace([preg_quote('\r\n'), preg_quote('\r')], preg_quote('\n'), $ignoredErrorPattern); + if (Strings::match($errorMessage, $ignoredErrorPattern) === null) { + return false; + } + } + + if ($path !== null) { + $fileExcluder = new FileExcluder($fileHelper, [$path]); + $isExcluded = $fileExcluder->isExcludedFromAnalysing($error->getFilePath()); + if (!$isExcluded && $error->getTraitFilePath() !== null) { + return $fileExcluder->isExcludedFromAnalysing($error->getTraitFilePath()); + } + + return $isExcluded; + } + + return true; + } + +} diff --git a/src/Analyser/IgnoredErrorHelper.php b/src/Analyser/Ignore/IgnoredErrorHelper.php similarity index 56% rename from src/Analyser/IgnoredErrorHelper.php rename to src/Analyser/Ignore/IgnoredErrorHelper.php index c25b0e495a..068114767c 100644 --- a/src/Analyser/IgnoredErrorHelper.php +++ b/src/Analyser/Ignore/IgnoredErrorHelper.php @@ -1,10 +1,13 @@ ignoreErrors as $ignoreError) { if (is_array($ignoreError)) { - if (!isset($ignoreError['message']) && !isset($ignoreError['messages'])) { + if (!isset($ignoreError['message']) && !isset($ignoreError['messages']) && !isset($ignoreError['identifier'])) { $errors[] = sprintf( - 'Ignored error %s is missing a message.', + 'Ignored error %s is missing a message or an identifier.', Json::encode($ignoreError), ); continue; @@ -54,6 +57,44 @@ public function initialize(): IgnoredErrorHelperResult } } + $uniquedExpandedIgnoreErrors = []; + foreach ($expandedIgnoreErrors as $ignoreError) { + if (!isset($ignoreError['message']) && !isset($ignoreError['identifier'])) { + $uniquedExpandedIgnoreErrors[] = $ignoreError; + continue; + } + if (!isset($ignoreError['path'])) { + $uniquedExpandedIgnoreErrors[] = $ignoreError; + continue; + } + + $key = ''; + if (isset($ignoreError['message'])) { + $key = sprintf("%s\n%s", $ignoreError['message'], $ignoreError['path']); + } + if (isset($ignoreError['identifier'])) { + $key = sprintf("%s\n%s", $key, $ignoreError['identifier']); + } + if ($key === '') { + throw new ShouldNotHappenException(); + } + + if (!array_key_exists($key, $uniquedExpandedIgnoreErrors)) { + $uniquedExpandedIgnoreErrors[$key] = $ignoreError; + continue; + } + + $uniquedExpandedIgnoreErrors[$key] = [ + 'message' => $ignoreError['message'] ?? null, + 'path' => $ignoreError['path'], + 'identifier' => $ignoreError['identifier'] ?? null, + 'count' => ($uniquedExpandedIgnoreErrors[$key]['count'] ?? 1) + ($ignoreError['count'] ?? 1), + 'reportUnmatched' => ($uniquedExpandedIgnoreErrors[$key]['reportUnmatched'] ?? $this->reportUnmatchedIgnoredErrors) || ($ignoreError['reportUnmatched'] ?? $this->reportUnmatchedIgnoredErrors), + ]; + } + + $expandedIgnoreErrors = array_values($uniquedExpandedIgnoreErrors); + foreach ($expandedIgnoreErrors as $i => $ignoreError) { $ignoreErrorEntry = [ 'index' => $i, @@ -61,9 +102,9 @@ public function initialize(): IgnoredErrorHelperResult ]; try { if (is_array($ignoreError)) { - if (!isset($ignoreError['message'])) { + if (!isset($ignoreError['message']) && !isset($ignoreError['identifier'])) { $errors[] = sprintf( - 'Ignored error %s is missing a message.', + 'Ignored error %s is missing a message or an identifier.', Json::encode($ignoreError), ); continue; diff --git a/src/Analyser/Ignore/IgnoredErrorHelperProcessedResult.php b/src/Analyser/Ignore/IgnoredErrorHelperProcessedResult.php new file mode 100644 index 0000000000..4ffacf46d4 --- /dev/null +++ b/src/Analyser/Ignore/IgnoredErrorHelperProcessedResult.php @@ -0,0 +1,47 @@ + $notIgnoredErrors + * @param list $ignoredErrors + * @param list $otherIgnoreMessages + */ + public function __construct( + private array $notIgnoredErrors, + private array $ignoredErrors, + private array $otherIgnoreMessages, + ) + { + } + + /** + * @return list + */ + public function getNotIgnoredErrors(): array + { + return $this->notIgnoredErrors; + } + + /** + * @return list + */ + public function getIgnoredErrors(): array + { + return $this->ignoredErrors; + } + + /** + * @return list + */ + public function getOtherIgnoreMessages(): array + { + return $this->otherIgnoreMessages; + } + +} diff --git a/src/Analyser/IgnoredErrorHelperResult.php b/src/Analyser/Ignore/IgnoredErrorHelperResult.php similarity index 82% rename from src/Analyser/IgnoredErrorHelperResult.php rename to src/Analyser/Ignore/IgnoredErrorHelperResult.php index 372fa17ff2..95d4273ef8 100644 --- a/src/Analyser/IgnoredErrorHelperResult.php +++ b/src/Analyser/Ignore/IgnoredErrorHelperResult.php @@ -1,13 +1,12 @@ $errors * @param array> $otherIgnoreErrors * @param array>> $ignoreErrorsByFile * @param (string|mixed[])[] $ignoreErrors @@ -35,7 +34,7 @@ public function __construct( } /** - * @return string[] + * @return list */ public function getErrors(): array { @@ -45,28 +44,27 @@ public function getErrors(): array /** * @param Error[] $errors * @param string[] $analysedFiles - * @return string[]|Error[] */ public function process( array $errors, bool $onlyFiles, array $analysedFiles, bool $hasInternalErrors, - ): array + ): IgnoredErrorHelperProcessedResult { $unmatchedIgnoredErrors = $this->ignoreErrors; - $addErrors = []; + $stringErrors = []; - $processIgnoreError = function (Error $error, int $i, $ignore) use (&$unmatchedIgnoredErrors, &$addErrors): bool { + $processIgnoreError = function (Error $error, int $i, $ignore) use (&$unmatchedIgnoredErrors, &$stringErrors): bool { $shouldBeIgnored = false; if (is_string($ignore)) { - $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore, null); + $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore, null, null); if ($shouldBeIgnored) { unset($unmatchedIgnoredErrors[$i]); } } else { if (isset($ignore['path'])) { - $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'], $ignore['path']); + $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'] ?? null, $ignore['identifier'] ?? null, $ignore['path']); if ($shouldBeIgnored) { if (isset($ignore['count'])) { $realCount = $unmatchedIgnoredErrors[$i]['realCount'] ?? 0; @@ -87,7 +85,7 @@ public function process( } } elseif (isset($ignore['paths'])) { foreach ($ignore['paths'] as $j => $ignorePath) { - $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'], $ignorePath); + $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'] ?? null, $ignore['identifier'] ?? null, $ignorePath); if (!$shouldBeIgnored) { continue; } @@ -104,7 +102,7 @@ public function process( break; } } else { - $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'], null); + $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'] ?? null, $ignore['identifier'] ?? null, null); if ($shouldBeIgnored) { unset($unmatchedIgnoredErrors[$i]); } @@ -113,7 +111,7 @@ public function process( if ($shouldBeIgnored) { if (!$error->canBeIgnored()) { - $addErrors[] = sprintf( + $stringErrors[] = sprintf( 'Error message "%s" cannot be ignored, use excludePaths instead.', $error->getMessage(), ); @@ -125,7 +123,8 @@ public function process( return true; }; - $errors = array_values(array_filter($errors, function (Error $error) use ($processIgnoreError): bool { + $ignoredErrors = []; + foreach ($errors as $errorIndex => $error) { $filePath = $this->fileHelper->normalizePath($error->getFilePath()); if (isset($this->ignoreErrorsByFile[$filePath])) { foreach ($this->ignoreErrorsByFile[$filePath] as $ignoreError) { @@ -133,7 +132,9 @@ public function process( $ignore = $ignoreError['ignoreError']; $result = $processIgnoreError($error, $i, $ignore); if (!$result) { - return false; + unset($errors[$errorIndex]); + $ignoredErrors[] = [$error, $ignore]; + continue 2; } } } @@ -147,7 +148,9 @@ public function process( $ignore = $ignoreError['ignoreError']; $result = $processIgnoreError($error, $i, $ignore); if (!$result) { - return false; + unset($errors[$errorIndex]); + $ignoredErrors[] = [$error, $ignore]; + continue 2; } } } @@ -159,12 +162,14 @@ public function process( $result = $processIgnoreError($error, $i, $ignore); if (!$result) { - return false; + unset($errors[$errorIndex]); + $ignoredErrors[] = [$error, $ignore]; + continue 2; } } + } - return true; - })); + $errors = array_values($errors); foreach ($unmatchedIgnoredErrors as $unmatchedIgnoredError) { if (!isset($unmatchedIgnoredError['count']) || !isset($unmatchedIgnoredError['realCount'])) { @@ -175,18 +180,16 @@ public function process( continue; } - $addErrors[] = new Error(sprintf( + $errors[] = (new Error(sprintf( 'Ignored error pattern %s is expected to occur %d %s, but occurred %d %s.', IgnoredError::stringifyPattern($unmatchedIgnoredError), $unmatchedIgnoredError['count'], $unmatchedIgnoredError['count'] === 1 ? 'time' : 'times', $unmatchedIgnoredError['realCount'], $unmatchedIgnoredError['realCount'] === 1 ? 'time' : 'times', - ), $unmatchedIgnoredError['file'], $unmatchedIgnoredError['line'], false); + ), $unmatchedIgnoredError['file'], $unmatchedIgnoredError['line'], false))->withIdentifier('ignore.count'); } - $errors = array_merge($errors, $addErrors); - $analysedFilesKeys = array_fill_keys($analysedFiles, true); if (!$hasInternalErrors) { @@ -201,21 +204,21 @@ public function process( && (isset($unmatchedIgnoredError['realPath']) || !$onlyFiles) ) { if ($unmatchedIgnoredError['realCount'] < $unmatchedIgnoredError['count']) { - $errors[] = new Error(sprintf( + $errors[] = (new Error(sprintf( 'Ignored error pattern %s is expected to occur %d %s, but occurred only %d %s.', IgnoredError::stringifyPattern($unmatchedIgnoredError), $unmatchedIgnoredError['count'], $unmatchedIgnoredError['count'] === 1 ? 'time' : 'times', $unmatchedIgnoredError['realCount'], $unmatchedIgnoredError['realCount'] === 1 ? 'time' : 'times', - ), $unmatchedIgnoredError['file'], $unmatchedIgnoredError['line'], false); + ), $unmatchedIgnoredError['file'], $unmatchedIgnoredError['line'], false))->withIdentifier('ignore.count'); } } elseif (isset($unmatchedIgnoredError['realPath'])) { if (!array_key_exists($unmatchedIgnoredError['realPath'], $analysedFilesKeys)) { continue; } - $errors[] = new Error( + $errors[] = (new Error( sprintf( 'Ignored error pattern %s was not matched in reported errors.', IgnoredError::stringifyPattern($unmatchedIgnoredError), @@ -223,9 +226,9 @@ public function process( $unmatchedIgnoredError['realPath'], null, false, - ); + ))->withIdentifier('ignore.unmatched'); } elseif (!$onlyFiles) { - $errors[] = sprintf( + $stringErrors[] = sprintf( 'Ignored error pattern %s was not matched in reported errors.', IgnoredError::stringifyPattern($unmatchedIgnoredError), ); @@ -233,7 +236,7 @@ public function process( } } - return $errors; + return new IgnoredErrorHelperProcessedResult($errors, $ignoredErrors, $stringErrors); } } diff --git a/src/Analyser/IgnoredError.php b/src/Analyser/IgnoredError.php deleted file mode 100644 index 52bacade42..0000000000 --- a/src/Analyser/IgnoredError.php +++ /dev/null @@ -1,75 +0,0 @@ -getMessage(); - $errorMessage = str_replace(['\r\n', '\r'], '\n', $errorMessage); - $ignoredErrorPattern = str_replace([preg_quote('\r\n'), preg_quote('\r')], preg_quote('\n'), $ignoredErrorPattern); - - if ($path !== null) { - if (Strings::match($errorMessage, $ignoredErrorPattern) === null) { - return false; - } - - $fileExcluder = new FileExcluder($fileHelper, new EmptyStubFilesProvider(), [$path]); - $isExcluded = $fileExcluder->isExcludedFromAnalysing($error->getFilePath()); - if (!$isExcluded && $error->getTraitFilePath() !== null) { - return $fileExcluder->isExcludedFromAnalysing($error->getTraitFilePath()); - } - - return $isExcluded; - } - - return Strings::match($errorMessage, $ignoredErrorPattern) !== null; - } - -} diff --git a/src/Analyser/ImpurePoint.php b/src/Analyser/ImpurePoint.php new file mode 100644 index 0000000000..83663b849b --- /dev/null +++ b/src/Analyser/ImpurePoint.php @@ -0,0 +1,60 @@ +scope; + } + + /** + * @return Node\Expr|Node\Stmt|VirtualNode + */ + public function getNode() + { + return $this->node; + } + + /** + * @return ImpurePointIdentifier + */ + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getDescription(): string + { + return $this->description; + } + + public function isCertain(): bool + { + return $this->certain; + } + +} diff --git a/src/Analyser/InternalScopeFactory.php b/src/Analyser/InternalScopeFactory.php new file mode 100644 index 0000000000..7d3414f297 --- /dev/null +++ b/src/Analyser/InternalScopeFactory.php @@ -0,0 +1,41 @@ + $expressionTypes + * @param array $nativeExpressionTypes + * @param array $conditionalExpressions + * @param list $inClosureBindScopeClasses + * @param array $currentlyAssignedExpressions + * @param array $currentlyAllowedUndefinedExpressions + * @param list $inFunctionCallsStack + */ + public function create( + ScopeContext $context, + bool $declareStrictTypes = false, + FunctionReflection|MethodReflection|null $function = null, + ?string $namespace = null, + array $expressionTypes = [], + array $nativeExpressionTypes = [], + array $conditionalExpressions = [], + array $inClosureBindScopeClasses = [], + ?ParametersAcceptor $anonymousFunctionReflection = null, + bool $inFirstLevelStatement = true, + array $currentlyAssignedExpressions = [], + array $currentlyAllowedUndefinedExpressions = [], + array $inFunctionCallsStack = [], + bool $afterExtractCall = false, + ?Scope $parentScope = null, + bool $nativeTypesPromoted = false, + ): MutatingScope; + +} diff --git a/src/Analyser/LazyScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php similarity index 80% rename from src/Analyser/LazyScopeFactory.php rename to src/Analyser/LazyInternalScopeFactory.php index 38b4fe2108..0d74666e85 100644 --- a/src/Analyser/LazyScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -4,23 +4,22 @@ use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider; +use PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\Type; use function is_a; -class LazyScopeFactory implements ScopeFactory +class LazyInternalScopeFactory implements InternalScopeFactory { - private bool $treatPhpDocTypesAsCertain; - private bool $explicitMixedInUnknownGenericNew; private bool $explicitMixedForGlobalVariables; @@ -33,40 +32,35 @@ public function __construct( private Container $container, ) { - $this->treatPhpDocTypesAsCertain = $container->getParameter('treatPhpDocTypesAsCertain'); $this->explicitMixedInUnknownGenericNew = $this->container->getParameter('featureToggles')['explicitMixedInUnknownGenericNew']; $this->explicitMixedForGlobalVariables = $this->container->getParameter('featureToggles')['explicitMixedForGlobalVariables']; } /** - * @param array $constantTypes - * @param VariableTypeHolder[] $variablesTypes - * @param VariableTypeHolder[] $moreSpecificTypes + * @param array $expressionTypes + * @param array $nativeExpressionTypes * @param array $conditionalExpressions * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions - * @param array $nativeExpressionTypes - * @param array<(FunctionReflection|MethodReflection)> $inFunctionCallsStack - * + * @param list $inFunctionCallsStack */ public function create( ScopeContext $context, bool $declareStrictTypes = false, - array $constantTypes = [], FunctionReflection|MethodReflection|null $function = null, ?string $namespace = null, - array $variablesTypes = [], - array $moreSpecificTypes = [], + array $expressionTypes = [], + array $nativeExpressionTypes = [], array $conditionalExpressions = [], - ?string $inClosureBindScopeClass = null, + array $inClosureBindScopeClasses = [], ?ParametersAcceptor $anonymousFunctionReflection = null, bool $inFirstLevelStatement = true, array $currentlyAssignedExpressions = [], array $currentlyAllowedUndefinedExpressions = [], - array $nativeExpressionTypes = [], array $inFunctionCallsStack = [], bool $afterExtractCall = false, ?Scope $parentScope = null, + bool $nativeTypesPromoted = false, ): MutatingScope { $scopeClass = $this->scopeClass; @@ -79,6 +73,7 @@ public function create( $this->container->getByType(ReflectionProvider::class), $this->container->getByType(InitializerExprTypeResolver::class), $this->container->getByType(DynamicReturnTypeExtensionRegistryProvider::class)->getRegistry(), + $this->container->getByType(ExpressionTypeResolverExtensionRegistryProvider::class)->getRegistry(), $this->container->getByType(ExprPrinter::class), $this->container->getByType(TypeSpecifier::class), $this->container->getByType(PropertyReflectionFinder::class), @@ -88,22 +83,20 @@ public function create( $context, $this->container->getByType(PhpVersion::class), $declareStrictTypes, - $constantTypes, $function, $namespace, - $variablesTypes, - $moreSpecificTypes, + $expressionTypes, + $nativeExpressionTypes, $conditionalExpressions, - $inClosureBindScopeClass, + $inClosureBindScopeClasses, $anonymousFunctionReflection, $inFirstLevelStatement, $currentlyAssignedExpressions, $currentlyAllowedUndefinedExpressions, - $nativeExpressionTypes, $inFunctionCallsStack, - $this->treatPhpDocTypesAsCertain, $afterExtractCall, $parentScope, + $nativeTypesPromoted, $this->explicitMixedInUnknownGenericNew, $this->explicitMixedForGlobalVariables, ); diff --git a/src/Analyser/LocalIgnoresProcessor.php b/src/Analyser/LocalIgnoresProcessor.php new file mode 100644 index 0000000000..04368960b0 --- /dev/null +++ b/src/Analyser/LocalIgnoresProcessor.php @@ -0,0 +1,101 @@ + $temporaryFileErrors + * @param LinesToIgnore $linesToIgnore + * @param LinesToIgnore $unmatchedLineIgnores + */ + public function process( + array $temporaryFileErrors, + array $linesToIgnore, + array $unmatchedLineIgnores, + ): LocalIgnoresProcessorResult + { + $fileErrors = []; + $locallyIgnoredErrors = []; + foreach ($temporaryFileErrors as $tmpFileError) { + $line = $tmpFileError->getLine(); + if ( + $line !== null + && $tmpFileError->canBeIgnored() + && array_key_exists($tmpFileError->getFile(), $linesToIgnore) + && array_key_exists($line, $linesToIgnore[$tmpFileError->getFile()]) + ) { + $identifiers = $linesToIgnore[$tmpFileError->getFile()][$line]; + if ($identifiers === null) { + $locallyIgnoredErrors[] = $tmpFileError; + unset($unmatchedLineIgnores[$tmpFileError->getFile()][$line]); + continue; + } + + if ($tmpFileError->getIdentifier() === null) { + $fileErrors[] = $tmpFileError; + continue; + } + + foreach ($identifiers as $i => $ignoredIdentifier) { + if ($ignoredIdentifier !== $tmpFileError->getIdentifier()) { + continue; + } + + unset($identifiers[$i]); + + if (count($identifiers) > 0) { + $linesToIgnore[$tmpFileError->getFile()][$line] = array_values($identifiers); + } else { + unset($linesToIgnore[$tmpFileError->getFile()][$line]); + } + + if ( + array_key_exists($tmpFileError->getFile(), $unmatchedLineIgnores) + && array_key_exists($line, $unmatchedLineIgnores[$tmpFileError->getFile()]) + ) { + $unmatchedIgnoredIdentifiers = $unmatchedLineIgnores[$tmpFileError->getFile()][$line]; + if (is_array($unmatchedIgnoredIdentifiers)) { + foreach ($unmatchedIgnoredIdentifiers as $j => $unmatchedIgnoredIdentifier) { + if ($ignoredIdentifier !== $unmatchedIgnoredIdentifier) { + continue; + } + + unset($unmatchedIgnoredIdentifiers[$j]); + + if (count($unmatchedIgnoredIdentifiers) > 0) { + $unmatchedLineIgnores[$tmpFileError->getFile()][$line] = array_values($unmatchedIgnoredIdentifiers); + } else { + unset($unmatchedLineIgnores[$tmpFileError->getFile()][$line]); + } + break; + } + } + } + + $locallyIgnoredErrors[] = $tmpFileError; + continue 2; + } + } + + $fileErrors[] = $tmpFileError; + } + + return new LocalIgnoresProcessorResult( + $fileErrors, + $locallyIgnoredErrors, + $linesToIgnore, + $unmatchedLineIgnores, + ); + } + +} diff --git a/src/Analyser/LocalIgnoresProcessorResult.php b/src/Analyser/LocalIgnoresProcessorResult.php new file mode 100644 index 0000000000..8c7a36287f --- /dev/null +++ b/src/Analyser/LocalIgnoresProcessorResult.php @@ -0,0 +1,58 @@ + $fileErrors + * @param list $locallyIgnoredErrors + * @param LinesToIgnore $linesToIgnore + * @param LinesToIgnore $unmatchedLineIgnores + */ + public function __construct( + private array $fileErrors, + private array $locallyIgnoredErrors, + private array $linesToIgnore, + private array $unmatchedLineIgnores, + ) + { + } + + /** + * @return list + */ + public function getFileErrors(): array + { + return $this->fileErrors; + } + + /** + * @return list + */ + public function getLocallyIgnoredErrors(): array + { + return $this->locallyIgnoredErrors; + } + + /** + * @return LinesToIgnore + */ + public function getLinesToIgnore(): array + { + return $this->linesToIgnore; + } + + /** + * @return LinesToIgnore + */ + public function getUnmatchedLineIgnores(): array + { + return $this->unmatchedLineIgnores; + } + +} diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 0636454a55..93e5816a73 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -5,7 +5,6 @@ use ArrayAccess; use Closure; use Generator; -use Nette\Utils\Strings; use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\Expr; @@ -30,22 +29,36 @@ use PhpParser\Node\Scalar\String_; use PhpParser\NodeFinder; use PHPStan\Node\ExecutionEndNode; +use PHPStan\Node\Expr\AlwaysRememberedExpr; +use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\GetIterableValueTypeExpr; use PHPStan\Node\Expr\GetOffsetValueTypeExpr; use PHPStan\Node\Expr\OriginalPropertyTypeExpr; +use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; +use PHPStan\Node\Expr\PropertyInitializationExpr; +use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr; use PHPStan\Node\Expr\SetOffsetValueTypeExpr; use PHPStan\Node\Expr\TypeExpr; +use PHPStan\Node\Expr\UnsetOffsetExpr; +use PHPStan\Node\InvalidateExprNode; +use PHPStan\Node\IssetExpr; use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Node\PropertyAssignNode; use PHPStan\Parser\ArrayMapArgVisitor; use PHPStan\Parser\NewAssignedToPropertyVisitor; use PHPStan\Parser\Parser; -use PHPStan\Parser\ParserErrorsException; use PHPStan\Php\PhpVersion; +use PHPStan\PhpDoc\Tag\TemplateTag; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\Reflection\Callables\SimpleThrowPoint; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\Dummy\DummyConstructorReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; @@ -54,6 +67,7 @@ use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\PassedByReference; use PHPStan\Reflection\Php\DummyParameter; use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; @@ -64,15 +78,16 @@ use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; -use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; -use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\NonEmptyArrayType; +use PHPStan\Type\Accessory\OversizedArrayType; use PHPStan\Type\ArrayType; +use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\BooleanType; use PHPStan\Type\ClosureType; -use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; @@ -81,25 +96,27 @@ use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\DynamicReturnTypeExtensionRegistry; use PHPStan\Type\ErrorType; +use PHPStan\Type\ExpressionTypeResolverExtensionRegistry; +use PHPStan\Type\FloatType; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; -use PHPStan\Type\GenericTypeVariableResolver; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; +use PHPStan\Type\NonAcceptingNeverType; use PHPStan\Type\NonexistentParentClassType; use PHPStan\Type\NullType; +use PHPStan\Type\ObjectShapeType; use PHPStan\Type\ObjectType; -use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\ParserNodeTypeToPHPStanType; use PHPStan\Type\StaticType; -use PHPStan\Type\StaticTypeFactory; use PHPStan\Type\StringType; use PHPStan\Type\ThisType; use PHPStan\Type\Type; @@ -111,20 +128,24 @@ use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; +use stdClass; use Throwable; use function abs; -use function array_column; -use function array_filter; use function array_key_exists; +use function array_key_first; use function array_keys; use function array_map; +use function array_merge; use function array_pop; use function array_slice; +use function array_values; use function count; use function explode; use function get_class; use function implode; use function in_array; +use function is_bool; +use function is_numeric; use function is_string; use function ltrim; use function sprintf; @@ -139,6 +160,10 @@ class MutatingScope implements Scope { + private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4; + + private const KEEP_VOID_ATTRIBUTE_NAME = 'keepVoid'; + /** @var Type[] */ private array $resolvedTypes = []; @@ -152,21 +177,23 @@ class MutatingScope implements Scope private ?self $scopeOutOfFirstLevelStatement = null; + private ?self $scopeWithPromotedNativeTypes = null; + /** - * @param array $constantTypes - * @param VariableTypeHolder[] $variableTypes - * @param VariableTypeHolder[] $moreSpecificTypes + * @param array $expressionTypes * @param array $conditionalExpressions + * @param list $inClosureBindScopeClasses * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions - * @param array $nativeExpressionTypes - * @param array $inFunctionCallsStack + * @param array $nativeExpressionTypes + * @param list $inFunctionCallsStack */ public function __construct( - private ScopeFactory $scopeFactory, + private InternalScopeFactory $scopeFactory, private ReflectionProvider $reflectionProvider, private InitializerExprTypeResolver $initializerExprTypeResolver, private DynamicReturnTypeExtensionRegistry $dynamicReturnTypeExtensionRegistry, + private ExpressionTypeResolverExtensionRegistry $expressionTypeResolverExtensionRegistry, private ExprPrinter $exprPrinter, private TypeSpecifier $typeSpecifier, private PropertyReflectionFinder $propertyReflectionFinder, @@ -176,22 +203,20 @@ public function __construct( private ScopeContext $context, private PhpVersion $phpVersion, private bool $declareStrictTypes = false, - private array $constantTypes = [], - private FunctionReflection|MethodReflection|null $function = null, + private FunctionReflection|ExtendedMethodReflection|null $function = null, ?string $namespace = null, - private array $variableTypes = [], - private array $moreSpecificTypes = [], + private array $expressionTypes = [], + private array $nativeExpressionTypes = [], private array $conditionalExpressions = [], - private ?string $inClosureBindScopeClass = null, + private array $inClosureBindScopeClasses = [], private ?ParametersAcceptor $anonymousFunctionReflection = null, private bool $inFirstLevelStatement = true, private array $currentlyAssignedExpressions = [], private array $currentlyAllowedUndefinedExpressions = [], - private array $nativeExpressionTypes = [], private array $inFunctionCallsStack = [], - private bool $treatPhpDocTypesAsCertain = true, private bool $afterExtractCall = false, private ?Scope $parentScope = null, + private bool $nativeTypesPromoted = false, private bool $explicitMixedInUnknownGenericNew = false, private bool $explicitMixedForGlobalVariables = false, ) @@ -247,10 +272,10 @@ public function enterDeclareStrictTypes(): self return $this->scopeFactory->create( $this->context, true, - [], null, null, - $this->variableTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, ); } @@ -280,7 +305,7 @@ public function getTraitReflection(): ?ClassReflection /** * @api - * @return FunctionReflection|MethodReflection|null + * @return FunctionReflection|ExtendedMethodReflection|null */ public function getFunction() { @@ -305,14 +330,6 @@ public function getParentScope(): ?Scope return $this->parentScope; } - /** - * @return array - */ - private function getVariableTypes(): array - { - return $this->variableTypes; - } - /** @api */ public function canAnyVariableExist(): bool { @@ -324,28 +341,27 @@ public function afterExtractCall(): self return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, [], - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $this->currentlyAssignedExpressions, $this->currentlyAllowedUndefinedExpressions, - $this->nativeExpressionTypes, $this->inFunctionCallsStack, true, $this->parentScope, + $this->nativeTypesPromoted, ); } public function afterClearstatcacheCall(): self { - $moreSpecificTypes = $this->moreSpecificTypes; - foreach (array_keys($moreSpecificTypes) as $exprString) { + $expressionTypes = $this->expressionTypes; + foreach (array_keys($expressionTypes) as $exprString) { // list from https://www.php.net/manual/en/function.clearstatcache.php // stat(), lstat(), file_exists(), is_writable(), is_readable(), is_executable(), is_file(), is_dir(), is_link(), filectime(), fileatime(), filemtime(), fileinode(), filegroup(), fileowner(), filesize(), filetype(), and fileperms(). @@ -370,38 +386,37 @@ public function afterClearstatcacheCall(): self 'filetype', 'fileperms', ] as $functionName) { - if (!str_starts_with((string) $exprString, $functionName . '(') && !str_starts_with((string) $exprString, '\\' . $functionName . '(')) { + if (!str_starts_with($exprString, $functionName . '(') && !str_starts_with($exprString, '\\' . $functionName . '(')) { continue; } - unset($moreSpecificTypes[$exprString]); + unset($expressionTypes[$exprString]); continue 2; } } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $moreSpecificTypes, + $expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $this->currentlyAssignedExpressions, $this->currentlyAllowedUndefinedExpressions, - $this->nativeExpressionTypes, $this->inFunctionCallsStack, $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); } public function afterOpenSslCall(string $openSslFunctionName): self { - $moreSpecificTypes = $this->moreSpecificTypes; + $expressionTypes = $this->expressionTypes; if (in_array($openSslFunctionName, [ 'openssl_cipher_iv_length', @@ -458,27 +473,26 @@ public function afterOpenSslCall(string $openSslFunctionName): self 'openssl_x509_read', 'openssl_x509_verify', ], true)) { - unset($moreSpecificTypes['\openssl_error_string()']); + unset($expressionTypes['\openssl_error_string()']); } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $moreSpecificTypes, + $expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $this->currentlyAssignedExpressions, $this->currentlyAllowedUndefinedExpressions, - $this->nativeExpressionTypes, $this->inFunctionCallsStack, $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); } @@ -489,7 +503,8 @@ public function hasVariableType(string $variableName): TrinaryLogic return TrinaryLogic::createYes(); } - if (!isset($this->variableTypes[$variableName])) { + $varExprString = '$' . $variableName; + if (!isset($this->expressionTypes[$varExprString])) { if ($this->canAnyVariableExist()) { return TrinaryLogic::createMaybe(); } @@ -497,7 +512,7 @@ public function hasVariableType(string $variableName): TrinaryLogic return TrinaryLogic::createNo(); } - return $this->variableTypes[$variableName]->getCertainty(); + return $this->expressionTypes[$varExprString]->getCertainty(); } /** @api */ @@ -508,26 +523,30 @@ public function getVariableType(string $variableName): Type return IntegerRangeType::fromInterval(1, null); } if ($variableName === 'argv') { - return TypeCombinator::intersect( + return AccessoryArrayListType::intersectWith(TypeCombinator::intersect( new ArrayType(new IntegerType(), new StringType()), new NonEmptyArrayType(), - ); + )); + } + if ($this->canAnyVariableExist()) { + return new MixedType(); } } if ($this->isGlobalVariable($variableName)) { - return new ArrayType(new StringType(), new MixedType($this->explicitMixedForGlobalVariables)); + return new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType($this->explicitMixedForGlobalVariables)); } if ($this->hasVariableType($variableName)->no()) { throw new UndefinedVariableException($this, $variableName); } - if (!array_key_exists($variableName, $this->variableTypes)) { + $varExprString = '$' . $variableName; + if (!array_key_exists($varExprString, $this->expressionTypes)) { return new MixedType(); } - return $this->variableTypes[$variableName]->getType(); + return TypeUtils::resolveLateResolvableTypes($this->expressionTypes[$varExprString]->getType()); } /** @@ -537,12 +556,15 @@ public function getVariableType(string $variableName): Type public function getDefinedVariables(): array { $variables = []; - foreach ($this->variableTypes as $variableName => $holder) { + foreach ($this->expressionTypes as $exprString => $holder) { + if (!$holder->getExpr() instanceof Variable) { + continue; + } if (!$holder->getCertainty()->yes()) { continue; } - $variables[] = $variableName; + $variables[] = substr($exprString, 1); } return $variables; @@ -550,17 +572,7 @@ public function getDefinedVariables(): array private function isGlobalVariable(string $variableName): bool { - return in_array($variableName, [ - 'GLOBALS', - '_SERVER', - '_GET', - '_POST', - '_FILES', - '_COOKIE', - '_SESSION', - '_REQUEST', - '_ENV', - ], true); + return in_array($variableName, self::SUPERGLOBAL_VARIABLES, true); } /** @api */ @@ -570,21 +582,13 @@ public function hasConstant(Name $name): bool if ($isCompilerHaltOffset) { return $this->fileHasCompilerHaltStatementCalls(); } - if ($name->isFullyQualified()) { - if (array_key_exists($name->toCodeString(), $this->constantTypes)) { - return true; - } - } - if ($this->getNamespace() !== null) { - $constantName = new FullyQualified([$this->getNamespace(), $name->toString()]); - if (array_key_exists($constantName->toCodeString(), $this->constantTypes)) { + if (!$name->isFullyQualified() && $this->getNamespace() !== null) { + if ($this->hasExpressionType(new ConstFetch(new FullyQualified([$this->getNamespace(), $name->toString()])))->yes()) { return true; } } - - $constantName = new FullyQualified($name->toString()); - if (array_key_exists($constantName->toCodeString(), $this->constantTypes)) { + if ($this->hasExpressionType(new ConstFetch(new FullyQualified($name->toString())))->yes()) { return true; } @@ -629,20 +633,32 @@ public function getAnonymousFunctionReturnType(): ?Type public function getType(Expr $node): Type { if ($node instanceof GetIterableKeyTypeExpr) { - return $this->getType($node->getExpr())->getIterableKeyType(); + return $this->getIterableKeyType($this->getType($node->getExpr())); } if ($node instanceof GetIterableValueTypeExpr) { - return $this->getType($node->getExpr())->getIterableValueType(); + return $this->getIterableValueType($this->getType($node->getExpr())); } if ($node instanceof GetOffsetValueTypeExpr) { return $this->getType($node->getVar())->getOffsetValueType($this->getType($node->getDim())); } + if ($node instanceof ExistingArrayDimFetch) { + return $this->getType(new Expr\ArrayDimFetch($node->getVar(), $node->getDim())); + } + if ($node instanceof UnsetOffsetExpr) { + return $this->getType($node->getVar())->unsetOffset($this->getType($node->getDim())); + } if ($node instanceof SetOffsetValueTypeExpr) { return $this->getType($node->getVar())->setOffsetValueType( $node->getDim() !== null ? $this->getType($node->getDim()) : null, $this->getType($node->getValue()), ); } + if ($node instanceof SetExistingOffsetValueTypeExpr) { + return $this->getType($node->getVar())->setExistingOffsetValueType( + $this->getType($node->getDim()), + $this->getType($node->getValue()), + ); + } if ($node instanceof TypeExpr) { return $node->getExprType(); } @@ -659,7 +675,7 @@ public function getType(Expr $node): Type $key = $this->getNodeKey($node); if (!array_key_exists($key, $this->resolvedTypes)) { - $this->resolvedTypes[$key] = TypeUtils::resolveLateResolvableTypes($this->resolveType($node)); + $this->resolvedTypes[$key] = TypeUtils::resolveLateResolvableTypes($this->resolveType($key, $node)); } return $this->resolvedTypes[$key]; } @@ -676,18 +692,32 @@ private function getNodeKey(Expr $node): string $key .= '/*' . $node->getAttribute('startFilePos') . '*/'; } + if ($node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME) === true) { + $key .= '/*' . self::KEEP_VOID_ATTRIBUTE_NAME . '*/'; + } + return $key; } - private function resolveType(Expr $node): Type + private function resolveType(string $exprString, Expr $node): Type { + foreach ($this->expressionTypeResolverExtensionRegistry->getExtensions() as $extension) { + $type = $extension->getType($node, $this); + if ($type !== null) { + return $type; + } + } + if ($node instanceof Expr\Exit_ || $node instanceof Expr\Throw_) { - return new NeverType(true); + return new NonAcceptingNeverType(); } - $exprString = $this->getNodeKey($node); - if (isset($this->moreSpecificTypes[$exprString]) && $this->moreSpecificTypes[$exprString]->getCertainty()->yes()) { - return $this->moreSpecificTypes[$exprString]->getType(); + if (!$node instanceof Variable && $this->hasExpressionType($node)->yes()) { + return $this->expressionTypes[$exprString]->getType(); + } + + if ($node instanceof AlwaysRememberedExpr) { + return $node->getExprType(); } if ($node instanceof Expr\BinaryOp\Smaller) { @@ -729,8 +759,8 @@ private function resolveType(Expr $node): Type if ($node instanceof Expr\Empty_) { $result = $this->issetCheck($node->expr, static function (Type $type): ?bool { - $isNull = (new NullType())->isSuperTypeOf($type); - $isFalsey = (new ConstantBooleanType(false))->isSuperTypeOf($type->toBoolean()); + $isNull = $type->isNull(); + $isFalsey = $type->toBoolean()->isFalse(); if ($isNull->maybe()) { return null; } @@ -739,14 +769,7 @@ private function resolveType(Expr $node): Type } if ($isNull->yes()) { - if ($isFalsey->yes()) { - return false; - } - if ($isFalsey->no()) { - return true; - } - - return false; + return $isFalsey->no(); } return !$isFalsey->yes(); @@ -759,11 +782,7 @@ private function resolveType(Expr $node): Type } if ($node instanceof Node\Expr\BooleanNot) { - if ($this->treatPhpDocTypesAsCertain) { - $exprBooleanType = $this->getType($node->expr)->toBoolean(); - } else { - $exprBooleanType = $this->getNativeType($node->expr)->toBoolean(); - } + $exprBooleanType = $this->getType($node->expr)->toBoolean(); if ($exprBooleanType instanceof ConstantBooleanType) { return new ConstantBooleanType(!$exprBooleanType->getValue()); } @@ -779,37 +798,27 @@ private function resolveType(Expr $node): Type $node instanceof Node\Expr\BinaryOp\BooleanAnd || $node instanceof Node\Expr\BinaryOp\LogicalAnd ) { - if ($this->treatPhpDocTypesAsCertain) { - $leftBooleanType = $this->getType($node->left)->toBoolean(); - } else { - $leftBooleanType = $this->getNativeType($node->left)->toBoolean(); - } - - if ( - $leftBooleanType instanceof ConstantBooleanType - && !$leftBooleanType->getValue() - ) { + $leftBooleanType = $this->getType($node->left)->toBoolean(); + if ($leftBooleanType->isFalse()->yes()) { return new ConstantBooleanType(false); } - if ($this->treatPhpDocTypesAsCertain) { - $rightBooleanType = $this->filterByTruthyValue($node->left)->getType($node->right)->toBoolean(); + if ($this->getBooleanExpressionDepth($node->left) <= self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH) { + $noopCallback = static function (): void { + }; + $leftResult = $this->nodeScopeResolver->processExprNode(new Node\Stmt\Expression($node->left), $node->left, $this, $noopCallback, ExpressionContext::createDeep()); + $rightBooleanType = $leftResult->getTruthyScope()->getType($node->right)->toBoolean(); } else { - $rightBooleanType = $this->promoteNativeTypes()->filterByTruthyValue($node->left)->getType($node->right)->toBoolean(); + $rightBooleanType = $this->filterByTruthyValue($node->left)->getType($node->right)->toBoolean(); } - if ( - $rightBooleanType instanceof ConstantBooleanType - && !$rightBooleanType->getValue() - ) { + if ($rightBooleanType->isFalse()->yes()) { return new ConstantBooleanType(false); } if ( - $leftBooleanType instanceof ConstantBooleanType - && $leftBooleanType->getValue() - && $rightBooleanType instanceof ConstantBooleanType - && $rightBooleanType->getValue() + $leftBooleanType->isTrue()->yes() + && $rightBooleanType->isTrue()->yes() ) { return new ConstantBooleanType(true); } @@ -821,36 +830,27 @@ private function resolveType(Expr $node): Type $node instanceof Node\Expr\BinaryOp\BooleanOr || $node instanceof Node\Expr\BinaryOp\LogicalOr ) { - if ($this->treatPhpDocTypesAsCertain) { - $leftBooleanType = $this->getType($node->left)->toBoolean(); - } else { - $leftBooleanType = $this->getNativeType($node->left)->toBoolean(); - } - if ( - $leftBooleanType instanceof ConstantBooleanType - && $leftBooleanType->getValue() - ) { + $leftBooleanType = $this->getType($node->left)->toBoolean(); + if ($leftBooleanType->isTrue()->yes()) { return new ConstantBooleanType(true); } - if ($this->treatPhpDocTypesAsCertain) { - $rightBooleanType = $this->filterByFalseyValue($node->left)->getType($node->right)->toBoolean(); + if ($this->getBooleanExpressionDepth($node->left) <= self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH) { + $noopCallback = static function (): void { + }; + $leftResult = $this->nodeScopeResolver->processExprNode(new Node\Stmt\Expression($node->left), $node->left, $this, $noopCallback, ExpressionContext::createDeep()); + $rightBooleanType = $leftResult->getFalseyScope()->getType($node->right)->toBoolean(); } else { - $rightBooleanType = $this->promoteNativeTypes()->filterByFalseyValue($node->left)->getType($node->right)->toBoolean(); + $rightBooleanType = $this->filterByFalseyValue($node->left)->getType($node->right)->toBoolean(); } - if ( - $rightBooleanType instanceof ConstantBooleanType - && $rightBooleanType->getValue() - ) { + if ($rightBooleanType->isTrue()->yes()) { return new ConstantBooleanType(true); } if ( - $leftBooleanType instanceof ConstantBooleanType - && !$leftBooleanType->getValue() - && $rightBooleanType instanceof ConstantBooleanType - && !$rightBooleanType->getValue() + $leftBooleanType->isFalse()->yes() + && $rightBooleanType->isFalse()->yes() ) { return new ConstantBooleanType(false); } @@ -859,13 +859,8 @@ private function resolveType(Expr $node): Type } if ($node instanceof Node\Expr\BinaryOp\LogicalXor) { - if ($this->treatPhpDocTypesAsCertain) { - $leftBooleanType = $this->getType($node->left)->toBoolean(); - $rightBooleanType = $this->getType($node->right)->toBoolean(); - } else { - $leftBooleanType = $this->getNativeType($node->left)->toBoolean(); - $rightBooleanType = $this->getNativeType($node->right)->toBoolean(); - } + $leftBooleanType = $this->getType($node->left)->toBoolean(); + $rightBooleanType = $this->getType($node->right)->toBoolean(); if ( $leftBooleanType instanceof ConstantBooleanType @@ -890,20 +885,15 @@ private function resolveType(Expr $node): Type return new ConstantBooleanType(true); } - if ($this->treatPhpDocTypesAsCertain) { - $leftType = $this->getType($node->left); - $rightType = $this->getType($node->right); - } else { - $leftType = $this->getNativeType($node->left); - $rightType = $this->getNativeType($node->right); - } + $leftType = $this->getType($node->left); + $rightType = $this->getType($node->right); if ( ( $node->left instanceof Node\Expr\PropertyFetch || $node->left instanceof Node\Expr\StaticPropertyFetch ) - && $rightType instanceof NullType + && $rightType->isNull()->yes() && !$this->hasPropertyNativeType($node->left) ) { return new BooleanType(); @@ -914,7 +904,7 @@ private function resolveType(Expr $node): Type $node->right instanceof Node\Expr\PropertyFetch || $node->right instanceof Node\Expr\StaticPropertyFetch ) - && $leftType instanceof NullType + && $leftType->isNull()->yes() && !$this->hasPropertyNativeType($node->right) ) { return new BooleanType(); @@ -928,11 +918,7 @@ private function resolveType(Expr $node): Type } if ($node instanceof Expr\Instanceof_) { - if ($this->treatPhpDocTypesAsCertain) { - $expressionType = $this->getType($node->expr); - } else { - $expressionType = $this->getNativeType($node->expr); - } + $expressionType = $this->getType($node->expr); if ( $this->isInTrait() && TypeUtils::findThisType($expressionType) !== null @@ -962,7 +948,7 @@ private function resolveType(Expr $node): Type if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } - if ($type instanceof TypeWithClassName) { + if ($type->getObjectClassNames() !== []) { $uncertainty = true; return $type; } @@ -1115,71 +1101,35 @@ private function resolveType(Expr $node): Type } elseif ($node instanceof String_) { return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); } elseif ($node instanceof Node\Scalar\Encapsed) { - $parts = []; - foreach ($node->parts as $part) { - if ($part instanceof EncapsedStringPart) { - $parts[] = new ConstantStringType($part->value); - continue; - } + $resultType = null; - $partStringType = $this->getType($part)->toString(); - if ($partStringType instanceof ErrorType) { - return new ErrorType(); - } - - $parts[] = $partStringType; - } + foreach ($node->parts as $part) { + $partType = $part instanceof EncapsedStringPart + ? new ConstantStringType($part->value) + : $this->getType($part)->toString(); + if ($resultType === null) { + $resultType = $partType; - $constantString = new ConstantStringType(''); - foreach ($parts as $part) { - if ($part instanceof ConstantStringType) { - $constantString = $constantString->append($part); continue; } - $isNonEmpty = false; - $isNonFalsy = false; - $isLiteralString = true; - foreach ($parts as $partType) { - if ($partType->isNonFalsyString()->yes()) { - $isNonFalsy = true; - } - if ($partType->isNonEmptyString()->yes()) { - $isNonEmpty = true; - } - if ($partType->isLiteralString()->yes()) { - continue; - } - $isLiteralString = false; - } - - $accessoryTypes = []; - if ($isNonFalsy === true) { - $accessoryTypes[] = new AccessoryNonFalsyStringType(); - } elseif ($isNonEmpty === true) { - $accessoryTypes[] = new AccessoryNonEmptyStringType(); - } - - if ($isLiteralString === true) { - $accessoryTypes[] = new AccessoryLiteralStringType(); - } - if (count($accessoryTypes) > 0) { - $accessoryTypes[] = new StringType(); - return new IntersectionType($accessoryTypes); + $resultType = $this->initializerExprTypeResolver->resolveConcatType($resultType, $partType); + if (count($resultType->getConstantStrings()) === 0) { + return $resultType; } - - return new StringType(); } - return $constantString; + return $resultType ?? new ConstantStringType(''); } elseif ($node instanceof DNumber) { return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); } elseif ($node instanceof Expr\CallLike && $node->isFirstClassCallable()) { if ($node instanceof FuncCall) { if ($node->name instanceof Name) { if ($this->reflectionProvider->hasFunction($node->name, $this)) { + $function = $this->reflectionProvider->getFunction($node->name, $this); return $this->createFirstClassCallable( - $this->reflectionProvider->getFunction($node->name, $this)->getVariants(), + $function, + $function->getVariants(), ); } @@ -1192,6 +1142,7 @@ private function resolveType(Expr $node): Type } return $this->createFirstClassCallable( + null, $callableType->getCallableParametersAcceptors($this), ); } @@ -1207,7 +1158,10 @@ private function resolveType(Expr $node): Type return new ObjectType(Closure::class); } - return $this->createFirstClassCallable($method->getVariants()); + return $this->createFirstClassCallable( + $method, + $method->getVariants(), + ); } if ($node instanceof Expr\StaticCall) { @@ -1215,17 +1169,21 @@ private function resolveType(Expr $node): Type return new ObjectType(Closure::class); } - $classType = $this->resolveTypeByName($node->class); if (!$node->name instanceof Node\Identifier) { return new ObjectType(Closure::class); } + $classType = $this->resolveTypeByName($node->class); $methodName = $node->name->toString(); if (!$classType->hasMethod($methodName)->yes()) { return new ObjectType(Closure::class); } - return $this->createFirstClassCallable($classType->getMethod($methodName, $this)->getVariants()); + $method = $classType->getMethod($methodName, $this); + return $this->createFirstClassCallable( + $method, + $method->getVariants(), + ); } if ($node instanceof New_) { @@ -1297,8 +1255,8 @@ private function resolveType(Expr $node): Type } } else { $yieldFromType = $arrowScope->getType($yieldNode->expr); - $keyType = $yieldFromType->getIterableKeyType(); - $valueType = $yieldFromType->getIterableValueType(); + $keyType = $arrowScope->getIterableKeyType($yieldFromType); + $valueType = $arrowScope->getIterableValueType($yieldFromType); } $returnType = new GenericObjectType(Generator::class, [ @@ -1308,21 +1266,52 @@ private function resolveType(Expr $node): Type new VoidType(), ]); } else { - $returnType = $arrowScope->getType($node->expr); + $returnType = $arrowScope->getKeepVoidType($node->expr); if ($node->returnType !== null) { $returnType = TypehintHelper::decideType($this->getFunctionType($node->returnType, false, false), $returnType); } } + + $arrowFunctionExprResult = $this->nodeScopeResolver->processExprNode( + new Node\Stmt\Expression($node->expr), + $node->expr, + $arrowScope, + static function (): void { + }, + ExpressionContext::createDeep(), + ); + $throwPoints = $arrowFunctionExprResult->getThrowPoints(); + $impurePoints = $arrowFunctionExprResult->getImpurePoints(); + $invalidateExpressions = []; + $usedVariables = []; } else { $closureScope = $this->enterAnonymousFunctionWithoutReflection($node, $callableParameters); $closureReturnStatements = []; $closureYieldStatements = []; $closureExecutionEnds = []; - $this->nodeScopeResolver->processStmtNodes($node, $node->stmts, $closureScope, static function (Node $node, Scope $scope) use ($closureScope, &$closureReturnStatements, &$closureYieldStatements, &$closureExecutionEnds): void { + $closureImpurePoints = []; + $invalidateExpressions = []; + $closureStatementResult = $this->nodeScopeResolver->processStmtNodes($node, $node->stmts, $closureScope, static function (Node $node, Scope $scope) use ($closureScope, &$closureReturnStatements, &$closureYieldStatements, &$closureExecutionEnds, &$closureImpurePoints, &$invalidateExpressions): void { if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { return; } + if ($node instanceof InvalidateExprNode) { + $invalidateExpressions[] = $node; + return; + } + + if ($node instanceof PropertyAssignNode) { + $closureImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + return; + } + if ($node instanceof ExecutionEndNode) { if ($node->getStatementResult()->isAlwaysTerminating()) { foreach ($node->getStatementResult()->getExitPoints() as $exitPoint) { @@ -1351,7 +1340,10 @@ private function resolveType(Expr $node): Type } $closureYieldStatements[] = [$node, $scope]; - }); + }, StatementContext::createTopLevel()); + + $throwPoints = $closureStatementResult->getThrowPoints(); + $impurePoints = array_merge($closureImpurePoints, $closureStatementResult->getImpurePoints()); $returnTypes = []; $hasNull = false; @@ -1366,13 +1358,13 @@ private function resolveType(Expr $node): Type if (count($returnTypes) === 0) { if (count($closureExecutionEnds) > 0 && !$hasNull) { - $returnType = new NeverType(true); + $returnType = new NonAcceptingNeverType(); } else { $returnType = new VoidType(); } } else { if (count($closureExecutionEnds) > 0) { - $returnTypes[] = new NeverType(true); + $returnTypes[] = new NonAcceptingNeverType(); } if ($hasNull) { $returnTypes[] = new NullType(); @@ -1401,8 +1393,8 @@ private function resolveType(Expr $node): Type } $yieldFromType = $yieldScope->getType($yieldNode->expr); - $keyTypes[] = $yieldFromType->getIterableKeyType(); - $valueTypes[] = $yieldFromType->getIterableValueType(); + $keyTypes[] = $yieldScope->getIterableKeyType($yieldFromType); + $valueTypes[] = $yieldScope->getIterableValueType($yieldFromType); } $returnType = new GenericObjectType(Generator::class, [ @@ -1414,12 +1406,44 @@ private function resolveType(Expr $node): Type } else { $returnType = TypehintHelper::decideType($this->getFunctionType($node->returnType, false, false), $returnType); } + + $usedVariables = []; + foreach ($node->uses as $use) { + if (!is_string($use->var->name)) { + continue; + } + + $usedVariables[] = $use->var->name; + } + + foreach ($node->uses as $use) { + if (!$use->byRef) { + continue; + } + + $impurePoints[] = new ImpurePoint( + $this, + $node, + 'functionCall', + 'call to a Closure with by-ref use', + true, + ); + break; + } } return new ClosureType( $parameters, $returnType, $isVariadic, + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + TemplateTypeVarianceMap::createEmpty(), + [], + array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? SimpleThrowPoint::createExplicit($throwPoint->getType(), $throwPoint->canContainAnyThrowable()) : SimpleThrowPoint::createImplicit(), $throwPoints), + array_map(static fn (ImpurePoint $impurePoint) => new SimpleImpurePoint($impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $impurePoints), + $invalidateExpressions, + $usedVariables, ); } elseif ($node instanceof New_) { if ($node->class instanceof Name) { @@ -1449,7 +1473,7 @@ private function resolveType(Expr $node): Type } $exprType = $this->getType($node->class); - return $this->getTypeToInstantiateForNew($exprType); + return $exprType->getObjectTypeOrClassStringObjectType(); } elseif ($node instanceof Array_) { return $this->initializerExprTypeResolver->getArrayType($node, fn (Expr $expr): Type => $this->getType($expr)); @@ -1467,7 +1491,31 @@ private function resolveType(Expr $node): Type return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); } elseif ($node instanceof Object_) { $castToObject = static function (Type $type): Type { - if ((new ObjectWithoutClassType())->isSuperTypeOf($type)->yes()) { + $constantArrays = $type->getConstantArrays(); + if (count($constantArrays) > 0) { + $objects = []; + foreach ($constantArrays as $constantArray) { + $properties = []; + $optionalProperties = []; + foreach ($constantArray->getKeyTypes() as $i => $keyType) { + if (!$keyType instanceof ConstantStringType) { + // an object with integer properties is >weird< + continue; + } + $valueType = $constantArray->getValueTypes()[$i]; + $optional = $constantArray->isOptionalKey($i); + if ($optional) { + $optionalProperties[] = $keyType->getValue(); + } + $properties[$keyType->getValue()] = $valueType; + } + + $objects[] = TypeCombinator::intersect(new ObjectShapeType($properties, $optionalProperties), new ObjectType(stdClass::class)); + } + + return TypeCombinator::union(...$objects); + } + if ($type->isObject()->yes()) { return $type; } @@ -1486,16 +1534,17 @@ private function resolveType(Expr $node): Type return $this->getType($node->var); } elseif ($node instanceof Expr\PreInc || $node instanceof Expr\PreDec) { $varType = $this->getType($node->var); - $varScalars = TypeUtils::getConstantScalars($varType); - $stringType = new StringType(); + $varScalars = $varType->getConstantScalarValues(); + if (count($varScalars) > 0) { $newTypes = []; - foreach ($varScalars as $scalar) { - $varValue = $scalar->getValue(); + foreach ($varScalars as $varValue) { if ($node instanceof Expr\PreInc) { - ++$varValue; - } else { + if (!is_bool($varValue)) { + ++$varValue; + } + } elseif (is_numeric($varValue)) { --$varValue; } @@ -1504,9 +1553,24 @@ private function resolveType(Expr $node): Type return TypeCombinator::union(...$newTypes); } elseif ($varType->isString()->yes()) { if ($varType->isLiteralString()->yes()) { - return new IntersectionType([$stringType, new AccessoryLiteralStringType()]); + return new IntersectionType([ + new StringType(), + new AccessoryLiteralStringType(), + ]); } - return $stringType; + + if ($varType->isNumericString()->yes()) { + return new BenevolentUnionType([ + new IntegerType(), + new FloatType(), + ]); + } + + return new BenevolentUnionType([ + new StringType(), + new IntegerType(), + new FloatType(), + ]); } if ($node instanceof Expr\PreInc) { @@ -1521,25 +1585,16 @@ private function resolveType(Expr $node): Type } $returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (!$returnType instanceof TypeWithClassName) { - return new MixedType(); - } - - $generatorSendType = GenericTypeVariableResolver::getType($returnType, Generator::class, 'TSend'); - if ($generatorSendType === null) { + $generatorSendType = $returnType->getTemplateType(Generator::class, 'TSend'); + if ($generatorSendType instanceof ErrorType) { return new MixedType(); } return $generatorSendType; } elseif ($node instanceof Expr\YieldFrom) { $yieldFromType = $this->getType($node->expr); - - if (!$yieldFromType instanceof TypeWithClassName) { - return new MixedType(); - } - - $generatorReturnType = GenericTypeVariableResolver::getType($yieldFromType, Generator::class, 'TReturn'); - if ($generatorReturnType === null) { + $generatorReturnType = $yieldFromType->getTemplateType(Generator::class, 'TReturn'); + if ($generatorReturnType instanceof ErrorType) { return new MixedType(); } @@ -1551,6 +1606,9 @@ private function resolveType(Expr $node): Type $matchScope = $this; foreach ($node->arms as $arm) { if ($arm->conds === null) { + if ($node->hasAttribute(self::KEEP_VOID_ATTRIBUTE_NAME)) { + $arm->body->setAttribute(self::KEEP_VOID_ATTRIBUTE_NAME, $node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME)); + } $types[] = $matchScope->getType($arm->body); continue; } @@ -1559,22 +1617,30 @@ private function resolveType(Expr $node): Type throw new ShouldNotHappenException(); } - $filteringExpr = null; - foreach ($arm->conds as $armCond) { - $armCondExpr = new BinaryOp\Identical($cond, $armCond); - - if ($filteringExpr === null) { - $filteringExpr = $armCondExpr; - continue; + if (count($arm->conds) === 1) { + $filteringExpr = new BinaryOp\Identical($cond, $arm->conds[0]); + } else { + $items = []; + foreach ($arm->conds as $filteringExpr) { + $items[] = new Expr\ArrayItem($filteringExpr); } - - $filteringExpr = new BinaryOp\BooleanOr($filteringExpr, $armCondExpr); + $filteringExpr = new FuncCall( + new Name\FullyQualified('in_array'), + [ + new Arg($cond), + new Arg(new Array_($items)), + new Arg(new ConstFetch(new Name\FullyQualified('true'))), + ], + ); } $filteringExprType = $matchScope->getType($filteringExpr); - if (!(new ConstantBooleanType(false))->isSuperTypeOf($filteringExprType)->yes()) { + if (!$filteringExprType->isFalse()->yes()) { $truthyScope = $matchScope->filterByTruthyValue($filteringExpr); + if ($node->hasAttribute(self::KEEP_VOID_ATTRIBUTE_NAME)) { + $arm->body->setAttribute(self::KEEP_VOID_ATTRIBUTE_NAME, $node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME)); + } $types[] = $truthyScope->getType($arm->body); } @@ -1588,7 +1654,7 @@ private function resolveType(Expr $node): Type $issetResult = true; foreach ($node->vars as $var) { $result = $this->issetCheck($var, static function (Type $type): ?bool { - $isNull = (new NullType())->isSuperTypeOf($type); + $isNull = $type->isNull(); if ($isNull->maybe()) { return null; } @@ -1618,13 +1684,11 @@ private function resolveType(Expr $node): Type } if ($node instanceof Expr\BinaryOp\Coalesce) { - $leftType = $this->getType($node->left); - $rightType = $this->filterByFalseyValue( - new BinaryOp\NotIdentical($node->left, new ConstFetch(new Name('null'))), - )->getType($node->right); + $issetLeftExpr = new Expr\Isset_([$node->left]); + $leftType = $this->filterByTruthyValue($issetLeftExpr)->getType($node->left); $result = $this->issetCheck($node->left, static function (Type $type): ?bool { - $isNull = (new NullType())->isSuperTypeOf($type); + $isNull = $type->isNull(); if ($isNull->maybe()) { return null; } @@ -1632,6 +1696,12 @@ private function resolveType(Expr $node): Type return !$isNull->yes(); }); + if ($result !== null && $result !== false) { + return TypeCombinator::removeNull($leftType); + } + + $rightType = $this->filterByFalseyValue($issetLeftExpr)->getType($node->right); + if ($result === null) { return TypeCombinator::union( TypeCombinator::removeNull($leftType), @@ -1639,10 +1709,6 @@ private function resolveType(Expr $node): Type ); } - if ($result) { - return TypeCombinator::removeNull($leftType); - } - return $rightType; } @@ -1657,22 +1723,23 @@ private function resolveType(Expr $node): Type return new NullType(); } - if ($node->name->isFullyQualified()) { - if (array_key_exists($node->name->toCodeString(), $this->constantTypes)) { - return $this->constantResolver->resolveConstantType($node->name->toString(), $this->constantTypes[$node->name->toCodeString()]); - } + $namespacedName = null; + if (!$node->name->isFullyQualified() && $this->getNamespace() !== null) { + $namespacedName = new FullyQualified([$this->getNamespace(), $node->name->toString()]); } + $globalName = new FullyQualified($node->name->toString()); - if ($this->getNamespace() !== null) { - $constantName = new FullyQualified([$this->getNamespace(), $constName]); - if (array_key_exists($constantName->toCodeString(), $this->constantTypes)) { - return $this->constantResolver->resolveConstantType($constantName->toString(), $this->constantTypes[$constantName->toCodeString()]); + foreach ([$namespacedName, $globalName] as $name) { + if ($name === null) { + continue; + } + $constFetch = new ConstFetch($name); + if ($this->hasExpressionType($constFetch)->yes()) { + return $this->constantResolver->resolveConstantType( + $name->toString(), + $this->expressionTypes[$this->getNodeKey($constFetch)]->getType(), + ); } - } - - $constantName = new FullyQualified($constName); - if (array_key_exists($constantName->toCodeString(), $this->constantTypes)) { - return $this->constantResolver->resolveConstantType($constantName->toString(), $this->constantTypes[$constantName->toCodeString()]); } $constantType = $this->constantResolver->resolveConstant($node->name, $this); @@ -1682,6 +1749,9 @@ private function resolveType(Expr $node): Type return new ErrorType(); } elseif ($node instanceof Node\Expr\ClassConstFetch && $node->name instanceof Node\Identifier) { + if ($this->hasExpressionType($node)->yes()) { + return $this->expressionTypes[$exprString]->getType(); + } return $this->initializerExprTypeResolver->getClassConstFetchTypeByReflection( $node->class, $node->name->name, @@ -1691,34 +1761,38 @@ private function resolveType(Expr $node): Type } if ($node instanceof Expr\Ternary) { + $noopCallback = static function (): void { + }; + $condResult = $this->nodeScopeResolver->processExprNode(new Node\Stmt\Expression($node->cond), $node->cond, $this, $noopCallback, ExpressionContext::createDeep()); if ($node->if === null) { $conditionType = $this->getType($node->cond); $booleanConditionType = $conditionType->toBoolean(); - if ($booleanConditionType instanceof ConstantBooleanType) { - if ($booleanConditionType->getValue()) { - return $this->filterByTruthyValue($node->cond)->getType($node->cond); - } + if ($booleanConditionType->isTrue()->yes()) { + return $condResult->getTruthyScope()->getType($node->cond); + } - return $this->filterByFalseyValue($node->cond)->getType($node->else); + if ($booleanConditionType->isFalse()->yes()) { + return $condResult->getFalseyScope()->getType($node->else); } + return TypeCombinator::union( - TypeCombinator::remove($this->filterByTruthyValue($node->cond)->getType($node->cond), StaticTypeFactory::falsey()), - $this->filterByFalseyValue($node->cond)->getType($node->else), + TypeCombinator::removeFalsey($condResult->getTruthyScope()->getType($node->cond)), + $condResult->getFalseyScope()->getType($node->else), ); } $booleanConditionType = $this->getType($node->cond)->toBoolean(); - if ($booleanConditionType instanceof ConstantBooleanType) { - if ($booleanConditionType->getValue()) { - return $this->filterByTruthyValue($node->cond)->getType($node->if); - } + if ($booleanConditionType->isTrue()->yes()) { + return $condResult->getTruthyScope()->getType($node->if); + } - return $this->filterByFalseyValue($node->cond)->getType($node->else); + if ($booleanConditionType->isFalse()->yes()) { + return $condResult->getFalseyScope()->getType($node->else); } return TypeCombinator::union( - $this->filterByTruthyValue($node->cond)->getType($node->if), - $this->filterByFalseyValue($node->cond)->getType($node->else), + $condResult->getTruthyScope()->getType($node->if), + $condResult->getFalseyScope()->getType($node->else), ); } @@ -1742,6 +1816,22 @@ private function resolveType(Expr $node): Type } if ($node instanceof MethodCall && $node->name instanceof Node\Identifier) { + if ($this->nativeTypesPromoted) { + $typeCallback = function () use ($node): Type { + $methodReflection = $this->getMethodReflection( + $this->getNativeType($node->var), + $node->name->name, + ); + if ($methodReflection === null) { + return new ErrorType(); + } + + return ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getNativeReturnType(); + }; + + return $this->getNullsafeShortCircuitingType($node->var, $typeCallback()); + } + $typeCallback = function () use ($node): Type { $returnType = $this->methodCallReturnType( $this->getType($node->var), @@ -1759,6 +1849,9 @@ private function resolveType(Expr $node): Type if ($node instanceof Expr\NullsafeMethodCall) { $varType = $this->getType($node->var); + if ($varType->isNull()->yes()) { + return new NullType(); + } if (!TypeCombinator::containsNull($varType)) { return $this->getType(new MethodCall($node->var, $node->name, $node->args)); } @@ -1771,25 +1864,37 @@ private function resolveType(Expr $node): Type } if ($node instanceof Expr\StaticCall && $node->name instanceof Node\Identifier) { + if ($this->nativeTypesPromoted) { + $typeCallback = function () use ($node): Type { + if ($node->class instanceof Name) { + $staticMethodCalledOnType = $this->resolveTypeByName($node->class); + } else { + $staticMethodCalledOnType = $this->getNativeType($node->class); + } + $methodReflection = $this->getMethodReflection( + $staticMethodCalledOnType, + $node->name->name, + ); + if ($methodReflection === null) { + return new ErrorType(); + } + + return ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getNativeReturnType(); + }; + + $callType = $typeCallback(); + if ($node->class instanceof Expr) { + return $this->getNullsafeShortCircuitingType($node->class, $callType); + } + + return $callType; + } + $typeCallback = function () use ($node): Type { if ($node->class instanceof Name) { $staticMethodCalledOnType = $this->resolveTypeByName($node->class); } else { - $staticMethodCalledOnType = TypeTraverser::map($this->getType($node->class), static function (Type $type, callable $traverse): Type { - if ($type instanceof UnionType) { - return $traverse($type); - } - - if ($type instanceof GenericClassStringType) { - return $type->getGenericType(); - } - - if ($type instanceof ConstantStringType && $type->isClassString()) { - return new ObjectType($type->getValue()); - } - - return $type; - }); + $staticMethodCalledOnType = TypeCombinator::removeNull($this->getType($node->class))->getObjectTypeOrClassStringObjectType(); } $returnType = $this->methodCallReturnType( @@ -1812,6 +1917,19 @@ private function resolveType(Expr $node): Type } if ($node instanceof PropertyFetch && $node->name instanceof Node\Identifier) { + if ($this->nativeTypesPromoted) { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node, $this); + if ($propertyReflection === null) { + return new ErrorType(); + } + $nativeType = $propertyReflection->getNativeType(); + if ($nativeType === null) { + return new ErrorType(); + } + + return $this->getNullsafeShortCircuitingType($node->var, $nativeType); + } + $typeCallback = function () use ($node): Type { $returnType = $this->propertyFetchType( $this->getType($node->var), @@ -1829,6 +1947,9 @@ private function resolveType(Expr $node): Type if ($node instanceof Expr\NullsafePropertyFetch) { $varType = $this->getType($node->var); + if ($varType->isNull()->yes()) { + return new NullType(); + } if (!TypeCombinator::containsNull($varType)) { return $this->getType(new PropertyFetch($node->var, $node->name)); } @@ -1844,14 +1965,28 @@ private function resolveType(Expr $node): Type $node instanceof Expr\StaticPropertyFetch && $node->name instanceof Node\VarLikeIdentifier ) { + if ($this->nativeTypesPromoted) { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node, $this); + if ($propertyReflection === null) { + return new ErrorType(); + } + $nativeType = $propertyReflection->getNativeType(); + if ($nativeType === null) { + return new ErrorType(); + } + + if ($node->class instanceof Expr) { + return $this->getNullsafeShortCircuitingType($node->class, $nativeType); + } + + return $nativeType; + } + $typeCallback = function () use ($node): Type { if ($node->class instanceof Name) { $staticPropertyFetchedOnType = $this->resolveTypeByName($node->class); } else { - $staticPropertyFetchedOnType = $this->getType($node->class); - if ($staticPropertyFetchedOnType instanceof GenericClassStringType) { - $staticPropertyFetchedOnType = $staticPropertyFetchedOnType->getGenericType(); - } + $staticPropertyFetchedOnType = TypeCombinator::removeNull($this->getType($node->class))->getObjectTypeOrClassStringObjectType(); } $returnType = $this->propertyFetchType( @@ -1884,6 +2019,7 @@ private function resolveType(Expr $node): Type $this, $node->getArgs(), $calledOnType->getCallableParametersAcceptors($this), + null, )->getReturnType(); } @@ -1892,10 +2028,24 @@ private function resolveType(Expr $node): Type } $functionReflection = $this->reflectionProvider->getFunction($node->name, $this); + if ($this->nativeTypesPromoted) { + return ParametersAcceptorSelector::combineAcceptors($functionReflection->getVariants())->getNativeReturnType(); + } + + if ($functionReflection->getName() === 'call_user_func') { + $result = ArgumentsNormalizer::reorderCallUserFuncArguments($node, $this); + if ($result !== null) { + [, $innerFuncCall] = $result; + + return $this->getType($innerFuncCall); + } + } + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $this, $node->getArgs(), $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), ); $normalizedNode = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); if ($normalizedNode !== null) { @@ -1915,7 +2065,7 @@ private function resolveType(Expr $node): Type } } - return $parametersAcceptor->getReturnType(); + return $this->transformVoidToNull($parametersAcceptor->getReturnType(), $node); } return new MixedType(); @@ -1955,6 +2105,25 @@ private function getNullsafeShortCircuitingType(Expr $expr, Type $type): Type return $type; } + private function transformVoidToNull(Type $type, Node $node): Type + { + if ($node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME) === true) { + return $type; + } + + return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if ($type->isVoid()->yes()) { + return new NullType(); + } + + return $type; + }); + } + /** * @param callable(Type): ?bool $typeCallback */ @@ -1981,22 +2150,18 @@ public function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = n return $result; } elseif ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { - $type = $this->treatPhpDocTypesAsCertain - ? $this->getType($expr->var) - : $this->getNativeType($expr->var); - $dimType = $this->treatPhpDocTypesAsCertain - ? $this->getType($expr->dim) - : $this->getNativeType($expr->dim); - $hasOffsetValue = $type->hasOffsetValueType($dimType); + $type = $this->getType($expr->var); if (!$type->isOffsetAccessible()->yes()) { return $result ?? $this->issetCheckUndefined($expr->var); } + $dimType = $this->getType($expr->dim); + $hasOffsetValue = $type->hasOffsetValueType($dimType); if ($hasOffsetValue->no()) { return false; } - // If offset is cannot be null, store this error message and see if one of the earlier offsets is. + // If offset cannot be null, store this error message and see if one of the earlier offsets is. // E.g. $array['a']['b']['c'] ?? null; is a valid coalesce if a OR b or C might be null. if ($hasOffsetValue->yes()) { $result = $typeCallback($type->getOffsetValueType($dimType)); @@ -2039,7 +2204,7 @@ public function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = n $nativeType = $propertyReflection->getNativeType(); if (!$nativeType instanceof MixedType) { - if (!$this->isSpecified($expr)) { + if (!$this->hasExpressionType($expr)->yes()) { if ($expr instanceof Node\Expr\PropertyFetch) { return $this->issetCheckUndefined($expr->var); } @@ -2090,12 +2255,13 @@ private function issetCheckUndefined(Expr $expr): ?bool if ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { $type = $this->getType($expr->var); - $dimType = $this->getType($expr->dim); - $hasOffsetValue = $type->hasOffsetValueType($dimType); if (!$type->isOffsetAccessible()->yes()) { return $this->issetCheckUndefined($expr->var); } + $dimType = $this->getType($expr->dim); + $hasOffsetValue = $type->hasOffsetValueType($dimType); + if (!$hasOffsetValue->no()) { return $this->issetCheckUndefined($expr->var); } @@ -2117,17 +2283,74 @@ private function issetCheckUndefined(Expr $expr): ?bool /** * @param ParametersAcceptor[] $variants */ - private function createFirstClassCallable(array $variants): Type + private function createFirstClassCallable( + FunctionReflection|ExtendedMethodReflection|null $function, + array $variants, + ): Type { $closureTypes = []; + foreach ($variants as $variant) { + $returnType = $variant->getReturnType(); + if ($variant instanceof ParametersAcceptorWithPhpDocs) { + $returnType = $this->nativeTypesPromoted ? $variant->getNativeReturnType() : $returnType; + } + + $templateTags = []; + foreach ($variant->getTemplateTypeMap()->getTypes() as $templateType) { + if (!$templateType instanceof TemplateType) { + continue; + } + $templateTags[$templateType->getName()] = new TemplateTag( + $templateType->getName(), + $templateType->getBound(), + $templateType->getVariance(), + ); + } + + $throwPoints = []; + $impurePoints = []; + if ($variant instanceof CallableParametersAcceptor) { + $throwPoints = $variant->getThrowPoints(); + $impurePoints = $variant->getImpurePoints(); + } elseif ($function !== null) { + $returnTypeForThrow = $variant->getReturnType(); + $throwType = $function->getThrowType(); + if ($throwType === null) { + if ($returnTypeForThrow instanceof NeverType && $returnTypeForThrow->isExplicit()) { + $throwType = new ObjectType(Throwable::class); + } + } + + if ($throwType !== null) { + if (!$throwType->isVoid()->yes()) { + $throwPoints[] = SimpleThrowPoint::createExplicit($throwType, true); + } + } else { + if (!(new ObjectType(Throwable::class))->isSuperTypeOf($returnTypeForThrow)->yes()) { + $throwPoints[] = SimpleThrowPoint::createImplicit(); + } + } + + $impurePoint = SimpleImpurePoint::createFromVariant($function, $variant); + if ($impurePoint !== null) { + $impurePoints[] = $impurePoint; + } + } + $parameters = $variant->getParameters(); $closureTypes[] = new ClosureType( $parameters, - $variant->getReturnType(), + $returnType, $variant->isVariadic(), $variant->getTemplateTypeMap(), $variant->getResolvedTemplateTypeMap(), + $variant instanceof ParametersAcceptorWithPhpDocs ? $variant->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $templateTags, + $throwPoints, + $impurePoints, + [], + [], ); } @@ -2137,99 +2360,53 @@ private function createFirstClassCallable(array $variants): Type /** @api */ public function getNativeType(Expr $expr): Type { - $key = $this->getNodeKey($expr); - - if (array_key_exists($key, $this->nativeExpressionTypes)) { - return $this->nativeExpressionTypes[$key]; - } + return $this->promoteNativeTypes()->getType($expr); + } - if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { - return $this->getNullsafeShortCircuitingType( - $expr->var, - $this->getTypeFromArrayDimFetch( - $expr, - $this->getNativeType($expr->dim), - $this->getNativeType($expr->var), - ), - ); - } + public function getKeepVoidType(Expr $node): Type + { + $clonedNode = clone $node; + $clonedNode->setAttribute(self::KEEP_VOID_ATTRIBUTE_NAME, true); - return $this->getType($expr); + return $this->getType($clonedNode); } - /** @api */ + /** + * @api + * @deprecated Use getNativeType() + */ public function doNotTreatPhpDocTypesAsCertain(): Scope { - if (!$this->treatPhpDocTypesAsCertain) { - return $this; - } - - return new self( - $this->scopeFactory, - $this->reflectionProvider, - $this->initializerExprTypeResolver, - $this->dynamicReturnTypeExtensionRegistry, - $this->exprPrinter, - $this->typeSpecifier, - $this->propertyReflectionFinder, - $this->parser, - $this->nodeScopeResolver, - $this->constantResolver, - $this->context, - $this->phpVersion, - $this->declareStrictTypes, - $this->constantTypes, - $this->function, - $this->namespace, - $this->variableTypes, - $this->moreSpecificTypes, - $this->conditionalExpressions, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $this->currentlyAllowedUndefinedExpressions, - $this->nativeExpressionTypes, - $this->inFunctionCallsStack, - false, - $this->afterExtractCall, - $this->parentScope, - $this->explicitMixedInUnknownGenericNew, - ); + return $this->promoteNativeTypes(); } private function promoteNativeTypes(): self { - $variableTypes = $this->variableTypes; - foreach ($this->nativeExpressionTypes as $expressionType => $type) { - if (substr($expressionType, 0, 1) !== '$') { - throw new ShouldNotHappenException(); - } - - $variableName = substr($expressionType, 1); - $has = $this->hasVariableType($variableName); - if ($has->no()) { - throw new ShouldNotHappenException(); - } + if ($this->nativeTypesPromoted) { + return $this; + } - $variableTypes[$variableName] = new VariableTypeHolder($type, $has); + if ($this->scopeWithPromotedNativeTypes !== null) { + return $this->scopeWithPromotedNativeTypes; } - return $this->scopeFactory->create( + return $this->scopeWithPromotedNativeTypes = $this->scopeFactory->create( $this->context, $this->declareStrictTypes, - $this->constantTypes, $this->function, $this->namespace, - $variableTypes, - $this->moreSpecificTypes, - $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->nativeExpressionTypes, + [], + [], + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, $this->currentlyAssignedExpressions, $this->currentlyAllowedUndefinedExpressions, - [], + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + true, ); } @@ -2311,8 +2488,8 @@ public function resolveName(Name $name): string 'self', 'static', ], true)) { - if ($this->inClosureBindScopeClass !== null && $this->inClosureBindScopeClass !== 'static') { - return $this->inClosureBindScopeClass; + if ($this->inClosureBindScopeClasses !== [] && $this->inClosureBindScopeClasses !== ['static']) { + return $this->inClosureBindScopeClasses[0]; } return $this->getClassReflection()->getName(); } elseif ($originalClass === 'parent') { @@ -2330,9 +2507,9 @@ public function resolveName(Name $name): string public function resolveTypeByName(Name $name): TypeWithClassName { if ($name->toLowerString() === 'static' && $this->isInClass()) { - if ($this->inClosureBindScopeClass !== null && $this->inClosureBindScopeClass !== 'static') { - if ($this->reflectionProvider->hasClass($this->inClosureBindScopeClass)) { - return new StaticType($this->reflectionProvider->getClass($this->inClosureBindScopeClass)); + if ($this->inClosureBindScopeClasses !== [] && $this->inClosureBindScopeClasses !== ['static']) { + if ($this->reflectionProvider->hasClass($this->inClosureBindScopeClasses[0])) { + return new StaticType($this->reflectionProvider->getClass($this->inClosureBindScopeClasses[0])); } } @@ -2341,11 +2518,11 @@ public function resolveTypeByName(Name $name): TypeWithClassName $originalClass = $this->resolveName($name); if ($this->isInClass()) { - if ($this->inClosureBindScopeClass === $originalClass) { - if ($this->reflectionProvider->hasClass($this->inClosureBindScopeClass)) { - return new ThisType($this->reflectionProvider->getClass($this->inClosureBindScopeClass)); + if ($this->inClosureBindScopeClasses === [$originalClass]) { + if ($this->reflectionProvider->hasClass($originalClass)) { + return new ThisType($this->reflectionProvider->getClass($originalClass)); } - return new ObjectType($this->inClosureBindScopeClass); + return new ObjectType($originalClass); } $thisType = new ThisType($this->getClassReflection()); @@ -2367,41 +2544,54 @@ public function getTypeFromValue($value): Type return ConstantTypeHelper::getTypeFromValue($value); } - /** @api */ + /** + * @api + * @deprecated use hasExpressionType instead + */ public function isSpecified(Expr $node): bool { - $exprString = $this->getNodeKey($node); + return !$node instanceof Variable && $this->hasExpressionType($node)->yes(); + } + + /** @api */ + public function hasExpressionType(Expr $node): TrinaryLogic + { + if ($node instanceof Variable && is_string($node->name)) { + return $this->hasVariableType($node->name); + } - return isset($this->moreSpecificTypes[$exprString]) - && $this->moreSpecificTypes[$exprString]->getCertainty()->yes(); + $exprString = $this->getNodeKey($node); + if (!isset($this->expressionTypes[$exprString])) { + return TrinaryLogic::createNo(); + } + return $this->expressionTypes[$exprString]->getCertainty(); } /** * @param MethodReflection|FunctionReflection $reflection */ - public function pushInFunctionCall($reflection): self + public function pushInFunctionCall($reflection, ?ParameterReflection $parameter): self { $stack = $this->inFunctionCallsStack; - $stack[] = $reflection; + $stack[] = [$reflection, $parameter]; $scope = $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $this->currentlyAssignedExpressions, $this->currentlyAllowedUndefinedExpressions, - $this->nativeExpressionTypes, $stack, $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); $scope->resolvedTypes = $this->resolvedTypes; $scope->truthyScopes = $this->truthyScopes; @@ -2418,21 +2608,20 @@ public function popInFunctionCall(): self $scope = $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $this->currentlyAssignedExpressions, $this->currentlyAllowedUndefinedExpressions, - $this->nativeExpressionTypes, $stack, $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); $scope->resolvedTypes = $this->resolvedTypes; $scope->truthyScopes = $this->truthyScopes; @@ -2444,7 +2633,7 @@ public function popInFunctionCall(): self /** @api */ public function isInClassExists(string $className): bool { - foreach ($this->inFunctionCallsStack as $inFunctionCall) { + foreach ($this->inFunctionCallsStack as [$inFunctionCall]) { if (!$inFunctionCall instanceof FunctionReflection) { continue; } @@ -2461,7 +2650,17 @@ public function isInClassExists(string $className): bool new Arg(new String_(ltrim($className, '\\'))), ]); - return (new ConstantBooleanType(true))->isSuperTypeOf($this->getType($expr))->yes(); + return $this->getType($expr)->isTrue()->yes(); + } + + public function getFunctionCallStack(): array + { + return array_map(static fn ($values) => $values[0], $this->inFunctionCallsStack); + } + + public function getFunctionCallStackWithParameters(): array + { + return $this->inFunctionCallsStack; } /** @api */ @@ -2471,30 +2670,32 @@ public function isInFunctionExists(string $functionName): bool new Arg(new String_(ltrim($functionName, '\\'))), ]); - return (new ConstantBooleanType(true))->isSuperTypeOf($this->getType($expr))->yes(); + return $this->getType($expr)->isTrue()->yes(); } /** @api */ public function enterClass(ClassReflection $classReflection): self { + $thisHolder = ExpressionTypeHolder::createYes(new Variable('this'), new ThisType($classReflection)); + $constantTypes = $this->getConstantTypes(); + $constantTypes['$this'] = $thisHolder; + $nativeConstantTypes = $this->getNativeConstantTypes(); + $nativeConstantTypes['$this'] = $thisHolder; + return $this->scopeFactory->create( $this->context->enterClass($classReflection), $this->isDeclareStrictTypes(), - $this->constantTypes, null, $this->getNamespace(), - [ - 'this' => VariableTypeHolder::createYes(new ThisType($classReflection)), - ], + $constantTypes, + $nativeConstantTypes, [], [], null, - null, true, [], [], [], - [], false, $classReflection->isAnonymous() ? $this : null, ); @@ -2511,13 +2712,12 @@ public function enterTrait(ClassReflection $traitReflection): self return $this->scopeFactory->create( $this->context->enterTrait($traitReflection), $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $namespace, - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, [], - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, ); } @@ -2525,6 +2725,9 @@ public function enterTrait(ClassReflection $traitReflection): self /** * @api * @param Type[] $phpDocParameterTypes + * @param Type[] $parameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters */ public function enterClassMethod( Node\Stmt\ClassMethod $classMethod, @@ -2538,6 +2741,12 @@ public function enterClassMethod( bool $isFinal, ?bool $isPure = null, bool $acceptsNamedArguments = true, + ?Assertions $asserts = null, + ?Type $selfOutType = null, + ?string $phpDocComment = null, + array $parameterOutTypes = [], + array $immediatelyInvokedCallableParameters = [], + array $phpDocClosureThisTypeParameters = [], ): self { if (!$this->isInClass()) { @@ -2551,10 +2760,10 @@ public function enterClassMethod( $this->getFile(), $templateTypeMap, $this->getRealParameterTypes($classMethod), - array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $phpDocParameterTypes), + array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocParameterTypes), $this->getRealParameterDefaultValues($classMethod), $this->transformStaticType($this->getFunctionType($classMethod->returnType, false, false)), - $phpDocReturnType !== null ? TemplateTypeHelper::toArgument($phpDocReturnType) : null, + $phpDocReturnType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($phpDocReturnType)) : null, $throwType, $deprecatedDescription, $isDeprecated, @@ -2562,6 +2771,12 @@ public function enterClassMethod( $isFinal, $isPure, $acceptsNamedArguments, + $asserts ?? Assertions::createEmpty(), + $selfOutType, + $phpDocComment, + array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $parameterOutTypes), + $immediatelyInvokedCallableParameters, + array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocClosureThisTypeParameters), ), !$classMethod->isStatic(), ); @@ -2598,7 +2813,7 @@ private function getRealParameterTypes(Node\FunctionLike $functionLike): array } $realParameterTypes[$parameter->var->name] = $this->getFunctionType( $parameter->type, - $this->isParameterValueNullable($parameter), + $this->isParameterValueNullable($parameter) && $parameter->flags === 0, false, ); } @@ -2628,6 +2843,9 @@ private function getRealParameterDefaultValues(Node\FunctionLike $functionLike): /** * @api * @param Type[] $phpDocParameterTypes + * @param Type[] $parameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters */ public function enterFunction( Node\Stmt\Function_ $function, @@ -2641,6 +2859,11 @@ public function enterFunction( bool $isFinal, ?bool $isPure = null, bool $acceptsNamedArguments = true, + ?Assertions $asserts = null, + ?string $phpDocComment = null, + array $parameterOutTypes = [], + array $immediatelyInvokedCallableParameters = [], + array $phpDocClosureThisTypeParameters = [], ): self { return $this->enterFunctionLike( @@ -2660,6 +2883,11 @@ public function enterFunction( $isFinal, $isPure, $acceptsNamedArguments, + $asserts ?? Assertions::createEmpty(), + $phpDocComment, + array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $parameterOutTypes), + $immediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, ), false, ); @@ -2670,129 +2898,229 @@ private function enterFunctionLike( bool $preserveThis, ): self { - $variableTypes = []; + $acceptor = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()); + $parametersByName = []; + + foreach ($acceptor->getParameters() as $parameter) { + $parametersByName[$parameter->getName()] = $parameter; + } + + $expressionTypes = []; $nativeExpressionTypes = []; - foreach (ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getParameters() as $parameter) { + $conditionalTypes = []; + foreach ($acceptor->getParameters() as $parameter) { $parameterType = $parameter->getType(); + + if ($parameterType instanceof ConditionalTypeForParameter) { + $targetParameterName = substr($parameterType->getParameterName(), 1); + if (array_key_exists($targetParameterName, $parametersByName)) { + $targetParameter = $parametersByName[$targetParameterName]; + + $ifType = $parameterType->isNegated() ? $parameterType->getElse() : $parameterType->getIf(); + $elseType = $parameterType->isNegated() ? $parameterType->getIf() : $parameterType->getElse(); + + $holder = new ConditionalExpressionHolder([ + $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), TypeCombinator::intersect($targetParameter->getType(), $parameterType->getTarget())), + ], new ExpressionTypeHolder(new Variable($parameter->getName()), $ifType, TrinaryLogic::createYes())); + $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder; + + $holder = new ConditionalExpressionHolder([ + $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), TypeCombinator::remove($targetParameter->getType(), $parameterType->getTarget())), + ], new ExpressionTypeHolder(new Variable($parameter->getName()), $elseType, TrinaryLogic::createYes())); + $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder; + } + } + + $paramExprString = '$' . $parameter->getName(); if ($parameter->isVariadic()) { if ($this->phpVersion->supportsNamedArguments() && $functionReflection->acceptsNamedArguments()) { $parameterType = new ArrayType(new UnionType([new IntegerType(), new StringType()]), $parameterType); } else { - $parameterType = new ArrayType(new IntegerType(), $parameterType); + $parameterType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $parameterType)); } } - $variableTypes[$parameter->getName()] = VariableTypeHolder::createYes($parameterType); + $parameterNode = new Variable($parameter->getName()); + $expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameterNode, $parameterType); + + $parameterOriginalValueExpr = new ParameterVariableOriginalValueExpr($parameter->getName()); + $parameterOriginalValueExprString = $this->getNodeKey($parameterOriginalValueExpr); + $expressionTypes[$parameterOriginalValueExprString] = ExpressionTypeHolder::createYes($parameterOriginalValueExpr, $parameterType); $nativeParameterType = $parameter->getNativeType(); if ($parameter->isVariadic()) { if ($this->phpVersion->supportsNamedArguments() && $functionReflection->acceptsNamedArguments()) { $nativeParameterType = new ArrayType(new UnionType([new IntegerType(), new StringType()]), $nativeParameterType); } else { - $nativeParameterType = new ArrayType(new IntegerType(), $nativeParameterType); + $nativeParameterType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $nativeParameterType)); } } - $nativeExpressionTypes[sprintf('$%s', $parameter->getName())] = $nativeParameterType; + $nativeExpressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameterNode, $nativeParameterType); + $nativeExpressionTypes[$parameterOriginalValueExprString] = ExpressionTypeHolder::createYes($parameterOriginalValueExpr, $nativeParameterType); } - if ($preserveThis && array_key_exists('this', $this->variableTypes)) { - $variableTypes['this'] = $this->variableTypes['this']; + if ($preserveThis && array_key_exists('$this', $this->expressionTypes)) { + $expressionTypes['$this'] = $this->expressionTypes['$this']; + } + if ($preserveThis && array_key_exists('$this', $this->nativeExpressionTypes)) { + $nativeExpressionTypes['$this'] = $this->nativeExpressionTypes['$this']; } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $functionReflection, $this->getNamespace(), - $variableTypes, - [], - [], - null, - null, - true, - [], - [], - $nativeExpressionTypes, + array_merge($this->getConstantTypes(), $expressionTypes), + array_merge($this->getNativeConstantTypes(), $nativeExpressionTypes), + $conditionalTypes, ); } + /** @api */ public function enterNamespace(string $namespaceName): self { return $this->scopeFactory->create( $this->context->beginFile(), $this->isDeclareStrictTypes(), - $this->constantTypes, null, $namespaceName, ); } - public function enterClosureBind(?Type $thisType, string $scopeClass): self + /** + * @param list $scopeClasses + */ + public function enterClosureBind(?Type $thisType, ?Type $nativeThisType, array $scopeClasses): self { - $variableTypes = $this->getVariableTypes(); - + $expressionTypes = $this->expressionTypes; if ($thisType !== null) { - $variableTypes['this'] = VariableTypeHolder::createYes($thisType); + $expressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $thisType); + } else { + unset($expressionTypes['$this']); + } + + $nativeExpressionTypes = $this->nativeExpressionTypes; + if ($nativeThisType !== null) { + $nativeExpressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $nativeThisType); } else { - unset($variableTypes['this']); + unset($nativeExpressionTypes['$this']); } - if ($scopeClass === 'static' && $this->isInClass()) { - $scopeClass = $this->getClassReflection()->getName(); + if ($scopeClasses === ['static'] && $this->isInClass()) { + $scopeClasses = [$this->getClassReflection()->getName()]; } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, + $expressionTypes, + $nativeExpressionTypes, $this->conditionalExpressions, - $scopeClass, + $scopeClasses, $this->anonymousFunctionReflection, ); } public function restoreOriginalScopeAfterClosureBind(self $originalScope): self { - $variableTypes = $this->getVariableTypes(); - if (isset($originalScope->variableTypes['this'])) { - $variableTypes['this'] = $originalScope->variableTypes['this']; + $expressionTypes = $this->expressionTypes; + if (isset($originalScope->expressionTypes['$this'])) { + $expressionTypes['$this'] = $originalScope->expressionTypes['$this']; + } else { + unset($expressionTypes['$this']); + } + + $nativeExpressionTypes = $this->nativeExpressionTypes; + if (isset($originalScope->nativeExpressionTypes['$this'])) { + $nativeExpressionTypes['$this'] = $originalScope->nativeExpressionTypes['$this']; + } else { + unset($nativeExpressionTypes['$this']); + } + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $expressionTypes, + $nativeExpressionTypes, + $this->conditionalExpressions, + $originalScope->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + ); + } + + public function restoreThis(self $restoreThisScope): self + { + $expressionTypes = $this->expressionTypes; + $nativeExpressionTypes = $this->nativeExpressionTypes; + + if ($restoreThisScope->isInClass()) { + $nodeFinder = new NodeFinder(); + $cb = static fn ($expr) => $expr instanceof Variable && $expr->name === 'this'; + foreach ($restoreThisScope->expressionTypes as $exprString => $expressionTypeHolder) { + $expr = $expressionTypeHolder->getExpr(); + $thisExpr = $nodeFinder->findFirst([$expr], $cb); + if ($thisExpr === null) { + continue; + } + + $expressionTypes[$exprString] = $expressionTypeHolder; + } + + foreach ($restoreThisScope->nativeExpressionTypes as $exprString => $expressionTypeHolder) { + $expr = $expressionTypeHolder->getExpr(); + $thisExpr = $nodeFinder->findFirst([$expr], $cb); + if ($thisExpr === null) { + continue; + } + + $nativeExpressionTypes[$exprString] = $expressionTypeHolder; + } } else { - unset($variableTypes['this']); + unset($expressionTypes['$this']); + unset($nativeExpressionTypes['$this']); } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, + $expressionTypes, + $nativeExpressionTypes, $this->conditionalExpressions, - $originalScope->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + [], + [], + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, ); } - public function enterClosureCall(Type $thisType): self + public function enterClosureCall(Type $thisType, Type $nativeThisType): self { - $variableTypes = $this->getVariableTypes(); - $variableTypes['this'] = VariableTypeHolder::createYes($thisType); + $expressionTypes = $this->expressionTypes; + $expressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $thisType); + + $nativeExpressionTypes = $this->nativeExpressionTypes; + $nativeExpressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $nativeThisType); return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, + $expressionTypes, + $nativeExpressionTypes, $this->conditionalExpressions, - $thisType instanceof TypeWithClassName ? $thisType->getClassName() : null, + $thisType->getObjectClassNames(), $this->anonymousFunctionReflection, ); } @@ -2800,7 +3128,7 @@ public function enterClosureCall(Type $thisType): self /** @api */ public function isInClosureBind(): bool { - return $this->inClosureBindScopeClass !== null; + return $this->inClosureBindScopeClasses !== []; } /** @@ -2822,21 +3150,20 @@ public function enterAnonymousFunction( return $this->scopeFactory->create( $scope->context, $scope->isDeclareStrictTypes(), - $scope->constantTypes, $scope->getFunction(), $scope->getNamespace(), - $scope->variableTypes, - $scope->moreSpecificTypes, + $scope->expressionTypes, + $scope->nativeExpressionTypes, [], - $scope->inClosureBindScopeClass, + $scope->inClosureBindScopeClasses, $anonymousFunctionReflection, true, [], [], - $scope->nativeExpressionTypes, - [], + $this->inFunctionCallsStack, false, $this, + $this->nativeTypesPromoted, ); } @@ -2848,8 +3175,13 @@ private function enterAnonymousFunctionWithoutReflection( ?array $callableParameters = null, ): self { - $variableTypes = []; + $expressionTypes = []; + $nativeTypes = []; foreach ($closure->params as $i => $parameter) { + if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { + throw new ShouldNotHappenException(); + } + $paramExprString = sprintf('$%s', $parameter->var->name); $isNullable = $this->isParameterValueNullable($parameter); $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic); if ($callableParameters !== null) { @@ -2866,72 +3198,121 @@ private function enterAnonymousFunctionWithoutReflection( $parameterType = TypehintHelper::decideType($parameterType, new MixedType()); } } - - if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { - throw new ShouldNotHappenException(); - } - $variableTypes[$parameter->var->name] = VariableTypeHolder::createYes( - $parameterType, - ); + $holder = ExpressionTypeHolder::createYes($parameter->var, $parameterType); + $expressionTypes[$paramExprString] = $holder; + $nativeTypes[$paramExprString] = $holder; } - $nativeTypes = []; - $moreSpecificTypes = []; + $nonRefVariableNames = []; foreach ($closure->uses as $use) { if (!is_string($use->var->name)) { throw new ShouldNotHappenException(); } + $variableName = $use->var->name; + $paramExprString = '$' . $use->var->name; if ($use->byRef) { + $holder = ExpressionTypeHolder::createYes($use->var, new MixedType()); + $expressionTypes[$paramExprString] = $holder; + $nativeTypes[$paramExprString] = $holder; continue; } - $variableName = $use->var->name; + $nonRefVariableNames[$variableName] = true; if ($this->hasVariableType($variableName)->no()) { $variableType = new ErrorType(); + $variableNativeType = new ErrorType(); } else { $variableType = $this->getVariableType($variableName); - $nativeTypes[sprintf('$%s', $variableName)] = $this->getNativeType($use->var); + $variableNativeType = $this->getNativeType($use->var); } - $variableTypes[$variableName] = VariableTypeHolder::createYes($variableType); - foreach ($this->moreSpecificTypes as $exprString => $moreSpecificType) { - $matches = Strings::matchAll((string) $exprString, '#^\$([a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*)#'); - if ($matches === []) { - continue; - } + $expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($use->var, $variableType); + $nativeTypes[$paramExprString] = ExpressionTypeHolder::createYes($use->var, $variableNativeType); + } - $matches = array_column($matches, 1); - if (!in_array($variableName, $matches, true)) { - continue; + foreach ($this->invalidateStaticExpressions($this->expressionTypes) as $exprString => $typeHolder) { + $expr = $typeHolder->getExpr(); + if ($expr instanceof Variable) { + continue; + } + $variables = (new NodeFinder())->findInstanceOf([$expr], Variable::class); + if ($variables === [] && !$this->expressionTypeIsUnchangeable($typeHolder)) { + continue; + } + foreach ($variables as $variable) { + if (!$variable instanceof Variable) { + continue 2; + } + if (!is_string($variable->name)) { + continue 2; + } + if (!array_key_exists($variable->name, $nonRefVariableNames)) { + continue 2; } - - $moreSpecificTypes[$exprString] = $moreSpecificType; } + + $expressionTypes[$exprString] = $typeHolder; } if ($this->hasVariableType('this')->yes() && !$closure->static) { - $variableTypes['this'] = VariableTypeHolder::createYes($this->getVariableType('this')); + $node = new Variable('this'); + $expressionTypes['$this'] = ExpressionTypeHolder::createYes($node, $this->getType($node)); + $nativeTypes['$this'] = ExpressionTypeHolder::createYes($node, $this->getNativeType($node)); } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $variableTypes, - $moreSpecificTypes, + array_merge($this->getConstantTypes(), $expressionTypes), + array_merge($this->getNativeConstantTypes(), $nativeTypes), [], - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, new TrivialParametersAcceptor(), true, [], [], - $nativeTypes, [], false, $this, + $this->nativeTypesPromoted, ); } + private function expressionTypeIsUnchangeable(ExpressionTypeHolder $typeHolder): bool + { + $expr = $typeHolder->getExpr(); + $type = $typeHolder->getType(); + + return $expr instanceof FuncCall + && !$expr->isFirstClassCallable() + && $expr->name instanceof FullyQualified + && $expr->name->toLowerString() === 'function_exists' + && isset($expr->getArgs()[0]) + && count($this->getType($expr->getArgs()[0]->value)->getConstantStrings()) === 1 + && $type->isTrue()->yes(); + } + + /** + * @param array $expressionTypes + * @return array + */ + private function invalidateStaticExpressions(array $expressionTypes): array + { + $filteredExpressionTypes = []; + $nodeFinder = new NodeFinder(); + foreach ($expressionTypes as $exprString => $expressionType) { + $staticExpression = $nodeFinder->findFirst( + [$expressionType->getExpr()], + static fn ($node) => $node instanceof Expr\StaticCall || $node instanceof Expr\StaticPropertyFetch, + ); + if ($staticExpression !== null) { + continue; + } + $filteredExpressionTypes[$exprString] = $expressionType; + } + return $filteredExpressionTypes; + } + /** * @api * @param ParameterReflection[]|null $callableParameters @@ -2948,21 +3329,20 @@ public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $ca return $this->scopeFactory->create( $scope->context, $scope->isDeclareStrictTypes(), - $scope->constantTypes, $scope->getFunction(), $scope->getNamespace(), - $scope->variableTypes, - $scope->moreSpecificTypes, + $scope->expressionTypes, + $scope->nativeExpressionTypes, $scope->conditionalExpressions, - $scope->inClosureBindScopeClass, + $scope->inClosureBindScopeClasses, $anonymousFunctionReflection, true, [], [], - [], - [], + $this->inFunctionCallsStack, $scope->afterExtractCall, $scope->parentScope, + $this->nativeTypesPromoted, ); } @@ -2971,13 +3351,10 @@ public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $ca */ private function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFunction, ?array $callableParameters): self { - $variableTypes = $this->variableTypes; - $mixed = new MixedType(); - $parameterVariables = []; - $parameterVariableExpressions = []; + $arrowFunctionScope = $this; foreach ($arrowFunction->params as $i => $parameter) { if ($parameter->type === null) { - $parameterType = $mixed; + $parameterType = new MixedType(); } else { $isNullable = $this->isParameterValueNullable($parameter); $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic); @@ -3001,106 +3378,41 @@ private function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFu if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { throw new ShouldNotHappenException(); } - - $variableTypes[$parameter->var->name] = VariableTypeHolder::createYes($parameterType); - $parameterVariables[] = $parameter->var->name; - $parameterVariableExpressions[] = $parameter->var; + $arrowFunctionScope = $arrowFunctionScope->assignVariable($parameter->var->name, $parameterType, $parameterType); } if ($arrowFunction->static) { - unset($variableTypes['this']); + $arrowFunctionScope = $arrowFunctionScope->invalidateExpression(new Variable('this')); } - $conditionalExpressions = []; - foreach ($this->conditionalExpressions as $conditionalExprString => $holders) { - $newHolders = []; - foreach ($parameterVariables as $parameterVariable) { - $exprString = '$' . $parameterVariable; - if ($exprString === $conditionalExprString) { - continue 2; - } - } - - foreach ($holders as $holder) { - foreach ($parameterVariables as $parameterVariable) { - $exprString = '$' . $parameterVariable; - foreach (array_keys($holder->getConditionExpressionTypes()) as $conditionalExprString2) { - if ($exprString === $conditionalExprString2) { - continue 3; - } - } - } - - $newHolders[] = $holder; - } - - if (count($newHolders) === 0) { - continue; - } + return $this->scopeFactory->create( + $arrowFunctionScope->context, + $this->isDeclareStrictTypes(), + $arrowFunctionScope->getFunction(), + $arrowFunctionScope->getNamespace(), + $this->invalidateStaticExpressions($arrowFunctionScope->expressionTypes), + $arrowFunctionScope->nativeExpressionTypes, + $arrowFunctionScope->conditionalExpressions, + $arrowFunctionScope->inClosureBindScopeClasses, + new TrivialParametersAcceptor(), + true, + [], + [], + [], + $arrowFunctionScope->afterExtractCall, + $arrowFunctionScope->parentScope, + $this->nativeTypesPromoted, + ); + } - $conditionalExpressions[$conditionalExprString] = $newHolders; + public function isParameterValueNullable(Node\Param $parameter): bool + { + if ($parameter->default instanceof ConstFetch) { + return strtolower((string) $parameter->default->name) === 'null'; } - foreach ($parameterVariables as $parameterVariable) { - $exprString = '$' . $parameterVariable; - foreach ($this->conditionalExpressions as $conditionalExprString => $holders) { - if ($exprString === $conditionalExprString) { - continue; - } - $newHolders = []; - foreach ($holders as $holder) { - foreach (array_keys($holder->getConditionExpressionTypes()) as $conditionalExprString2) { - if ($exprString === $conditionalExprString2) { - continue 2; - } - } - - $newHolders[] = $holder; - } - - if (count($newHolders) === 0) { - continue; - } - - $conditionalExpressions[$conditionalExprString] = $newHolders; - } - } - - $scope = $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->constantTypes, - $this->getFunction(), - $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, - $conditionalExpressions, - $this->inClosureBindScopeClass, - null, - true, - [], - [], - [], - [], - $this->afterExtractCall, - $this->parentScope, - ); - - foreach ($parameterVariableExpressions as $expr) { - $scope = $scope->invalidateExpression($expr); - } - - return $scope; - } - - public function isParameterValueNullable(Node\Param $parameter): bool - { - if ($parameter->default instanceof ConstFetch) { - return strtolower((string) $parameter->default->name) === 'null'; - } - - return false; - } + return false; + } /** * @api @@ -3122,11 +3434,11 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type )); } - return new ArrayType(new IntegerType(), $this->getFunctionType( + return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $this->getFunctionType( $type, false, false, - )); + ))); } if ($type instanceof Name) { @@ -3144,31 +3456,54 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type return ParserNodeTypeToPHPStanType::resolve($type, $this->isInClass() ? $this->getClassReflection() : null); } - public function enterForeach(Expr $iteratee, string $valueName, ?string $keyName): self + public function enterMatch(Expr\Match_ $expr): self { - $iterateeType = $this->getType($iteratee); - $nativeIterateeType = $this->getNativeType($iteratee); - $scope = $this->assignVariable($valueName, $iterateeType->getIterableValueType()); - $scope->nativeExpressionTypes[sprintf('$%s', $valueName)] = $nativeIterateeType->getIterableValueType(); + if ($expr->cond instanceof Variable) { + return $this; + } + if ($expr->cond instanceof AlwaysRememberedExpr) { + return $this; + } + + $type = $this->getType($expr->cond); + $nativeType = $this->getNativeType($expr->cond); + $condExpr = new AlwaysRememberedExpr($expr->cond, $type, $nativeType); + $expr->cond = $condExpr; + + return $this->assignExpression($condExpr, $type, $nativeType); + } + public function enterForeach(self $originalScope, Expr $iteratee, string $valueName, ?string $keyName): self + { + $iterateeType = $originalScope->getType($iteratee); + $nativeIterateeType = $originalScope->getNativeType($iteratee); + $scope = $this->assignVariable( + $valueName, + $originalScope->getIterableValueType($iterateeType), + $originalScope->getIterableValueType($nativeIterateeType), + ); if ($keyName !== null) { - $scope = $scope->enterForeachKey($iteratee, $keyName); + $scope = $scope->enterForeachKey($originalScope, $iteratee, $keyName); } return $scope; } - public function enterForeachKey(Expr $iteratee, string $keyName): self + public function enterForeachKey(self $originalScope, Expr $iteratee, string $keyName): self { - $iterateeType = $this->getType($iteratee); - $nativeIterateeType = $this->getNativeType($iteratee); - $scope = $this->assignVariable($keyName, $iterateeType->getIterableKeyType()); - $scope->nativeExpressionTypes[sprintf('$%s', $keyName)] = $nativeIterateeType->getIterableKeyType(); + $iterateeType = $originalScope->getType($iteratee); + $nativeIterateeType = $originalScope->getNativeType($iteratee); + $scope = $this->assignVariable( + $keyName, + $originalScope->getIterableKeyType($iterateeType), + $originalScope->getIterableKeyType($nativeIterateeType), + ); if ($iterateeType->isArray()->yes()) { - $scope = $scope->specifyExpressionType( + $scope = $scope->assignExpression( new Expr\ArrayDimFetch($iteratee, new Variable($keyName)), - $iterateeType->getIterableValueType(), + $originalScope->getIterableValueType($iterateeType), + $originalScope->getIterableValueType($nativeIterateeType), ); } @@ -3195,6 +3530,7 @@ public function enterCatchType(Type $catchType, ?string $variableName): self return $this->assignVariable( $variableName, TypeCombinator::intersect($catchType, new ObjectType(Throwable::class)), + TypeCombinator::intersect($catchType, new ObjectType(Throwable::class)), ); } @@ -3207,21 +3543,20 @@ public function enterExpressionAssign(Expr $expr): self $scope = $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $currentlyAssignedExpressions, $this->currentlyAllowedUndefinedExpressions, - $this->nativeExpressionTypes, [], $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); $scope->resolvedTypes = $this->resolvedTypes; $scope->truthyScopes = $this->truthyScopes; @@ -3239,21 +3574,20 @@ public function exitExpressionAssign(Expr $expr): self $scope = $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $currentlyAssignedExpressions, $this->currentlyAllowedUndefinedExpressions, - $this->nativeExpressionTypes, [], $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); $scope->resolvedTypes = $this->resolvedTypes; $scope->truthyScopes = $this->truthyScopes; @@ -3282,21 +3616,20 @@ public function setAllowedUndefinedExpression(Expr $expr): self $scope = $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $this->currentlyAssignedExpressions, $currentlyAllowedUndefinedExpressions, - $this->nativeExpressionTypes, [], $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); $scope->resolvedTypes = $this->resolvedTypes; $scope->truthyScopes = $this->truthyScopes; @@ -3314,21 +3647,20 @@ public function unsetAllowedUndefinedExpression(Expr $expr): self $scope = $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->isInFirstLevelStatement(), $this->currentlyAssignedExpressions, $currentlyAllowedUndefinedExpressions, - $this->nativeExpressionTypes, [], $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); $scope->resolvedTypes = $this->resolvedTypes; $scope->truthyScopes = $this->truthyScopes; @@ -3344,261 +3676,90 @@ public function isUndefinedExpressionAllowed(Expr $expr): bool return array_key_exists($exprString, $this->currentlyAllowedUndefinedExpressions); } - public function assignVariable(string $variableName, Type $type, ?TrinaryLogic $certainty = null): self + public function assignVariable(string $variableName, Type $type, Type $nativeType, ?TrinaryLogic $certainty = null): self { - if ($certainty === null) { - $certainty = TrinaryLogic::createYes(); - } elseif ($certainty->no()) { - throw new ShouldNotHappenException(); - } - $variableTypes = $this->getVariableTypes(); - $variableTypes[$variableName] = new VariableTypeHolder($type, $certainty); - - $nativeTypes = $this->nativeExpressionTypes; - $nativeTypes[sprintf('$%s', $variableName)] = $type; - - $variableString = $this->exprPrinter->printExpr(new Variable($variableName)); - $moreSpecificTypeHolders = $this->moreSpecificTypes; - foreach (array_keys($moreSpecificTypeHolders) as $key) { - $matches = Strings::matchAll((string) $key, '#\$[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*#'); - - if ($matches === []) { - continue; - } - - $matches = array_column($matches, 0); - - if (!in_array($variableString, $matches, true)) { - continue; + $node = new Variable($variableName); + $scope = $this->assignExpression($node, $type, $nativeType); + if ($certainty !== null) { + if ($certainty->no()) { + throw new ShouldNotHappenException(); + } elseif (!$certainty->yes()) { + $exprString = '$' . $variableName; + $scope->expressionTypes[$exprString] = new ExpressionTypeHolder($node, $type, $certainty); + $scope->nativeExpressionTypes[$exprString] = new ExpressionTypeHolder($node, $nativeType, $certainty); } - - unset($moreSpecificTypeHolders[$key]); } - $conditionalExpressions = []; - foreach ($this->conditionalExpressions as $exprString => $holders) { - $exprVariableName = '$' . $variableName; - if ($exprString === $exprVariableName) { - continue; - } - - foreach ($holders as $holder) { - foreach (array_keys($holder->getConditionExpressionTypes()) as $conditionExprString) { - if ($conditionExprString === $exprVariableName) { - continue 3; - } - } - } - - $conditionalExpressions[$exprString] = $holders; - } + $parameterOriginalValueExprString = $this->getNodeKey(new ParameterVariableOriginalValueExpr($variableName)); + unset($scope->expressionTypes[$parameterOriginalValueExprString]); + unset($scope->nativeExpressionTypes[$parameterOriginalValueExprString]); - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->constantTypes, - $this->getFunction(), - $this->getNamespace(), - $variableTypes, - $moreSpecificTypeHolders, - $conditionalExpressions, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $this->currentlyAllowedUndefinedExpressions, - $nativeTypes, - $this->inFunctionCallsStack, - $this->afterExtractCall, - $this->parentScope, - ); + return $scope; } public function unsetExpression(Expr $expr): self { - if ($expr instanceof Variable && is_string($expr->name)) { - if ($this->hasVariableType($expr->name)->no()) { - return $this; - } - $variableTypes = $this->getVariableTypes(); - unset($variableTypes[$expr->name]); - $nativeTypes = $this->nativeExpressionTypes; - - $exprString = sprintf('$%s', $expr->name); - unset($nativeTypes[$exprString]); - - $conditionalExpressions = $this->conditionalExpressions; - unset($conditionalExpressions[$exprString]); - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->constantTypes, - $this->getFunction(), - $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, - $conditionalExpressions, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - [], - [], - $nativeTypes, - [], - $this->afterExtractCall, - $this->parentScope, - ); - } elseif ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { - $exprVarType = $this->getType($expr->var); - $dimType = $this->getType($expr->dim); + $scope = $this; + if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { + $exprVarType = $scope->getType($expr->var); + $dimType = $scope->getType($expr->dim); $unsetType = $exprVarType->unsetOffset($dimType); - $scope = $this->specifyExpressionType($expr->var, $unsetType)->invalidateExpression( + $exprVarNativeType = $scope->getNativeType($expr->var); + $dimNativeType = $scope->getNativeType($expr->dim); + $unsetNativeType = $exprVarNativeType->unsetOffset($dimNativeType); + $scope = $scope->assignExpression($expr->var, $unsetType, $unsetNativeType)->invalidateExpression( new FuncCall(new FullyQualified('count'), [new Arg($expr->var)]), )->invalidateExpression( new FuncCall(new FullyQualified('sizeof'), [new Arg($expr->var)]), + )->invalidateExpression( + new FuncCall(new Name('count'), [new Arg($expr->var)]), + )->invalidateExpression( + new FuncCall(new Name('sizeof'), [new Arg($expr->var)]), ); if ($expr->var instanceof Expr\ArrayDimFetch && $expr->var->dim !== null) { - $scope = $scope->specifyExpressionType( + $scope = $scope->assignExpression( $expr->var->var, - $scope->getType($expr->var->var)->setOffsetValueType( + $this->getType($expr->var->var)->setOffsetValueType( $scope->getType($expr->var->dim), $scope->getType($expr->var), ), + $this->getNativeType($expr->var->var)->setOffsetValueType( + $scope->getNativeType($expr->var->dim), + $scope->getNativeType($expr->var), + ), ); } - - return $scope; } - return $this; + return $scope->invalidateExpression($expr); } - private function specifyExpressionType(Expr $expr, Type $type, ?Type $nativeType = null): self + public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, ?TrinaryLogic $certainty = null): self { - if ($expr instanceof Node\Scalar || $expr instanceof Array_) { - return $this; - } - if ($expr instanceof ConstFetch) { - $constantTypes = $this->constantTypes; - $constantName = new FullyQualified($expr->name->toString()); - - if ($type instanceof NeverType) { - unset($constantTypes[$constantName->toCodeString()]); - } else { - $constantTypes[$constantName->toCodeString()] = $type; - } - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $constantTypes, - $this->getFunction(), - $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, - $this->conditionalExpressions, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $this->currentlyAllowedUndefinedExpressions, - $this->nativeExpressionTypes, - $this->inFunctionCallsStack, - $this->afterExtractCall, - $this->parentScope, - ); - } - - $exprString = $this->getNodeKey($expr); - - $scope = $this; - - if ($expr instanceof Variable && is_string($expr->name)) { - $variableName = $expr->name; - - $variableTypes = $this->getVariableTypes(); - $variableTypes[$variableName] = VariableTypeHolder::createYes($type); - - $nativeTypes = $this->nativeExpressionTypes; - $exprString = sprintf('$%s', $variableName); - if ($nativeType !== null) { - $nativeTypes[$exprString] = $nativeType; + $loweredConstName = strtolower($expr->name->toString()); + if (in_array($loweredConstName, ['true', 'false', 'null'], true)) { + return $this; } + } - $conditionalExpressions = []; - foreach ($this->conditionalExpressions as $conditionalExprString => $holders) { - if ($conditionalExprString === $exprString) { - continue; - } - - foreach ($holders as $holder) { - foreach (array_keys($holder->getConditionExpressionTypes()) as $conditionExprString2) { - if ($conditionExprString2 === $exprString) { - continue 3; - } - } - } - - $conditionalExpressions[$conditionalExprString] = $holders; + if ($expr instanceof FuncCall && $expr->name instanceof Name && $type->isFalse()->yes()) { + $functionName = $this->reflectionProvider->resolveFunctionName($expr->name, $this); + if ($functionName !== null && in_array(strtolower($functionName), [ + 'is_dir', + 'is_file', + 'file_exists', + ], true)) { + return $this; } + } - $moreSpecificTypeHolders = $this->moreSpecificTypes; - foreach ($moreSpecificTypeHolders as $specifiedExprString => $specificTypeHolder) { - if (!$specificTypeHolder->getCertainty()->yes()) { - continue; - } - - $specifiedExprString = (string) $specifiedExprString; - $specifiedExpr = $this->exprStringToExpr($specifiedExprString); - if ($specifiedExpr === null) { - continue; - } - if (!$specifiedExpr instanceof Expr\ArrayDimFetch) { - continue; - } - - if (!$specifiedExpr->dim instanceof Variable) { - continue; - } - - if (!is_string($specifiedExpr->dim->name)) { - continue; - } - - if ($specifiedExpr->dim->name !== $variableName) { - continue; - } - - $moreSpecificTypeHolders[$specifiedExprString] = VariableTypeHolder::createYes($this->getType($specifiedExpr->var)->getOffsetValueType($type)); - unset($nativeTypes[$specifiedExprString]); - } - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->constantTypes, - $this->getFunction(), - $this->getNamespace(), - $variableTypes, - $moreSpecificTypeHolders, - $conditionalExpressions, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $this->currentlyAllowedUndefinedExpressions, - $nativeTypes, - $this->inFunctionCallsStack, - $this->afterExtractCall, - $this->parentScope, - ); - } elseif ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { - $dimType = ArrayType::castToArrayKeyType($this->getType($expr->dim)); + $scope = $this; + if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { + $dimType = $scope->getType($expr->dim)->toArrayKey(); if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) { - $exprVarType = $this->getType($expr->var); + $exprVarType = $scope->getType($expr->var); if (!$exprVarType instanceof MixedType && !$exprVarType->isArray()->no()) { $types = [ new ArrayType(new MixedType(), new MixedType()), @@ -3608,88 +3769,132 @@ private function specifyExpressionType(Expr $expr, Type $type, ?Type $nativeType if ($dimType instanceof ConstantIntegerType) { $types[] = new StringType(); } - $scope = $this->specifyExpressionType( + + $scope = $scope->specifyExpressionType( $expr->var, TypeCombinator::intersect( TypeCombinator::intersect($exprVarType, TypeCombinator::union(...$types)), new HasOffsetValueType($dimType, $type), ), + $scope->getNativeType($expr->var), + $certainty, ); } } } - if ($expr instanceof FuncCall && $expr->name instanceof Name && $type instanceof ConstantBooleanType && !$type->getValue()) { - $functionName = $this->reflectionProvider->resolveFunctionName($expr->name, $this); - if ($functionName !== null && in_array(strtolower($functionName), [ - 'is_dir', - 'is_file', - 'file_exists', - ], true)) { - return $this; - } + if ($certainty === null) { + $certainty = TrinaryLogic::createYes(); + } elseif ($certainty->no()) { + throw new ShouldNotHappenException(); } - return $scope->addMoreSpecificTypes([ - $exprString => $type, - ]); + $exprString = $this->getNodeKey($expr); + $expressionTypes = $scope->expressionTypes; + $expressionTypes[$exprString] = new ExpressionTypeHolder($expr, $type, $certainty); + $nativeTypes = $scope->nativeExpressionTypes; + $nativeTypes[$exprString] = new ExpressionTypeHolder($expr, $nativeType, $certainty); + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $expressionTypes, + $nativeTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + $this->currentlyAssignedExpressions, + $this->currentlyAllowedUndefinedExpressions, + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); } public function assignExpression(Expr $expr, Type $type, ?Type $nativeType = null): self { + if ($nativeType === null) { + $nativeType = new MixedType(); + } $scope = $this; if ($expr instanceof PropertyFetch) { $scope = $this->invalidateExpression($expr) ->invalidateMethodsOnExpression($expr->var); } elseif ($expr instanceof Expr\StaticPropertyFetch) { $scope = $this->invalidateExpression($expr); + } elseif ($expr instanceof Variable) { + $scope = $this->invalidateExpression($expr); } return $scope->specifyExpressionType($expr, $type, $nativeType); } + public function assignInitializedProperty(Type $fetchedOnType, string $propertyName): self + { + if (!$this->isInClass()) { + return $this; + } + + if (TypeUtils::findThisType($fetchedOnType) === null) { + return $this; + } + + $propertyReflection = $this->getPropertyReflection($fetchedOnType, $propertyName); + if ($propertyReflection === null) { + return $this; + } + $declaringClass = $propertyReflection->getDeclaringClass(); + if ($this->getClassReflection()->getName() !== $declaringClass->getName()) { + return $this; + } + if (!$declaringClass->hasNativeProperty($propertyName)) { + return $this; + } + + return $this->assignExpression(new PropertyInitializationExpr($propertyName), new MixedType(), new MixedType()); + } + public function invalidateExpression(Expr $expressionToInvalidate, bool $requireMoreCharacters = false): self { - $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate); - $expressionToInvalidateClass = get_class($expressionToInvalidate); - $moreSpecificTypeHolders = $this->moreSpecificTypes; + $expressionTypes = $this->expressionTypes; $nativeExpressionTypes = $this->nativeExpressionTypes; $invalidated = false; - $nodeFinder = new NodeFinder(); - foreach (array_keys($moreSpecificTypeHolders) as $exprString) { - $exprString = (string) $exprString; - $exprExpr = $this->exprStringToExpr($exprString); - if ($exprExpr === null) { + $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate); + + foreach ($expressionTypes as $exprString => $exprTypeHolder) { + $exprExpr = $exprTypeHolder->getExpr(); + if (!$this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $exprExpr, $requireMoreCharacters)) { continue; } - if ($exprExpr instanceof PropertyFetch) { - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($exprExpr, $this); - if ($propertyReflection !== null) { - $nativePropertyReflection = $propertyReflection->getNativeReflection(); - if ($nativePropertyReflection !== null && $nativePropertyReflection->isReadOnly()) { - continue; - } - } - } - $found = $nodeFinder->findFirst([$exprExpr], function (Node $node) use ($expressionToInvalidateClass, $exprStringToInvalidate): bool { - if (!$node instanceof $expressionToInvalidateClass) { - return false; - } + unset($expressionTypes[$exprString]); + unset($nativeExpressionTypes[$exprString]); + $invalidated = true; + } - return $this->getNodeKey($node) === $exprStringToInvalidate; - }); - if ($found === null) { + $newConditionalExpressions = []; + foreach ($this->conditionalExpressions as $conditionalExprString => $holders) { + if (count($holders) === 0) { continue; } - - if ($requireMoreCharacters && $exprString === $exprStringToInvalidate) { + if ($this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $holders[array_key_first($holders)]->getTypeHolder()->getExpr())) { + $invalidated = true; continue; } - - unset($moreSpecificTypeHolders[$exprString]); - unset($nativeExpressionTypes[$exprString]); - $invalidated = true; + foreach ($holders as $holder) { + $conditionalTypeHolders = $holder->getConditionExpressionTypeHolders(); + foreach ($conditionalTypeHolders as $conditionalTypeHolder) { + if ($this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $conditionalTypeHolder->getExpr())) { + $invalidated = true; + continue 3; + } + } + } + $newConditionalExpressions[$conditionalExprString] = $holders; } if (!$invalidated) { @@ -3699,57 +3904,73 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $moreSpecificTypeHolders, - $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $expressionTypes, + $nativeExpressionTypes, + $newConditionalExpressions, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, $this->currentlyAssignedExpressions, $this->currentlyAllowedUndefinedExpressions, - $nativeExpressionTypes, [], $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); } - private function exprStringToExpr(string $exprString): ?Expr + private function shouldInvalidateExpression(string $exprStringToInvalidate, Expr $exprToInvalidate, Expr $expr, bool $requireMoreCharacters = false): bool { - try { - $expr = $this->parser->parseString('getNodeKey($expr)) { + return false; } - if (!$expr instanceof Node\Stmt\Expression) { - throw new ShouldNotHappenException(); + + // Variables will not contain traversable expressions. skip the NodeFinder overhead + if ($expr instanceof Variable && is_string($expr->name) && !$requireMoreCharacters) { + return $exprStringToInvalidate === $this->getNodeKey($expr); } - return $expr->expr; + $nodeFinder = new NodeFinder(); + $expressionToInvalidateClass = get_class($exprToInvalidate); + $found = $nodeFinder->findFirst([$expr], function (Node $node) use ($expressionToInvalidateClass, $exprStringToInvalidate): bool { + if (!$node instanceof $expressionToInvalidateClass) { + return false; + } + + $nodeString = $this->getNodeKey($node); + + return $nodeString === $exprStringToInvalidate; + }); + + if ($found === null) { + return false; + } + + if ($this->phpVersion->supportsReadOnlyProperties() && $expr instanceof PropertyFetch && $expr->name instanceof Node\Identifier && $requireMoreCharacters) { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this); + if ($propertyReflection !== null) { + $nativePropertyReflection = $propertyReflection->getNativeReflection(); + if ($nativePropertyReflection !== null && $nativePropertyReflection->isReadOnly()) { + return false; + } + } + } + + return true; } private function invalidateMethodsOnExpression(Expr $expressionToInvalidate): self { $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate); - $moreSpecificTypeHolders = $this->moreSpecificTypes; + $expressionTypes = $this->expressionTypes; $nativeExpressionTypes = $this->nativeExpressionTypes; $invalidated = false; $nodeFinder = new NodeFinder(); - foreach (array_keys($moreSpecificTypeHolders) as $exprString) { - $exprString = (string) $exprString; - - try { - $expr = $this->parser->parseString('findFirst([$expr->expr], function (Node $node) use ($exprStringToInvalidate): bool { + foreach ($expressionTypes as $exprString => $exprTypeHolder) { + $expr = $exprTypeHolder->getExpr(); + $found = $nodeFinder->findFirst([$expr], function (Node $node) use ($exprStringToInvalidate): bool { if (!$node instanceof MethodCall) { return false; } @@ -3760,7 +3981,7 @@ private function invalidateMethodsOnExpression(Expr $expressionToInvalidate): se continue; } - unset($moreSpecificTypeHolders[$exprString]); + unset($expressionTypes[$exprString]); unset($nativeExpressionTypes[$exprString]); $invalidated = true; } @@ -3772,21 +3993,37 @@ private function invalidateMethodsOnExpression(Expr $expressionToInvalidate): se return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $moreSpecificTypeHolders, + $expressionTypes, + $nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, $this->currentlyAssignedExpressions, $this->currentlyAllowedUndefinedExpressions, - $nativeExpressionTypes, [], $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, + ); + } + + private function setExpressionCertainty(Expr $expr, TrinaryLogic $certainty): self + { + if ($this->hasExpressionType($expr)->no()) { + throw new ShouldNotHappenException(); + } + + $originalExprType = $this->getType($expr); + $nativeType = $this->getNativeType($expr); + + return $this->specifyExpressionType( + $expr, + $originalExprType, + $nativeType, + $certainty, ); } @@ -3863,157 +4100,119 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self { $typeSpecifications = []; foreach ($specifiedTypes->getSureTypes() as $exprString => [$expr, $type]) { + if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) { + continue; + } $typeSpecifications[] = [ 'sure' => true, - 'exprString' => $exprString, + 'exprString' => (string) $exprString, 'expr' => $expr, 'type' => $type, ]; } foreach ($specifiedTypes->getSureNotTypes() as $exprString => [$expr, $type]) { + if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) { + continue; + } $typeSpecifications[] = [ 'sure' => false, - 'exprString' => $exprString, + 'exprString' => (string) $exprString, 'expr' => $expr, 'type' => $type, ]; } usort($typeSpecifications, static function (array $a, array $b): int { - // @phpstan-ignore-next-line - $length = strlen((string) $a['exprString']) - strlen((string) $b['exprString']); + $length = strlen($a['exprString']) - strlen($b['exprString']); if ($length !== 0) { return $length; } - return $b['sure'] - $a['sure']; // @phpstan-ignore-line + return $b['sure'] - $a['sure']; // @phpstan-ignore minus.leftNonNumeric, minus.rightNonNumeric }); $scope = $this; - $typeGuards = []; - $skipVariables = []; - $saveConditionalVariables = []; + $specifiedExpressions = []; foreach ($typeSpecifications as $typeSpecification) { $expr = $typeSpecification['expr']; $type = $typeSpecification['type']; - if ($typeSpecification['sure']) { - if ($specifiedTypes->shouldOverwrite()) { - $scope = $scope->specifyExpressionType($expr, $type, $type); - } else { - $scope = $scope->addTypeToExpression($expr, $type); - } - } else { - $scope = $scope->removeTypeFromExpression($expr, $type); - } - - if ( - !$expr instanceof Variable - || !is_string($expr->name) - || $specifiedTypes->shouldOverwrite() - ) { - // @phpstan-ignore-next-line - $match = Strings::match((string) $typeSpecification['exprString'], '#^\$([a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*)#'); - if ($match !== null) { - $skipVariables[$match[1]] = true; - } - continue; - } - - if ($scope->hasVariableType($expr->name)->no()) { - continue; - } - - $saveConditionalVariables[$expr->name] = $scope->getVariableType($expr->name); - } - - foreach ($saveConditionalVariables as $variableName => $typeGuard) { - if (array_key_exists($variableName, $skipVariables)) { - continue; - } - - $typeGuards['$' . $variableName] = $typeGuard; - } - - $newConditionalExpressions = $specifiedTypes->getNewConditionalExpressionHolders(); - foreach ($this->conditionalExpressions as $variableExprString => $conditionalExpressions) { - if (array_key_exists($variableExprString, $typeGuards)) { - continue; - } - - $typeHolder = null; - - $variableName = substr($variableExprString, 1); - foreach ($conditionalExpressions as $conditionalExpression) { - $matchingConditions = []; - foreach ($conditionalExpression->getConditionExpressionTypes() as $conditionExprString => $conditionalType) { - if (!array_key_exists($conditionExprString, $typeGuards)) { - continue; - } - - if (!$typeGuards[$conditionExprString]->equals($conditionalType)) { - continue; - } - $matchingConditions[$conditionExprString] = $conditionalType; - } + if ($expr instanceof IssetExpr) { + $issetExpr = $expr; + $expr = $issetExpr->getExpr(); - if (count($matchingConditions) === 0) { - $newConditionalExpressions[$variableExprString][$conditionalExpression->getKey()] = $conditionalExpression; - continue; + if ($typeSpecification['sure']) { + $scope = $scope->setExpressionCertainty( + $expr, + TrinaryLogic::createMaybe(), + ); + } else { + $scope = $scope->unsetExpression($expr); } - if (count($matchingConditions) < count($conditionalExpression->getConditionExpressionTypes())) { - $filteredConditions = $conditionalExpression->getConditionExpressionTypes(); - foreach (array_keys($matchingConditions) as $conditionExprString) { - unset($filteredConditions[$conditionExprString]); - } + continue; + } - $holder = new ConditionalExpressionHolder($filteredConditions, $conditionalExpression->getTypeHolder()); - $newConditionalExpressions[$variableExprString][$holder->getKey()] = $holder; - continue; + if ($typeSpecification['sure']) { + if ($specifiedTypes->shouldOverwrite()) { + $scope = $scope->assignExpression($expr, $type, $type); + } else { + $scope = $scope->addTypeToExpression($expr, $type); } - - $typeHolder = $conditionalExpression->getTypeHolder(); - break; + } else { + $scope = $scope->removeTypeFromExpression($expr, $type); } + $specifiedExpressions[$this->getNodeKey($expr)] = ExpressionTypeHolder::createYes($expr, $scope->getType($expr)); + } - if ($typeHolder === null) { - continue; + $conditions = []; + foreach ($scope->conditionalExpressions as $conditionalExprString => $conditionalExpressions) { + foreach ($conditionalExpressions as $conditionalExpression) { + foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { + if (!array_key_exists($holderExprString, $specifiedExpressions) || !$specifiedExpressions[$holderExprString]->equals($conditionalTypeHolder)) { + continue 2; + } + } + + $conditions[$conditionalExprString][] = $conditionalExpression; + $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); } + } - if ($typeHolder->getCertainty()->no()) { - unset($scope->variableTypes[$variableName]); + foreach ($conditions as $conditionalExprString => $expressions) { + $certainty = TrinaryLogic::lazyExtremeIdentity($expressions, static fn (ConditionalExpressionHolder $holder) => $holder->getTypeHolder()->getCertainty()); + if ($certainty->no()) { + unset($scope->expressionTypes[$conditionalExprString]); } else { - $scope->variableTypes[$variableName] = $typeHolder; + $type = TypeCombinator::intersect(...array_map(static fn (ConditionalExpressionHolder $holder) => $holder->getTypeHolder()->getType(), $expressions)); + + $scope->expressionTypes[$conditionalExprString] = array_key_exists($conditionalExprString, $scope->expressionTypes) + ? new ExpressionTypeHolder( + $scope->expressionTypes[$conditionalExprString]->getExpr(), + TypeCombinator::intersect($scope->expressionTypes[$conditionalExprString]->getType(), $type), + TrinaryLogic::maxMin($scope->expressionTypes[$conditionalExprString]->getCertainty(), $certainty), + ) + : $expressions[0]->getTypeHolder(); } } - return $scope->changeConditionalExpressions($newConditionalExpressions); - } - - /** - * @param array $newConditionalExpressionHolders - */ - public function changeConditionalExpressions(array $newConditionalExpressionHolders): self - { - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->constantTypes, - $this->getFunction(), - $this->getNamespace(), - $this->variableTypes, - $this->moreSpecificTypes, - $newConditionalExpressionHolders, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $this->currentlyAllowedUndefinedExpressions, - $this->nativeExpressionTypes, - $this->inFunctionCallsStack, - $this->afterExtractCall, - $this->parentScope, + return $scope->scopeFactory->create( + $scope->context, + $scope->isDeclareStrictTypes(), + $scope->getFunction(), + $scope->getNamespace(), + $scope->expressionTypes, + $scope->nativeExpressionTypes, + array_merge($specifiedTypes->getNewConditionalExpressionHolders(), $scope->conditionalExpressions), + $scope->inClosureBindScopeClasses, + $scope->anonymousFunctionReflection, + $scope->inFirstLevelStatement, + $scope->currentlyAssignedExpressions, + $scope->currentlyAllowedUndefinedExpressions, + $scope->inFunctionCallsStack, + $scope->afterExtractCall, + $scope->parentScope, + $scope->nativeTypesPromoted, ); } @@ -4027,21 +4226,20 @@ public function addConditionalExpressions(string $exprString, array $conditional return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->variableTypes, - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, $conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, $this->currentlyAssignedExpressions, $this->currentlyAllowedUndefinedExpressions, - $this->nativeExpressionTypes, $this->inFunctionCallsStack, $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); } @@ -4058,21 +4256,20 @@ public function exitFirstLevelStatements(): self $scope = $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, false, $this->currentlyAssignedExpressions, $this->currentlyAllowedUndefinedExpressions, - $this->nativeExpressionTypes, $this->inFunctionCallsStack, $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); $scope->resolvedTypes = $this->resolvedTypes; $scope->truthyScopes = $this->truthyScopes; @@ -4088,107 +4285,45 @@ public function isInFirstLevelStatement(): bool return $this->inFirstLevelStatement; } - /** - * @phpcsSuppress SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedMethod - * @param Type[] $types - */ - private function addMoreSpecificTypes(array $types): self - { - $moreSpecificTypeHolders = $this->moreSpecificTypes; - foreach ($types as $exprString => $type) { - $moreSpecificTypeHolders[$exprString] = VariableTypeHolder::createYes($type); - } - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->constantTypes, - $this->getFunction(), - $this->getNamespace(), - $this->getVariableTypes(), - $moreSpecificTypeHolders, - $this->conditionalExpressions, - $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, - $this->currentlyAssignedExpressions, - $this->currentlyAllowedUndefinedExpressions, - $this->nativeExpressionTypes, - [], - $this->afterExtractCall, - $this->parentScope, - ); - } - public function mergeWith(?self $otherScope): self { if ($otherScope === null) { return $this; } + $ourExpressionTypes = $this->expressionTypes; + $theirExpressionTypes = $otherScope->expressionTypes; - $variableHolderToType = static fn (VariableTypeHolder $holder): Type => $holder->getType(); - $typeToVariableHolder = static fn (Type $type): VariableTypeHolder => new VariableTypeHolder($type, TrinaryLogic::createYes()); - - $filterVariableHolders = static fn (VariableTypeHolder $holder): bool => $holder->getCertainty()->yes(); - - $ourVariableTypes = $this->getVariableTypes(); - $theirVariableTypes = $otherScope->getVariableTypes(); - if ($this->canAnyVariableExist()) { - foreach (array_keys($theirVariableTypes) as $name) { - if (array_key_exists($name, $ourVariableTypes)) { - continue; - } - - $ourVariableTypes[$name] = VariableTypeHolder::createMaybe(new MixedType()); - } - - foreach (array_keys($ourVariableTypes) as $name) { - if (array_key_exists($name, $theirVariableTypes)) { - continue; - } - - $theirVariableTypes[$name] = VariableTypeHolder::createMaybe(new MixedType()); - } - } - - $mergedVariableHolders = $this->mergeVariableHolders($ourVariableTypes, $theirVariableTypes); + $mergedExpressionTypes = $this->mergeVariableHolders($ourExpressionTypes, $theirExpressionTypes); $conditionalExpressions = $this->intersectConditionalExpressions($otherScope->conditionalExpressions); $conditionalExpressions = $this->createConditionalExpressions( $conditionalExpressions, - $ourVariableTypes, - $theirVariableTypes, - $mergedVariableHolders, + $ourExpressionTypes, + $theirExpressionTypes, + $mergedExpressionTypes, ); $conditionalExpressions = $this->createConditionalExpressions( $conditionalExpressions, - $theirVariableTypes, - $ourVariableTypes, - $mergedVariableHolders, + $theirExpressionTypes, + $ourExpressionTypes, + $mergedExpressionTypes, ); return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - array_map($variableHolderToType, array_filter($this->mergeVariableHolders( - array_map($typeToVariableHolder, $this->constantTypes), - array_map($typeToVariableHolder, $otherScope->constantTypes), - ), $filterVariableHolders)), $this->getFunction(), $this->getNamespace(), - $mergedVariableHolders, - $this->mergeVariableHolders($this->moreSpecificTypes, $otherScope->moreSpecificTypes), + $mergedExpressionTypes, + $this->mergeVariableHolders($this->nativeExpressionTypes, $otherScope->nativeExpressionTypes), $conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, [], [], - array_map($variableHolderToType, array_filter($this->mergeVariableHolders( - array_map($typeToVariableHolder, $this->nativeExpressionTypes), - array_map($typeToVariableHolder, $otherScope->nativeExpressionTypes), - ), $filterVariableHolders)), [], $this->afterExtractCall && $otherScope->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); } @@ -4219,59 +4354,58 @@ private function intersectConditionalExpressions(array $otherConditionalExpressi /** * @param array $conditionalExpressions - * @param array $variableTypes - * @param array $theirVariableTypes - * @param array $mergedVariableHolders + * @param array $ourExpressionTypes + * @param array $theirExpressionTypes + * @param array $mergedExpressionTypes * @return array */ private function createConditionalExpressions( array $conditionalExpressions, - array $variableTypes, - array $theirVariableTypes, - array $mergedVariableHolders, + array $ourExpressionTypes, + array $theirExpressionTypes, + array $mergedExpressionTypes, ): array { - $newVariableTypes = $variableTypes; - foreach ($theirVariableTypes as $name => $holder) { - if (!array_key_exists($name, $mergedVariableHolders)) { + $newVariableTypes = $ourExpressionTypes; + foreach ($theirExpressionTypes as $exprString => $holder) { + if (!array_key_exists($exprString, $mergedExpressionTypes)) { continue; } - if (!$mergedVariableHolders[$name]->getType()->equals($holder->getType())) { + if (!$mergedExpressionTypes[$exprString]->getType()->equals($holder->getType())) { continue; } - unset($newVariableTypes[$name]); + unset($newVariableTypes[$exprString]); } $typeGuards = []; - foreach ($newVariableTypes as $name => $holder) { + foreach ($newVariableTypes as $exprString => $holder) { if (!$holder->getCertainty()->yes()) { continue; } - if (!array_key_exists($name, $mergedVariableHolders)) { + if (!array_key_exists($exprString, $mergedExpressionTypes)) { continue; } - if ($mergedVariableHolders[$name]->getType()->equals($holder->getType())) { + if ($mergedExpressionTypes[$exprString]->getType()->equals($holder->getType())) { continue; } - $typeGuards['$' . $name] = $holder->getType(); + $typeGuards[$exprString] = $holder; } if (count($typeGuards) === 0) { return $conditionalExpressions; } - foreach ($newVariableTypes as $name => $holder) { + foreach ($newVariableTypes as $exprString => $holder) { if ( - array_key_exists($name, $mergedVariableHolders) - && $mergedVariableHolders[$name]->equals($holder) + array_key_exists($exprString, $mergedExpressionTypes) + && $mergedExpressionTypes[$exprString]->equals($holder) ) { continue; } - $exprString = '$' . $name; $variableTypeGuards = $typeGuards; unset($variableTypeGuards[$exprString]); @@ -4283,93 +4417,108 @@ private function createConditionalExpressions( $conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression; } - foreach (array_keys($mergedVariableHolders) as $name) { - if (array_key_exists($name, $variableTypes)) { + foreach ($mergedExpressionTypes as $exprString => $mergedExprTypeHolder) { + if (array_key_exists($exprString, $ourExpressionTypes)) { continue; } - $conditionalExpression = new ConditionalExpressionHolder($typeGuards, new VariableTypeHolder(new ErrorType(), TrinaryLogic::createNo())); - $conditionalExpressions['$' . $name][$conditionalExpression->getKey()] = $conditionalExpression; + $conditionalExpression = new ConditionalExpressionHolder($typeGuards, new ExpressionTypeHolder($mergedExprTypeHolder->getExpr(), new ErrorType(), TrinaryLogic::createNo())); + $conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression; } return $conditionalExpressions; } /** - * @param VariableTypeHolder[] $ourVariableTypeHolders - * @param VariableTypeHolder[] $theirVariableTypeHolders - * @return VariableTypeHolder[] + * @param array $ourVariableTypeHolders + * @param array $theirVariableTypeHolders + * @return array */ private function mergeVariableHolders(array $ourVariableTypeHolders, array $theirVariableTypeHolders): array { $intersectedVariableTypeHolders = []; - foreach ($ourVariableTypeHolders as $name => $variableTypeHolder) { - if (isset($theirVariableTypeHolders[$name])) { - $intersectedVariableTypeHolders[$name] = $variableTypeHolder->and($theirVariableTypeHolders[$name]); + foreach ($ourVariableTypeHolders as $exprString => $variableTypeHolder) { + if (isset($theirVariableTypeHolders[$exprString])) { + $intersectedVariableTypeHolders[$exprString] = $variableTypeHolder->and($theirVariableTypeHolders[$exprString]); } else { - $intersectedVariableTypeHolders[$name] = VariableTypeHolder::createMaybe($variableTypeHolder->getType()); + $intersectedVariableTypeHolders[$exprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); } } - foreach ($theirVariableTypeHolders as $name => $variableTypeHolder) { - if (isset($intersectedVariableTypeHolders[$name])) { + foreach ($theirVariableTypeHolders as $exprString => $variableTypeHolder) { + if (isset($intersectedVariableTypeHolders[$exprString])) { continue; } - $intersectedVariableTypeHolders[$name] = VariableTypeHolder::createMaybe($variableTypeHolder->getType()); + $intersectedVariableTypeHolders[$exprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); } return $intersectedVariableTypeHolders; } - public function processFinallyScope(self $finallyScope, self $originalFinallyScope): self + public function mergeInitializedProperties(self $calledMethodScope): self { - $variableHolderToType = static fn (VariableTypeHolder $holder): Type => $holder->getType(); - $typeToVariableHolder = static fn (Type $type): VariableTypeHolder => new VariableTypeHolder($type, TrinaryLogic::createYes()); - $filterVariableHolders = static fn (VariableTypeHolder $holder): bool => $holder->getCertainty()->yes(); + $scope = $this; + foreach ($calledMethodScope->expressionTypes as $exprString => $typeHolder) { + $exprString = (string) $exprString; + if (!str_starts_with($exprString, '__phpstanPropertyInitialization(')) { + continue; + } + $propertyName = substr($exprString, strlen('__phpstanPropertyInitialization('), -1); + $propertyExpr = new PropertyInitializationExpr($propertyName); + if (!array_key_exists($exprString, $scope->expressionTypes)) { + $scope = $scope->assignExpression($propertyExpr, new MixedType(), new MixedType()); + $scope->expressionTypes[$exprString] = $typeHolder; + continue; + } + + $certainty = $scope->expressionTypes[$exprString]->getCertainty(); + $scope = $scope->assignExpression($propertyExpr, new MixedType(), new MixedType()); + $scope->expressionTypes[$exprString] = new ExpressionTypeHolder( + $typeHolder->getExpr(), + $typeHolder->getType(), + $typeHolder->getCertainty()->or($certainty), + ); + } + + return $scope; + } + public function processFinallyScope(self $finallyScope, self $originalFinallyScope): self + { return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - array_map($variableHolderToType, array_filter($this->processFinallyScopeVariableTypeHolders( - array_map($typeToVariableHolder, $this->constantTypes), - array_map($typeToVariableHolder, $finallyScope->constantTypes), - array_map($typeToVariableHolder, $originalFinallyScope->constantTypes), - ), $filterVariableHolders)), $this->getFunction(), $this->getNamespace(), $this->processFinallyScopeVariableTypeHolders( - $this->getVariableTypes(), - $finallyScope->getVariableTypes(), - $originalFinallyScope->getVariableTypes(), + $this->expressionTypes, + $finallyScope->expressionTypes, + $originalFinallyScope->expressionTypes, ), $this->processFinallyScopeVariableTypeHolders( - $this->moreSpecificTypes, - $finallyScope->moreSpecificTypes, - $originalFinallyScope->moreSpecificTypes, + $this->nativeExpressionTypes, + $finallyScope->nativeExpressionTypes, + $originalFinallyScope->nativeExpressionTypes, ), $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, [], [], - array_map($variableHolderToType, array_filter($this->processFinallyScopeVariableTypeHolders( - array_map($typeToVariableHolder, $this->nativeExpressionTypes), - array_map($typeToVariableHolder, $finallyScope->nativeExpressionTypes), - array_map($typeToVariableHolder, $originalFinallyScope->nativeExpressionTypes), - ), $filterVariableHolders)), [], $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); } /** - * @param VariableTypeHolder[] $ourVariableTypeHolders - * @param VariableTypeHolder[] $finallyVariableTypeHolders - * @param VariableTypeHolder[] $originalVariableTypeHolders - * @return VariableTypeHolder[] + * @param array $ourVariableTypeHolders + * @param array $finallyVariableTypeHolders + * @param array $originalVariableTypeHolders + * @return array */ private function processFinallyScopeVariableTypeHolders( array $ourVariableTypeHolders, @@ -4377,20 +4526,20 @@ private function processFinallyScopeVariableTypeHolders( array $originalVariableTypeHolders, ): array { - foreach ($finallyVariableTypeHolders as $name => $variableTypeHolder) { + foreach ($finallyVariableTypeHolders as $exprString => $variableTypeHolder) { if ( - isset($originalVariableTypeHolders[$name]) - && !$originalVariableTypeHolders[$name]->getType()->equals($variableTypeHolder->getType()) + isset($originalVariableTypeHolders[$exprString]) + && !$originalVariableTypeHolders[$exprString]->getType()->equals($variableTypeHolder->getType()) ) { - $ourVariableTypeHolders[$name] = $variableTypeHolder; + $ourVariableTypeHolders[$exprString] = $variableTypeHolder; continue; } - if (isset($originalVariableTypeHolders[$name])) { + if (isset($originalVariableTypeHolders[$exprString])) { continue; } - $ourVariableTypeHolders[$name] = $variableTypeHolder; + $ourVariableTypeHolders[$exprString] = $variableTypeHolder; } return $ourVariableTypeHolders; @@ -4406,7 +4555,7 @@ public function processClosureScope( ): self { $nativeExpressionTypes = $this->nativeExpressionTypes; - $variableTypes = $this->variableTypes; + $expressionTypes = $this->expressionTypes; if (count($byRefUses) === 0) { return $this; } @@ -4417,10 +4566,12 @@ public function processClosureScope( } $variableName = $use->var->name; + $variableExprString = '$' . $variableName; if (!$closureScope->hasVariableType($variableName)->yes()) { - $variableTypes[$variableName] = VariableTypeHolder::createYes(new NullType()); - $nativeExpressionTypes[sprintf('$%s', $variableName)] = new NullType(); + $holder = ExpressionTypeHolder::createYes($use->var, new NullType()); + $expressionTypes[$variableExprString] = $holder; + $nativeExpressionTypes[$variableExprString] = $holder; continue; } @@ -4430,147 +4581,132 @@ public function processClosureScope( $prevVariableType = $prevScope->getVariableType($variableName); if (!$variableType->equals($prevVariableType)) { $variableType = TypeCombinator::union($variableType, $prevVariableType); - $variableType = self::generalizeType($variableType, $prevVariableType); + $variableType = self::generalizeType($variableType, $prevVariableType, 0); } } - $variableTypes[$variableName] = VariableTypeHolder::createYes($variableType); - $nativeExpressionTypes[sprintf('$%s', $variableName)] = $variableType; + $expressionTypes[$variableExprString] = ExpressionTypeHolder::createYes($use->var, $variableType); + $nativeExpressionTypes[$variableExprString] = ExpressionTypeHolder::createYes($use->var, $variableType); } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $variableTypes, - $this->moreSpecificTypes, + $expressionTypes, + $nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, [], [], - $nativeExpressionTypes, $this->inFunctionCallsStack, $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); } public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope): self { - $variableTypeHolders = $this->variableTypes; - $nativeTypes = $this->nativeExpressionTypes; - foreach ($finalScope->variableTypes as $name => $variableTypeHolder) { - $nativeTypes[sprintf('$%s', $name)] = $variableTypeHolder->getType(); - if (!isset($variableTypeHolders[$name])) { - $variableTypeHolders[$name] = VariableTypeHolder::createMaybe($variableTypeHolder->getType()); + $expressionTypes = $this->expressionTypes; + foreach ($finalScope->expressionTypes as $variableExprString => $variableTypeHolder) { + if (!isset($expressionTypes[$variableExprString])) { + $expressionTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); continue; } - $variableTypeHolders[$name] = new VariableTypeHolder( + $expressionTypes[$variableExprString] = new ExpressionTypeHolder( + $variableTypeHolder->getExpr(), $variableTypeHolder->getType(), - $variableTypeHolder->getCertainty()->and($variableTypeHolders[$name]->getCertainty()), + $variableTypeHolder->getCertainty()->and($expressionTypes[$variableExprString]->getCertainty()), ); } - - $moreSpecificTypes = $this->moreSpecificTypes; - foreach ($finalScope->moreSpecificTypes as $exprString => $variableTypeHolder) { - if (!isset($moreSpecificTypes[$exprString])) { - $moreSpecificTypes[$exprString] = VariableTypeHolder::createMaybe($variableTypeHolder->getType()); + $nativeTypes = $this->nativeExpressionTypes; + foreach ($finalScope->nativeExpressionTypes as $variableExprString => $variableTypeHolder) { + if (!isset($nativeTypes[$variableExprString])) { + $nativeTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); continue; } - $moreSpecificTypes[$exprString] = new VariableTypeHolder( + $nativeTypes[$variableExprString] = new ExpressionTypeHolder( + $variableTypeHolder->getExpr(), $variableTypeHolder->getType(), - $variableTypeHolder->getCertainty()->and($moreSpecificTypes[$exprString]->getCertainty()), + $variableTypeHolder->getCertainty()->and($nativeTypes[$variableExprString]->getCertainty()), ); } return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - $this->constantTypes, $this->getFunction(), $this->getNamespace(), - $variableTypeHolders, - $moreSpecificTypes, + $expressionTypes, + $nativeTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, [], [], - $nativeTypes, [], $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); } public function generalizeWith(self $otherScope): self { $variableTypeHolders = $this->generalizeVariableTypeHolders( - $this->getVariableTypes(), - $otherScope->getVariableTypes(), + $this->expressionTypes, + $otherScope->expressionTypes, ); - - $moreSpecificTypes = $this->generalizeVariableTypeHolders( - $this->moreSpecificTypes, - $otherScope->moreSpecificTypes, + $nativeTypes = $this->generalizeVariableTypeHolders( + $this->nativeExpressionTypes, + $otherScope->nativeExpressionTypes, ); - $variableHolderToType = static fn (VariableTypeHolder $holder): Type => $holder->getType(); - $typeToVariableHolder = static fn (Type $type): VariableTypeHolder => new VariableTypeHolder($type, TrinaryLogic::createYes()); - $filterVariableHolders = static fn (VariableTypeHolder $holder): bool => $holder->getCertainty()->yes(); - $nativeTypes = array_map($variableHolderToType, array_filter($this->generalizeVariableTypeHolders( - array_map($typeToVariableHolder, $this->nativeExpressionTypes), - array_map($typeToVariableHolder, $otherScope->nativeExpressionTypes), - ), $filterVariableHolders)); - return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), - array_map($variableHolderToType, array_filter($this->generalizeVariableTypeHolders( - array_map($typeToVariableHolder, $this->constantTypes), - array_map($typeToVariableHolder, $otherScope->constantTypes), - ), $filterVariableHolders)), $this->getFunction(), $this->getNamespace(), $variableTypeHolders, - $moreSpecificTypes, + $nativeTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClass, + $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, [], [], - $nativeTypes, [], $this->afterExtractCall, $this->parentScope, + $this->nativeTypesPromoted, ); } /** - * @param VariableTypeHolder[] $variableTypeHolders - * @param VariableTypeHolder[] $otherVariableTypeHolders - * @return VariableTypeHolder[] + * @param array $variableTypeHolders + * @param array $otherVariableTypeHolders + * @return array */ private function generalizeVariableTypeHolders( array $variableTypeHolders, array $otherVariableTypeHolders, ): array { - foreach ($variableTypeHolders as $name => $variableTypeHolder) { - if (!isset($otherVariableTypeHolders[$name])) { + foreach ($variableTypeHolders as $variableExprString => $variableTypeHolder) { + if (!isset($otherVariableTypeHolders[$variableExprString])) { continue; } - $variableTypeHolders[$name] = new VariableTypeHolder( - self::generalizeType($variableTypeHolder->getType(), $otherVariableTypeHolders[$name]->getType()), + $variableTypeHolders[$variableExprString] = new ExpressionTypeHolder( + $variableTypeHolder->getExpr(), + self::generalizeType($variableTypeHolder->getType(), $otherVariableTypeHolders[$variableExprString]->getType(), 0), $variableTypeHolder->getCertainty(), ); } @@ -4578,7 +4714,7 @@ private function generalizeVariableTypeHolders( return $variableTypeHolders; } - private static function generalizeType(Type $a, Type $b): Type + private static function generalizeType(Type $a, Type $b, int $depth): Type { if ($a->equals($b)) { return $a; @@ -4614,7 +4750,7 @@ private static function generalizeType(Type $a, Type $b): Type $constantStrings[$key][] = $type; continue; } - if ($type instanceof ConstantArrayType) { + if ($type->isConstantArray()->yes()) { $constantArrays[$key][] = $type; continue; } @@ -4671,6 +4807,7 @@ private static function generalizeType(Type $a, Type $b): Type self::generalizeType( $constantArraysA->getOffsetValueType($keyType), $constantArraysB->getOffsetValueType($keyType), + $depth + 1, ), !$constantArraysA->hasOffsetValueType($keyType)->and($constantArraysB->hasOffsetValueType($keyType))->negate()->no(), ); @@ -4678,10 +4815,17 @@ private static function generalizeType(Type $a, Type $b): Type $resultTypes[] = $resultArrayBuilder->getArray(); } else { - $resultTypes[] = new ArrayType( - TypeCombinator::union(self::generalizeType($constantArraysA->getIterableKeyType(), $constantArraysB->getIterableKeyType())), - TypeCombinator::union(self::generalizeType($constantArraysA->getIterableValueType(), $constantArraysB->getIterableValueType())), + $resultType = new ArrayType( + TypeCombinator::union(self::generalizeType($constantArraysA->getIterableKeyType(), $constantArraysB->getIterableKeyType(), $depth + 1)), + TypeCombinator::union(self::generalizeType($constantArraysA->getIterableValueType(), $constantArraysB->getIterableValueType(), $depth + 1)), ); + if ($constantArraysA->isIterableAtLeastOnce()->yes() && $constantArraysB->isIterableAtLeastOnce()->yes()) { + $resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType()); + } + if ($constantArraysA->isList()->yes() && $constantArraysB->isList()->yes()) { + $resultType = AccessoryArrayListType::intersectWith($resultType); + } + $resultTypes[] = $resultType; } } } elseif (count($constantArrays['b']) > 0) { @@ -4697,16 +4841,14 @@ private static function generalizeType(Type $a, Type $b): Type $aValueType = $generalArraysA->getIterableValueType(); $bValueType = $generalArraysB->getIterableValueType(); - $aArrays = TypeUtils::getAnyArrays($aValueType); - $bArrays = TypeUtils::getAnyArrays($bValueType); if ( - count($aArrays) === 1 - && !$aArrays[0] instanceof ConstantArrayType - && count($bArrays) === 1 - && !$bArrays[0] instanceof ConstantArrayType + $aValueType->isArray()->yes() + && $aValueType->isConstantArray()->no() + && $bValueType->isArray()->yes() + && $bValueType->isConstantArray()->no() ) { - $aDepth = self::getArrayDepth($aArrays[0]); - $bDepth = self::getArrayDepth($bArrays[0]); + $aDepth = self::getArrayDepth($aValueType) + $depth; + $bDepth = self::getArrayDepth($bValueType) + $depth; if ( ($aDepth > 2 || $bDepth > 2) && abs($aDepth - $bDepth) > 0 @@ -4716,10 +4858,20 @@ private static function generalizeType(Type $a, Type $b): Type } } - $resultTypes[] = new ArrayType( - TypeCombinator::union(self::generalizeType($generalArraysA->getIterableKeyType(), $generalArraysB->getIterableKeyType())), - TypeCombinator::union(self::generalizeType($aValueType, $bValueType)), + $resultType = new ArrayType( + TypeCombinator::union(self::generalizeType($generalArraysA->getIterableKeyType(), $generalArraysB->getIterableKeyType(), $depth + 1)), + TypeCombinator::union(self::generalizeType($aValueType, $bValueType, $depth + 1)), ); + if ($generalArraysA->isIterableAtLeastOnce()->yes() && $generalArraysB->isIterableAtLeastOnce()->yes()) { + $resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType()); + } + if ($generalArraysA->isList()->yes() && $generalArraysB->isList()->yes()) { + $resultType = AccessoryArrayListType::intersectWith($resultType); + } + if ($generalArraysA->isOversizedArray()->yes() && $generalArraysB->isOversizedArray()->yes()) { + $resultType = TypeCombinator::intersect($resultType, new OversizedArrayType()); + } + $resultTypes[] = $resultType; } } elseif (count($generalArrays['b']) > 0) { $resultTypes[] = TypeCombinator::union(...$generalArrays['b']); @@ -4861,23 +5013,20 @@ private static function generalizeType(Type $a, Type $b): Type TypeUtils::getAccessoryTypes($a), ); - return TypeCombinator::intersect( + return TypeCombinator::union(TypeCombinator::intersect( TypeCombinator::union(...$resultTypes, ...$otherTypes), ...$accessoryTypes, - ); + ), ...$otherTypes); } - private static function getArrayDepth(ArrayType $type): int + private static function getArrayDepth(Type $type): int { $depth = 0; - while ($type instanceof ArrayType) { + $arrays = TypeUtils::getAnyArrays($type); + while (count($arrays) > 0) { $temp = $type->getIterableValueType(); - $arrays = TypeUtils::getAnyArrays($temp); - if (count($arrays) === 1) { - $type = $arrays[0]; - } else { - $type = $temp; - } + $type = $temp; + $arrays = TypeUtils::getAnyArrays($type); $depth++; } @@ -4890,59 +5039,54 @@ public function equals(self $otherScope): bool return false; } - if (!$this->compareVariableTypeHolders($this->variableTypes, $otherScope->variableTypes)) { - return false; - } - - if (!$this->compareVariableTypeHolders($this->moreSpecificTypes, $otherScope->moreSpecificTypes)) { + if (!$this->compareVariableTypeHolders($this->expressionTypes, $otherScope->expressionTypes)) { return false; } - - $typeToVariableHolder = static fn (Type $type): VariableTypeHolder => new VariableTypeHolder($type, TrinaryLogic::createYes()); - - $nativeExpressionTypesResult = $this->compareVariableTypeHolders( - array_map($typeToVariableHolder, $this->nativeExpressionTypes), - array_map($typeToVariableHolder, $otherScope->nativeExpressionTypes), - ); - - if (!$nativeExpressionTypesResult) { - return false; - } - - return $this->compareVariableTypeHolders( - array_map($typeToVariableHolder, $this->constantTypes), - array_map($typeToVariableHolder, $otherScope->constantTypes), - ); + return $this->compareVariableTypeHolders($this->nativeExpressionTypes, $otherScope->nativeExpressionTypes); } /** - * @param VariableTypeHolder[] $variableTypeHolders - * @param VariableTypeHolder[] $otherVariableTypeHolders + * @param array $variableTypeHolders + * @param array $otherVariableTypeHolders */ private function compareVariableTypeHolders(array $variableTypeHolders, array $otherVariableTypeHolders): bool { if (count($variableTypeHolders) !== count($otherVariableTypeHolders)) { return false; } - foreach ($variableTypeHolders as $name => $variableTypeHolder) { - if (!isset($otherVariableTypeHolders[$name])) { + foreach ($variableTypeHolders as $variableExprString => $variableTypeHolder) { + if (!isset($otherVariableTypeHolders[$variableExprString])) { return false; } - if (!$variableTypeHolder->getCertainty()->equals($otherVariableTypeHolders[$name]->getCertainty())) { + if (!$variableTypeHolder->getCertainty()->equals($otherVariableTypeHolders[$variableExprString]->getCertainty())) { return false; } - if (!$variableTypeHolder->getType()->equals($otherVariableTypeHolders[$name]->getType())) { + if (!$variableTypeHolder->getType()->equals($otherVariableTypeHolders[$variableExprString]->getType())) { return false; } - unset($otherVariableTypeHolders[$name]); + unset($otherVariableTypeHolders[$variableExprString]); } return true; } + private function getBooleanExpressionDepth(Expr $expr, int $depth = 0): int + { + while ( + $expr instanceof BinaryOp\BooleanOr + || $expr instanceof BinaryOp\LogicalOr + || $expr instanceof BinaryOp\BooleanAnd + || $expr instanceof BinaryOp\LogicalAnd + ) { + return $this->getBooleanExpressionDepth($expr->left, $depth + 1); + } + + return $depth; + } + /** @api */ public function canAccessProperty(PropertyReflection $propertyReflection): bool { @@ -4971,29 +5115,39 @@ private function canAccessClassMember(ClassMemberReflection $classMemberReflecti return true; } - if ($this->inClosureBindScopeClass !== null && $this->reflectionProvider->hasClass($this->inClosureBindScopeClass)) { - $currentClassReflection = $this->reflectionProvider->getClass($this->inClosureBindScopeClass); - } elseif ($this->isInClass()) { - $currentClassReflection = $this->getClassReflection(); - } else { - return false; - } - $classReflectionName = $classMemberReflection->getDeclaringClass()->getName(); - if ($classMemberReflection->isPrivate()) { - return $currentClassReflection->getName() === $classReflectionName; - } + $canAccessClassMember = static function (ClassReflection $classReflection) use ($classMemberReflection, $classReflectionName) { + if ($classMemberReflection->isPrivate()) { + return $classReflection->getName() === $classReflectionName; + } - // protected + // protected - if ( - $currentClassReflection->getName() === $classReflectionName - || $currentClassReflection->isSubclassOf($classReflectionName) - ) { - return true; + if ( + $classReflection->getName() === $classReflectionName + || $classReflection->isSubclassOf($classReflectionName) + ) { + return true; + } + + return $classMemberReflection->getDeclaringClass()->isSubclassOf($classReflection->getName()); + }; + + foreach ($this->inClosureBindScopeClasses as $inClosureBindScopeClass) { + if (!$this->reflectionProvider->hasClass($inClosureBindScopeClass)) { + continue; + } + + if ($canAccessClassMember($this->reflectionProvider->getClass($inClosureBindScopeClass))) { + return true; + } + } + + if ($this->isInClass()) { + return $canAccessClassMember($this->getClassReflection()); } - return $classMemberReflection->getDeclaringClass()->isSubclassOf($currentClassReflection->getName()); + return false; } /** @@ -5002,25 +5156,31 @@ private function canAccessClassMember(ClassMemberReflection $classMemberReflecti public function debug(): array { $descriptions = []; - foreach ($this->getVariableTypes() as $name => $variableTypeHolder) { - $key = sprintf('$%s (%s)', $name, $variableTypeHolder->getCertainty()->describe()); + foreach ($this->expressionTypes as $name => $variableTypeHolder) { + $key = sprintf('%s (%s)', $name, $variableTypeHolder->getCertainty()->describe()); $descriptions[$key] = $variableTypeHolder->getType()->describe(VerbosityLevel::precise()); } - foreach ($this->moreSpecificTypes as $exprString => $typeHolder) { - $key = sprintf( - '%s-specified (%s)', - $exprString, - $typeHolder->getCertainty()->describe(), - ); - $descriptions[$key] = $typeHolder->getType()->describe(VerbosityLevel::precise()); - } - foreach ($this->constantTypes as $name => $type) { - $key = sprintf('const %s', $name); - $descriptions[$key] = $type->describe(VerbosityLevel::precise()); - } - foreach ($this->nativeExpressionTypes as $exprString => $nativeType) { + foreach ($this->nativeExpressionTypes as $exprString => $nativeTypeHolder) { $key = sprintf('native %s', $exprString); - $descriptions[$key] = $nativeType->describe(VerbosityLevel::precise()); + $descriptions[$key] = $nativeTypeHolder->getType()->describe(VerbosityLevel::precise()); + } + + foreach ($this->conditionalExpressions as $exprString => $holders) { + foreach (array_values($holders) as $i => $holder) { + $key = sprintf('condition about %s #%d', $exprString, $i + 1); + $parts = []; + foreach ($holder->getConditionExpressionTypeHolders() as $conditionalExprString => $expressionTypeHolder) { + $parts[] = $conditionalExprString . '=' . $expressionTypeHolder->getType()->describe(VerbosityLevel::precise()); + } + $condition = implode(' && ', $parts); + $descriptions[$key] = sprintf( + 'if %s then %s is %s (%s)', + $condition, + $exprString, + $holder->getTypeHolder()->getType()->describe(VerbosityLevel::precise()), + $holder->getTypeHolder()->getCertainty()->describe(), + ); + } } return $descriptions; @@ -5055,6 +5215,7 @@ private function exactInstantiation(New_ $node, string $className): ?Type $this, $methodCall->getArgs(), $constructorMethod->getVariants(), + $constructorMethod->getNamedArgumentsVariants(), ); $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall); @@ -5129,13 +5290,26 @@ private function exactInstantiation(New_ $node, string $className): ?Type $this, $methodCall->getArgs(), $constructorMethod->getVariants(), + $constructorMethod->getNamedArgumentsVariants(), ); if ($this->explicitMixedInUnknownGenericNew) { - return new GenericObjectType( + $resolvedTemplateTypeMap = $parametersAcceptor->getResolvedTemplateTypeMap(); + return TypeTraverser::map(new GenericObjectType( $resolvedClassName, - $classReflection->typeMapToList($parametersAcceptor->getResolvedTemplateTypeMap()), - ); + $classReflection->typeMapToList($classReflection->getTemplateTypeMap()), + ), static function (Type $type, callable $traverse) use ($resolvedTemplateTypeMap): Type { + if ($type instanceof TemplateType && !$type->isArgument()) { + $newType = $resolvedTemplateTypeMap->getType($type->getName()); + if ($newType === null || $newType instanceof ErrorType) { + return $type->getBound(); + } + + return TemplateTypeHelper::generalizeInferredTemplateType($type, $newType); + } + + return $traverse($type); + }); } $resolvedPhpDoc = $classReflection->getResolvedPhpDoc(); @@ -5164,35 +5338,7 @@ private function exactInstantiation(New_ $node, string $className): ?Type ); } - private function getTypeToInstantiateForNew(Type $type): Type - { - if ($type instanceof UnionType) { - $types = array_map(fn (Type $type) => $this->getTypeToInstantiateForNew($type), $type->getTypes()); - return TypeCombinator::union(...$types); - } - - if ($type instanceof IntersectionType) { - $types = array_map(fn (Type $type) => $this->getTypeToInstantiateForNew($type), $type->getTypes()); - return TypeCombinator::intersect(...$types); - } - - if ($type instanceof ConstantStringType) { - return new ObjectType($type->getValue()); - } - - if ($type instanceof GenericClassStringType) { - return $type->getGenericType(); - } - - if ((new ObjectWithoutClassType())->isSuperTypeOf($type)->yes()) { - return $type; - } - - return new ObjectWithoutClassType(); - } - - /** @api */ - public function getMethodReflection(Type $typeWithMethod, string $methodName): ?MethodReflection + private function filterTypeWithMethod(Type $typeWithMethod, string $methodName): ?Type { if ($typeWithMethod instanceof UnionType) { $newTypes = []; @@ -5213,7 +5359,29 @@ public function getMethodReflection(Type $typeWithMethod, string $methodName): ? return null; } - return $typeWithMethod->getMethod($methodName, $this); + return $typeWithMethod; + } + + /** @api */ + public function getMethodReflection(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection + { + $type = $this->filterTypeWithMethod($typeWithMethod, $methodName); + if ($type === null) { + return null; + } + + return $type->getMethod($methodName, $this); + } + + /** @api */ + public function getNakedMethod(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection + { + $type = $this->filterTypeWithMethod($typeWithMethod, $methodName); + if ($type === null) { + return null; + } + + return $type->getUnresolvedMethodPrototype($methodName, $this)->getNakedMethod(); } /** @@ -5221,15 +5389,17 @@ public function getMethodReflection(Type $typeWithMethod, string $methodName): ? */ private function methodCallReturnType(Type $typeWithMethod, string $methodName, Expr $methodCall): ?Type { - $methodReflection = $this->getMethodReflection($typeWithMethod, $methodName); - if ($methodReflection === null) { + $typeWithMethod = $this->filterTypeWithMethod($typeWithMethod, $methodName); + if ($typeWithMethod === null) { return null; } + $methodReflection = $typeWithMethod->getMethod($methodName, $this); $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $this, $methodCall->getArgs(), $methodReflection->getVariants(), + $methodReflection->getNamedArgumentsVariants(), ); if ($methodCall instanceof MethodCall) { $normalizedMethodCall = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $methodCall); @@ -5237,11 +5407,11 @@ private function methodCallReturnType(Type $typeWithMethod, string $methodName, $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall); } if ($normalizedMethodCall === null) { - return $parametersAcceptor->getReturnType(); + return $this->transformVoidToNull($parametersAcceptor->getReturnType(), $methodCall); } $resolvedTypes = []; - foreach (TypeUtils::getDirectClassNames($typeWithMethod) as $className) { + foreach ($typeWithMethod->getObjectClassNames() as $className) { if ($normalizedMethodCall instanceof MethodCall) { foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicMethodReturnTypeExtensionsForClass($className) as $dynamicMethodReturnTypeExtension) { if (!$dynamicMethodReturnTypeExtension->isMethodSupported($methodReflection)) { @@ -5276,10 +5446,10 @@ private function methodCallReturnType(Type $typeWithMethod, string $methodName, } if (count($resolvedTypes) > 0) { - return TypeCombinator::union(...$resolvedTypes); + return $this->transformVoidToNull(TypeCombinator::union(...$resolvedTypes), $methodCall); } - return $parametersAcceptor->getReturnType(); + return $this->transformVoidToNull($parametersAcceptor->getReturnType(), $methodCall); } /** @api */ @@ -5346,4 +5516,76 @@ public function getConstantReflection(Type $typeWithConstant, string $constantNa return $typeWithConstant->getConstant($constantName); } + /** + * @return array + */ + private function getConstantTypes(): array + { + $constantTypes = []; + foreach ($this->expressionTypes as $exprString => $typeHolder) { + $expr = $typeHolder->getExpr(); + if (!$expr instanceof ConstFetch) { + continue; + } + $constantTypes[$exprString] = $typeHolder; + } + return $constantTypes; + } + + /** + * @return array + */ + private function getNativeConstantTypes(): array + { + $constantTypes = []; + foreach ($this->nativeExpressionTypes as $exprString => $typeHolder) { + $expr = $typeHolder->getExpr(); + if (!$expr instanceof ConstFetch) { + continue; + } + $constantTypes[$exprString] = $typeHolder; + } + return $constantTypes; + } + + public function getIterableKeyType(Type $iteratee): Type + { + if ($iteratee instanceof UnionType) { + $newTypes = []; + foreach ($iteratee->getTypes() as $innerType) { + if (!$innerType->isIterable()->yes()) { + continue; + } + + $newTypes[] = $innerType; + } + if (count($newTypes) === 0) { + return $iteratee->getIterableKeyType(); + } + $iteratee = TypeCombinator::union(...$newTypes); + } + + return $iteratee->getIterableKeyType(); + } + + public function getIterableValueType(Type $iteratee): Type + { + if ($iteratee instanceof UnionType) { + $newTypes = []; + foreach ($iteratee->getTypes() as $innerType) { + if (!$innerType->isIterable()->yes()) { + continue; + } + + $newTypes[] = $innerType; + } + if (count($newTypes) === 0) { + return $iteratee->getIterableValueType(); + } + $iteratee = TypeCombinator::union(...$newTypes); + } + + return $iteratee->getIterableValueType(); + } + } diff --git a/src/Analyser/NameScope.php b/src/Analyser/NameScope.php index bb8a4347e6..af3688e107 100644 --- a/src/Analyser/NameScope.php +++ b/src/Analyser/NameScope.php @@ -13,7 +13,7 @@ use function implode; use function ltrim; use function sprintf; -use function strpos; +use function str_starts_with; use function strtolower; /** @api */ @@ -28,7 +28,7 @@ class NameScope * @param array $constUses alias(string) => fullName(string) * @param array $typeAliasesMap */ - public function __construct(private ?string $namespace, private array $uses, private ?string $className = null, private ?string $functionName = null, ?TemplateTypeMap $templateTypeMap = null, private array $typeAliasesMap = [], private bool $bypassTypeAliases = false, private array $constUses = []) + public function __construct(private ?string $namespace, private array $uses, private ?string $className = null, private ?string $functionName = null, ?TemplateTypeMap $templateTypeMap = null, private array $typeAliasesMap = [], private bool $bypassTypeAliases = false, private array $constUses = [], private ?string $typeAliasClassName = null) { $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); } @@ -64,9 +64,14 @@ public function getClassName(): ?string return $this->className; } + public function getClassNameForTypeAlias(): ?string + { + return $this->typeAliasClassName ?? $this->className; + } + public function resolveStringName(string $name): string { - if (strpos($name, '\\') === 0) { + if (str_starts_with($name, '\\')) { return ltrim($name, '\\'); } @@ -92,7 +97,7 @@ public function resolveStringName(string $name): string */ public function resolveConstantNames(string $name): array { - if (strpos($name, '\\') === 0) { + if (str_starts_with($name, '\\')) { return [ltrim($name, '\\')]; } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index de5d6dc7e4..331373a5e2 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -45,12 +45,15 @@ use PhpParser\Node\Stmt\If_; use PhpParser\Node\Stmt\Return_; use PhpParser\Node\Stmt\Static_; -use PhpParser\Node\Stmt\StaticVar; use PhpParser\Node\Stmt\Switch_; use PhpParser\Node\Stmt\Throw_; use PhpParser\Node\Stmt\TryCatch; use PhpParser\Node\Stmt\Unset_; use PhpParser\Node\Stmt\While_; +use PhpParser\NodeFinder; +use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor\CloningVisitor; +use PhpParser\NodeVisitorAbstract; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; use PHPStan\BetterReflection\Reflection\ReflectionEnum; use PHPStan\BetterReflection\Reflector\Reflector; @@ -72,11 +75,17 @@ use PHPStan\Node\ClosureReturnStatementsNode; use PHPStan\Node\DoWhileLoopConditionNode; use PHPStan\Node\ExecutionEndNode; +use PHPStan\Node\Expr\AlwaysRememberedExpr; +use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\GetIterableValueTypeExpr; use PHPStan\Node\Expr\GetOffsetValueTypeExpr; use PHPStan\Node\Expr\OriginalPropertyTypeExpr; +use PHPStan\Node\Expr\PropertyInitializationExpr; +use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr; use PHPStan\Node\Expr\SetOffsetValueTypeExpr; +use PHPStan\Node\Expr\TypeExpr; +use PHPStan\Node\Expr\UnsetOffsetExpr; use PHPStan\Node\FinallyExitPointsNode; use PHPStan\Node\FunctionCallableNode; use PHPStan\Node\FunctionReturnStatementsNode; @@ -87,6 +96,8 @@ use PHPStan\Node\InForeachNode; use PHPStan\Node\InFunctionNode; use PHPStan\Node\InstantiationCallableNode; +use PHPStan\Node\InTraitNode; +use PHPStan\Node\InvalidateExprNode; use PHPStan\Node\LiteralArrayItem; use PHPStan\Node\LiteralArrayNode; use PHPStan\Node\MatchExpressionArm; @@ -95,10 +106,13 @@ use PHPStan\Node\MatchExpressionNode; use PHPStan\Node\MethodCallableNode; use PHPStan\Node\MethodReturnStatementsNode; +use PHPStan\Node\NoopExpressionNode; use PHPStan\Node\PropertyAssignNode; use PHPStan\Node\ReturnStatement; use PHPStan\Node\StaticMethodCallableNode; use PHPStan\Node\UnreachableStatementNode; +use PHPStan\Node\VariableAssignNode; +use PHPStan\Node\VarTagChangedExpressionTypeNode; use PHPStan\Parser\ArrowFunctionArgVisitor; use PHPStan\Parser\ClosureArgVisitor; use PHPStan\Parser\Parser; @@ -107,19 +121,31 @@ use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDoc\StubPhpDocProvider; use PHPStan\PhpDoc\Tag\VarTag; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\Reflection\Callables\SimpleThrowPoint; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\Native\NativeMethodReflection; use PHPStan\Reflection\Native\NativeParameterReflection; +use PHPStan\Reflection\ParameterReflection; +use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; +use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; +use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Reflection\SignatureMap\SignatureMapProvider; use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\ClosureType; @@ -127,28 +153,30 @@ use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; +use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\ResourceType; use PHPStan\Type\StaticType; use PHPStan\Type\StaticTypeFactory; use PHPStan\Type\StringType; +use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; -use PHPStan\Type\VoidType; use Throwable; use Traversable; use TypeError; @@ -156,11 +184,14 @@ use function array_fill_keys; use function array_filter; use function array_key_exists; +use function array_key_last; +use function array_keys; use function array_map; use function array_merge; use function array_pop; use function array_reverse; use function array_slice; +use function array_values; use function base64_decode; use function count; use function in_array; @@ -183,11 +214,18 @@ class NodeScopeResolver private array $analysedFiles = []; /** @var array */ - private array $earlyTerminatingMethodNames = []; + private array $earlyTerminatingMethodNames; + + /** @var array */ + private array $calledMethodStack = []; + + /** @var array */ + private array $calledMethodResults = []; /** * @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[]) * @param array $earlyTerminatingFunctionCalls + * @param string[] $universalObjectCratesClasses */ public function __construct( private readonly ReflectionProvider $reflectionProvider, @@ -198,16 +236,22 @@ public function __construct( private readonly FileTypeMapper $fileTypeMapper, private readonly StubPhpDocProvider $stubPhpDocProvider, private readonly PhpVersion $phpVersion, + private readonly SignatureMapProvider $signatureMapProvider, private readonly PhpDocInheritanceResolver $phpDocInheritanceResolver, private readonly FileHelper $fileHelper, private readonly TypeSpecifier $typeSpecifier, private readonly DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider, private readonly ReadWritePropertiesExtensionProvider $readWritePropertiesExtensionProvider, + private readonly ScopeFactory $scopeFactory, private readonly bool $polluteScopeWithLoopInitialAssignments, private readonly bool $polluteScopeWithAlwaysIterableForeach, private readonly array $earlyTerminatingMethodCalls, private readonly array $earlyTerminatingFunctionCalls, + private readonly array $universalObjectCratesClasses, private readonly bool $implicitThrows, + private readonly bool $treatPhpDocTypesAsCertain, + private readonly bool $detectDeadTypeInMultiCatch, + private readonly bool $paramOutType, ) { $earlyTerminatingMethodNames = []; @@ -239,26 +283,23 @@ public function processNodes( callable $nodeCallback, ): void { - $nodesCount = count($nodes); foreach ($nodes as $i => $node) { if (!$node instanceof Node\Stmt) { continue; } - $statementResult = $this->processStmtNode($node, $scope, $nodeCallback); + $statementResult = $this->processStmtNode($node, $scope, $nodeCallback, StatementContext::createTopLevel()); $scope = $statementResult->getScope(); if (!$statementResult->isAlwaysTerminating()) { continue; } - if ($i < $nodesCount - 1) { - $nextStmt = $nodes[$i + 1]; - if (!$nextStmt instanceof Node\Stmt) { - continue; - } - - $nodeCallback(new UnreachableStatementNode($nextStmt), $scope); + $nextStmt = $this->getFirstUnreachableNode(array_slice($nodes, $i + 1), true); + if (!$nextStmt instanceof Node\Stmt) { + continue; } + + $nodeCallback(new UnreachableStatementNode($nextStmt), $scope); break; } } @@ -273,10 +314,15 @@ public function processStmtNodes( array $stmts, MutatingScope $scope, callable $nodeCallback, + ?StatementContext $context = null, ): StatementResult { + if ($context === null) { + $context = StatementContext::createTopLevel(); + } $exitPoints = []; $throwPoints = []; + $impurePoints = []; $alreadyTerminated = false; $hasYield = false; $stmtCount = count($stmts); @@ -289,6 +335,7 @@ public function processStmtNodes( $stmt, $scope, $nodeCallback, + $context, ); $scope = $statementResult->getScope(); $hasYield = $hasYield || $statementResult->hasYield(); @@ -304,6 +351,7 @@ public function processStmtNodes( $statementResult->isAlwaysTerminating(), $statementResult->getExitPoints(), $statementResult->getThrowPoints(), + $statementResult->getImpurePoints(), ), $parentNode->returnType !== null, ), $scope); @@ -311,20 +359,21 @@ public function processStmtNodes( $exitPoints = array_merge($exitPoints, $statementResult->getExitPoints()); $throwPoints = array_merge($throwPoints, $statementResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $statementResult->getImpurePoints()); if (!$statementResult->isAlwaysTerminating()) { continue; } $alreadyTerminated = true; - if ($i < $stmtCount - 1) { - $nextStmt = $stmts[$i + 1]; + $nextStmt = $this->getFirstUnreachableNode(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_); + if ($nextStmt !== null) { $nodeCallback(new UnreachableStatementNode($nextStmt), $scope); } break; } - $statementResult = new StatementResult($scope, $hasYield, $alreadyTerminated, $exitPoints, $throwPoints); + $statementResult = new StatementResult($scope, $hasYield, $alreadyTerminated, $exitPoints, $throwPoints, $impurePoints); if ($stmtCount === 0 && $shouldCheckLastStatement) { /** @var Node\Stmt\Function_|Node\Stmt\ClassMethod|Expr\Closure $parentNode */ $parentNode = $parentNode; @@ -345,21 +394,19 @@ private function processStmtNode( Node\Stmt $stmt, MutatingScope $scope, callable $nodeCallback, + StatementContext $context, ): StatementResult { if ( - $stmt instanceof Throw_ - || $stmt instanceof Return_ - ) { - $scope = $this->processStmtVarAnnotation($scope, $stmt, $stmt->expr); - } elseif ( !$stmt instanceof Static_ && !$stmt instanceof Foreach_ && !$stmt instanceof Node\Stmt\Global_ && !$stmt instanceof Node\Stmt\Property && !$stmt instanceof Node\Stmt\PropertyProperty + && !$stmt instanceof Node\Stmt\ClassConst + && !$stmt instanceof Node\Stmt\Const_ ) { - $scope = $this->processStmtVarAnnotation($scope, $stmt, null); + $scope = $this->processStmtVarAnnotation($scope, $stmt, null, $nodeCallback); } if ($stmt instanceof Node\Stmt\ClassMethod) { @@ -372,24 +419,32 @@ private function processStmtNode( ) { $methodReflection = $scope->getClassReflection()->getNativeMethod($stmt->name->toString()); if ($methodReflection instanceof NativeMethodReflection) { - return new StatementResult($scope, false, false, [], []); + return new StatementResult($scope, false, false, [], [], []); } if ($methodReflection instanceof PhpMethodReflection) { $declaringTrait = $methodReflection->getDeclaringTrait(); if ($declaringTrait === null || $declaringTrait->getName() !== $scope->getTraitReflection()->getName()) { - return new StatementResult($scope, false, false, [], []); + return new StatementResult($scope, false, false, [], [], []); } } } } - $nodeCallback($stmt, $scope); + $stmtScope = $scope; + if ($stmt instanceof Throw_ || $stmt instanceof Return_) { + $stmtScope = $this->processStmtVarAnnotation($scope, $stmt, $stmt->expr, $nodeCallback); + } + + $nodeCallback($stmt, $stmtScope); $overridingThrowPoints = $this->getOverridingThrowPoints($stmt, $scope); if ($stmt instanceof Node\Stmt\Declare_) { $hasYield = false; $throwPoints = []; + $impurePoints = []; + $alwaysTerminating = false; + $exitPoints = []; foreach ($stmt->declares as $declare) { $nodeCallback($declare, $scope); $nodeCallback($declare->value, $scope); @@ -403,14 +458,27 @@ private function processStmtNode( $scope = $scope->enterDeclareStrictTypes(); } + + if ($stmt->stmts !== null) { + $result = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context); + $scope = $result->getScope(); + $hasYield = $result->hasYield(); + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + $alwaysTerminating = $result->isAlwaysTerminating(); + $exitPoints = $result->getExitPoints(); + } + + return new StatementResult($scope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints); } elseif ($stmt instanceof Node\Stmt\Function_) { $hasYield = false; $throwPoints = []; - $this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback); - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments] = $this->getPhpDocs($scope, $stmt); + $impurePoints = []; + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { - $this->processParamNode($param, $scope, $nodeCallback); + $this->processParamNode($stmt, $param, $scope, $nodeCallback); } if ($stmt->returnType !== null) { @@ -429,17 +497,24 @@ private function processStmtNode( $isFinal, $isPure, $acceptsNamedArguments, + $asserts, + $phpDocComment, + $phpDocParameterOutTypes, + $phpDocImmediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, ); $functionReflection = $functionScope->getFunction(); - if (!$functionReflection instanceof FunctionReflection) { + if (!$functionReflection instanceof PhpFunctionFromParserNodeReflection) { throw new ShouldNotHappenException(); } $nodeCallback(new InFunctionNode($functionReflection, $stmt), $functionScope); $gatheredReturnStatements = []; + $gatheredYieldStatements = []; $executionEnds = []; - $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $functionScope, static function (Node $node, Scope $scope) use ($nodeCallback, $functionScope, &$gatheredReturnStatements, &$executionEnds): void { + $functionImpurePoints = []; + $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $functionScope, static function (Node $node, Scope $scope) use ($nodeCallback, $functionScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$functionImpurePoints): void { $nodeCallback($node, $scope); if ($scope->getFunction() !== $functionScope->getFunction()) { return; @@ -447,31 +522,48 @@ private function processStmtNode( if ($scope->isInAnonymousFunction()) { return; } + if ($node instanceof PropertyAssignNode) { + $functionImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + return; + } if ($node instanceof ExecutionEndNode) { $executionEnds[] = $node; return; } + if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) { + $gatheredYieldStatements[] = $node; + } if (!$node instanceof Return_) { return; } $gatheredReturnStatements[] = new ReturnStatement($scope, $node); - }); + }, StatementContext::createTopLevel()); $nodeCallback(new FunctionReturnStatementsNode( $stmt, $gatheredReturnStatements, + $gatheredYieldStatements, $statementResult, $executionEnds, + array_merge($statementResult->getImpurePoints(), $functionImpurePoints), + $functionReflection, ), $functionScope); } elseif ($stmt instanceof Node\Stmt\ClassMethod) { $hasYield = false; $throwPoints = []; - $this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback); - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments] = $this->getPhpDocs($scope, $stmt); + $impurePoints = []; + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { - $this->processParamNode($param, $scope, $nodeCallback); + $this->processParamNode($stmt, $param, $scope, $nodeCallback); } if ($stmt->returnType !== null) { @@ -490,9 +582,20 @@ private function processStmtNode( $isFinal, $isPure, $acceptsNamedArguments, + $asserts, + $selfOutType, + $phpDocComment, + $phpDocParameterOutTypes, + $phpDocImmediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, ); - if ($stmt->name->toLowerString() === '__construct') { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + + $isFromTrait = $stmt->getAttribute('originalTraitMethodName') === '__construct'; + if ($isFromTrait || $stmt->name->toLowerString() === '__construct') { foreach ($stmt->params as $param) { if ($param->flags === 0) { continue; @@ -505,9 +608,6 @@ private function processStmtNode( if ($param->getDocComment() !== null) { $phpDoc = $param->getDocComment()->getText(); } - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } $nodeCallback(new ClassPropertyNode( $param->var->name, $param->flags, @@ -516,26 +616,32 @@ private function processStmtNode( $phpDoc, $phpDocParameterTypes[$param->var->name] ?? null, true, + $isFromTrait, $param, false, $scope->isInTrait(), $scope->getClassReflection()->isReadOnly(), + false, + $scope->getClassReflection(), ), $methodScope); + $methodScope = $methodScope->assignExpression(new PropertyInitializationExpr($param->var->name), new MixedType(), new MixedType()); } } if ($stmt->getAttribute('virtual', false) === false) { $methodReflection = $methodScope->getFunction(); - if (!$methodReflection instanceof MethodReflection) { + if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) { throw new ShouldNotHappenException(); } - $nodeCallback(new InClassMethodNode($methodReflection, $stmt), $methodScope); + $nodeCallback(new InClassMethodNode($scope->getClassReflection(), $methodReflection, $stmt), $methodScope); } if ($stmt->stmts !== null) { $gatheredReturnStatements = []; + $gatheredYieldStatements = []; $executionEnds = []; - $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $methodScope, static function (Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$executionEnds): void { + $methodImpurePoints = []; + $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $methodScope, static function (Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$methodImpurePoints): void { $nodeCallback($node, $scope); if ($scope->getFunction() !== $methodScope->getFunction()) { return; @@ -543,65 +649,132 @@ private function processStmtNode( if ($scope->isInAnonymousFunction()) { return; } + if ($node instanceof PropertyAssignNode) { + if ( + $node->getPropertyFetch() instanceof Expr\PropertyFetch + && $scope->getFunction() instanceof PhpMethodFromParserNodeReflection + && $scope->getFunction()->getDeclaringClass()->hasConstructor() + && $scope->getFunction()->getDeclaringClass()->getConstructor()->getName() === $scope->getFunction()->getName() + && TypeUtils::findThisType($scope->getType($node->getPropertyFetch()->var)) !== null + ) { + return; + } + $methodImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + return; + } if ($node instanceof ExecutionEndNode) { $executionEnds[] = $node; return; } + if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) { + $gatheredYieldStatements[] = $node; + } if (!$node instanceof Return_) { return; } $gatheredReturnStatements[] = new ReturnStatement($scope, $node); - }); + }, StatementContext::createTopLevel()); + + $classReflection = $scope->getClassReflection(); + + $methodReflection = $methodScope->getFunction(); + if (!$methodReflection instanceof ExtendedMethodReflection) { + throw new ShouldNotHappenException(); + } + $nodeCallback(new MethodReturnStatementsNode( $stmt, $gatheredReturnStatements, + $gatheredYieldStatements, $statementResult, $executionEnds, + array_merge($statementResult->getImpurePoints(), $methodImpurePoints), + $classReflection, + $methodReflection, ), $methodScope); } } elseif ($stmt instanceof Echo_) { $hasYield = false; $throwPoints = []; foreach ($stmt->exprs as $echoExpr) { - $result = $this->processExprNode($echoExpr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $echoExpr, $scope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); } $throwPoints = $overridingThrowPoints ?? $throwPoints; + $impurePoints = [ + new ImpurePoint($scope, $stmt, 'echo', 'echo', true), + ]; } elseif ($stmt instanceof Return_) { if ($stmt->expr !== null) { - $result = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); $hasYield = $result->hasYield(); } else { $hasYield = false; $throwPoints = []; + $impurePoints = []; } return new StatementResult($scope, $hasYield, true, [ new StatementExitPoint($stmt, $scope), - ], $overridingThrowPoints ?? $throwPoints); + ], $overridingThrowPoints ?? $throwPoints, $impurePoints); } elseif ($stmt instanceof Continue_ || $stmt instanceof Break_) { if ($stmt->num !== null) { - $result = $this->processExprNode($stmt->num, $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $stmt->num, $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); } else { $hasYield = false; $throwPoints = []; + $impurePoints = []; } return new StatementResult($scope, $hasYield, true, [ new StatementExitPoint($stmt, $scope), - ], $overridingThrowPoints ?? $throwPoints); + ], $overridingThrowPoints ?? $throwPoints, $impurePoints); } elseif ($stmt instanceof Node\Stmt\Expression) { $earlyTerminationExpr = $this->findEarlyTerminatingExpr($stmt->expr, $scope); - $result = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createTopLevel()); + $hasAssign = false; + $currentScope = $scope; + $result = $this->processExprNode($stmt, $stmt->expr, $scope, static function (Node $node, Scope $scope) use ($nodeCallback, $currentScope, &$hasAssign): void { + $nodeCallback($node, $scope); + if ($scope->getAnonymousFunctionReflection() !== $currentScope->getAnonymousFunctionReflection()) { + return; + } + if ($scope->getFunction() !== $currentScope->getFunction()) { + return; + } + if (!$node instanceof VariableAssignNode && !$node instanceof PropertyAssignNode) { + return; + } + + $hasAssign = true; + }, ExpressionContext::createTopLevel()); + $throwPoints = array_filter($result->getThrowPoints(), static fn ($throwPoint) => $throwPoint->isExplicit()); + if ( + count($result->getImpurePoints()) === 0 + && count($throwPoints) === 0 + && !$stmt->expr instanceof Expr\PostInc + && !$stmt->expr instanceof Expr\PreInc + && !$stmt->expr instanceof Expr\PostDec + && !$stmt->expr instanceof Expr\PreDec + ) { + $nodeCallback(new NoopExpressionNode($stmt->expr, $hasAssign), $scope); + } $scope = $result->getScope(); $scope = $scope->filterBySpecifiedTypes($this->typeSpecifier->specifyTypesInCondition( $scope, @@ -610,25 +783,28 @@ private function processStmtNode( )); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); if ($earlyTerminationExpr !== null) { return new StatementResult($scope, $hasYield, true, [ new StatementExitPoint($stmt, $scope), - ], $overridingThrowPoints ?? $throwPoints); + ], $overridingThrowPoints ?? $throwPoints, $impurePoints); } - return new StatementResult($scope, $hasYield, false, [], $overridingThrowPoints ?? $throwPoints); + return new StatementResult($scope, $hasYield, false, [], $overridingThrowPoints ?? $throwPoints, $impurePoints); } elseif ($stmt instanceof Node\Stmt\Namespace_) { if ($stmt->name !== null) { $scope = $scope->enterNamespace($stmt->name->toString()); } - $scope = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback)->getScope(); + $scope = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context)->getScope(); $hasYield = false; $throwPoints = []; + $impurePoints = []; } elseif ($stmt instanceof Node\Stmt\Trait_) { - return new StatementResult($scope, false, false, [], []); + return new StatementResult($scope, false, false, [], [], []); } elseif ($stmt instanceof Node\Stmt\ClassLike) { $hasYield = false; $throwPoints = []; + $impurePoints = []; if (isset($stmt->namespacedName)) { $classReflection = $this->getCurrentClassReflection($stmt, $stmt->namespacedName->toString(), $scope); $classScope = $scope->enterClass($classReflection); @@ -649,21 +825,26 @@ private function processStmtNode( } $classStatementsGatherer = new ClassStatementsGatherer($classReflection, $nodeCallback); - $this->processAttributeGroups($stmt->attrGroups, $classScope, $classStatementsGatherer); + $this->processAttributeGroups($stmt, $stmt->attrGroups, $classScope, $classStatementsGatherer); - $this->processStmtNodes($stmt, $stmt->stmts, $classScope, $classStatementsGatherer); - $nodeCallback(new ClassPropertiesNode($stmt, $this->readWritePropertiesExtensionProvider, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls()), $classScope); - $nodeCallback(new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls()), $classScope); - $nodeCallback(new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches()), $classScope); + $this->processStmtNodes($stmt, $stmt->stmts, $classScope, $classStatementsGatherer, $context); + $nodeCallback(new ClassPropertiesNode($stmt, $this->readWritePropertiesExtensionProvider, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls(), $classStatementsGatherer->getReturnStatementsNodes(), $classReflection), $classScope); + $nodeCallback(new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls(), $classReflection), $classScope); + $nodeCallback(new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches(), $classReflection), $classScope); $classReflection->evictPrivateSymbols(); + $this->calledMethodResults = []; } elseif ($stmt instanceof Node\Stmt\Property) { $hasYield = false; $throwPoints = []; - $this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback); + $impurePoints = []; + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); foreach ($stmt->props as $prop) { - $this->processStmtNode($prop, $scope, $nodeCallback); - [,,,,,,,,,,$isReadOnly, $docComment, $varTags] = $this->getPhpDocs($scope, $stmt); + $nodeCallback($prop, $scope); + if ($prop->default !== null) { + $this->processExprNode($stmt, $prop->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + } + [,,,,,,,,,,,,$isReadOnly, $docComment, ,,,$varTags, $isAllowedPrivateMutation] = $this->getPhpDocs($scope, $stmt); if (!$scope->isInClass()) { throw new ShouldNotHappenException(); } @@ -683,10 +864,13 @@ private function processStmtNode( $docComment, $phpDocType, false, + false, $prop, $isReadOnly, $scope->isInTrait(), $scope->getClassReflection()->isReadOnly(), + $isAllowedPrivateMutation, + $scope->getClassReflection(), ), $scope, ); @@ -695,34 +879,31 @@ private function processStmtNode( if ($stmt->type !== null) { $nodeCallback($stmt->type, $scope); } - } elseif ($stmt instanceof Node\Stmt\PropertyProperty) { - $hasYield = false; - $throwPoints = []; - if ($stmt->default !== null) { - $this->processExprNode($stmt->default, $scope, $nodeCallback, ExpressionContext::createDeep()); - } } elseif ($stmt instanceof Throw_) { - $result = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = $result->getThrowPoints(); $throwPoints[] = ThrowPoint::createExplicit($result->getScope(), $scope->getType($stmt->expr), $stmt, false); + $impurePoints = $result->getImpurePoints(); return new StatementResult($result->getScope(), $result->hasYield(), true, [ new StatementExitPoint($stmt, $scope), - ], $throwPoints); + ], $throwPoints, $impurePoints); } elseif ($stmt instanceof If_) { - $conditionType = $scope->getType($stmt->cond)->toBoolean(); - $ifAlwaysTrue = $conditionType instanceof ConstantBooleanType && $conditionType->getValue(); - $condResult = $this->processExprNode($stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); + $conditionType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean(); + $ifAlwaysTrue = $conditionType->isTrue()->yes(); + $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); $exitPoints = []; $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); $finalScope = null; $alwaysTerminating = true; $hasYield = $condResult->hasYield(); - $branchScopeStatementResult = $this->processStmtNodes($stmt, $stmt->stmts, $condResult->getTruthyScope(), $nodeCallback); + $branchScopeStatementResult = $this->processStmtNodes($stmt, $stmt->stmts, $condResult->getTruthyScope(), $nodeCallback, $context); if (!$conditionType instanceof ConstantBooleanType || $conditionType->getValue()) { $exitPoints = $branchScopeStatementResult->getExitPoints(); $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints()); $branchScope = $branchScopeStatementResult->getScope(); $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? null : $branchScope; $alwaysTerminating = $branchScopeStatementResult->isAlwaysTerminating(); @@ -735,11 +916,12 @@ private function processStmtNode( $condScope = $scope; foreach ($stmt->elseifs as $elseif) { $nodeCallback($elseif, $scope); - $elseIfConditionType = $condScope->getType($elseif->cond)->toBoolean(); - $condResult = $this->processExprNode($elseif->cond, $condScope, $nodeCallback, ExpressionContext::createDeep()); + $elseIfConditionType = ($this->treatPhpDocTypesAsCertain ? $condScope->getType($elseif->cond) : $scope->getNativeType($elseif->cond))->toBoolean(); + $condResult = $this->processExprNode($stmt, $elseif->cond, $condScope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints()); $condScope = $condResult->getScope(); - $branchScopeStatementResult = $this->processStmtNodes($elseif, $elseif->stmts, $condResult->getTruthyScope(), $nodeCallback); + $branchScopeStatementResult = $this->processStmtNodes($elseif, $elseif->stmts, $condResult->getTruthyScope(), $nodeCallback, $context); if ( !$ifAlwaysTrue @@ -753,6 +935,7 @@ private function processStmtNode( ) { $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints()); $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints()); $branchScope = $branchScopeStatementResult->getScope(); $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope); $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating(); @@ -760,8 +943,7 @@ private function processStmtNode( } if ( - $elseIfConditionType instanceof ConstantBooleanType - && $elseIfConditionType->getValue() + $elseIfConditionType->isTrue()->yes() ) { $lastElseIfConditionIsTrue = true; } @@ -771,17 +953,18 @@ private function processStmtNode( } if ($stmt->else === null) { - if (!$ifAlwaysTrue) { + if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) { $finalScope = $scope->mergeWith($finalScope); $alwaysTerminating = false; } } else { $nodeCallback($stmt->else, $scope); - $branchScopeStatementResult = $this->processStmtNodes($stmt->else, $stmt->else->stmts, $scope, $nodeCallback); + $branchScopeStatementResult = $this->processStmtNodes($stmt->else, $stmt->else->stmts, $scope, $nodeCallback, $context); if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) { $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints()); $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints()); $branchScope = $branchScopeStatementResult->getScope(); $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope); $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating(); @@ -793,50 +976,56 @@ private function processStmtNode( $finalScope = $scope; } - return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints); + return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints); } elseif ($stmt instanceof Node\Stmt\TraitUse) { $hasYield = false; $throwPoints = []; + $impurePoints = []; $this->processTraitUse($stmt, $scope, $nodeCallback); } elseif ($stmt instanceof Foreach_) { - $condResult = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $condResult = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); $scope = $condResult->getScope(); $arrayComparisonExpr = new BinaryOp\NotIdentical( $stmt->expr, new Array_([]), ); - $inForeachScope = $scope; if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { - $inForeachScope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt); + $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt); } - $nodeCallback(new InForeachNode($stmt), $inForeachScope); - $bodyScope = $this->enterForeach($scope->filterByTruthyValue($arrayComparisonExpr), $stmt); - $count = 0; - do { - $prevScope = $bodyScope; - $bodyScope = $bodyScope->mergeWith($scope->filterByTruthyValue($arrayComparisonExpr)); - $bodyScope = $this->enterForeach($bodyScope, $stmt); - $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { - })->filterOutLoopExitPoints(); - $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); - $bodyScope = $bodyScopeResult->getScope(); - foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); - } - if ($bodyScope->equals($prevScope)) { - break; - } + $nodeCallback(new InForeachNode($stmt), $scope); + $originalScope = $scope; + $bodyScope = $scope; - if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $bodyScope = $prevScope->generalizeWith($bodyScope); - } - $count++; - } while (!$alwaysTerminating && $count < self::LOOP_SCOPE_ITERATIONS); + if ($context->isTopLevel()) { + $originalScope = $this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope; + $bodyScope = $this->enterForeach($originalScope, $originalScope, $stmt); + $count = 0; + do { + $prevScope = $bodyScope; + $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope); + $bodyScope = $this->enterForeach($bodyScope, $originalScope, $stmt); + $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { + }, $context->enterDeep())->filterOutLoopExitPoints(); + $bodyScope = $bodyScopeResult->getScope(); + foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + } + if ($bodyScope->equals($prevScope)) { + break; + } + + if ($count >= self::GENERALIZE_AFTER_ITERATION) { + $bodyScope = $prevScope->generalizeWith($bodyScope); + } + $count++; + } while ($count < self::LOOP_SCOPE_ITERATIONS); + } - $bodyScope = $bodyScope->mergeWith($scope->filterByTruthyValue($arrayComparisonExpr)); - $bodyScope = $this->enterForeach($bodyScope, $stmt); - $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback)->filterOutLoopExitPoints(); + $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope); + $bodyScope = $this->enterForeach($bodyScope, $originalScope, $stmt); + $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints(); $finalScope = $finalScopeResult->getScope(); foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope); @@ -845,15 +1034,20 @@ private function processStmtNode( $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); } - $isIterableAtLeastOnce = $scope->getType($stmt->expr)->isIterableAtLeastOnce(); - if ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) { + $exprType = $scope->getType($stmt->expr); + $isIterableAtLeastOnce = $exprType->isIterableAtLeastOnce(); + if ($exprType->isIterable()->no() || $isIterableAtLeastOnce->maybe()) { + $finalScope = $finalScope->mergeWith($scope->filterByTruthyValue(new BooleanOr( + new BinaryOp\Identical( + $stmt->expr, + new Array_([]), + ), + new FuncCall(new Name\FullyQualified('is_object'), [ + new Arg($stmt->expr), + ]), + ))); + } elseif ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) { $finalScope = $scope; - } elseif ($isIterableAtLeastOnce->maybe()) { - if ($this->polluteScopeWithAlwaysIterableForeach) { - $finalScope = $finalScope->mergeWith($scope->filterByFalseyValue($arrayComparisonExpr)); - } else { - $finalScope = $finalScope->mergeWith($scope); - } } elseif (!$this->polluteScopeWithAlwaysIterableForeach) { $finalScope = $scope->processAlwaysIterableForeachScopeWithoutPollute($finalScope); // get types from finalScope, but don't create new variables @@ -861,6 +1055,7 @@ private function processStmtNode( if (!$isIterableAtLeastOnce->no()) { $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints()); } if (!(new ObjectType(Traversable::class))->isSuperTypeOf($scope->getType($stmt->expr))->no()) { $throwPoints[] = ThrowPoint::createImplicit($scope, $stmt->expr); @@ -872,38 +1067,41 @@ private function processStmtNode( $isIterableAtLeastOnce->yes() && $finalScopeResult->isAlwaysTerminating(), $finalScopeResult->getExitPointsForOuterLoop(), $throwPoints, + $impurePoints, ); } elseif ($stmt instanceof While_) { - $condResult = $this->processExprNode($stmt->cond, $scope, static function (): void { + $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, static function (): void { }, ExpressionContext::createDeep()); $bodyScope = $condResult->getTruthyScope(); - $count = 0; - do { - $prevScope = $bodyScope; - $bodyScope = $bodyScope->mergeWith($scope); - $bodyScope = $this->processExprNode($stmt->cond, $bodyScope, static function (): void { - }, ExpressionContext::createDeep())->getTruthyScope(); - $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { - })->filterOutLoopExitPoints(); - $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); - $bodyScope = $bodyScopeResult->getScope(); - foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); - } - if ($bodyScope->equals($prevScope)) { - break; - } - if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $bodyScope = $prevScope->generalizeWith($bodyScope); - } - $count++; - } while (!$alwaysTerminating && $count < self::LOOP_SCOPE_ITERATIONS); + if ($context->isTopLevel()) { + $count = 0; + do { + $prevScope = $bodyScope; + $bodyScope = $bodyScope->mergeWith($scope); + $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, static function (): void { + }, ExpressionContext::createDeep())->getTruthyScope(); + $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { + }, $context->enterDeep())->filterOutLoopExitPoints(); + $bodyScope = $bodyScopeResult->getScope(); + foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + } + if ($bodyScope->equals($prevScope)) { + break; + } + + if ($count >= self::GENERALIZE_AFTER_ITERATION) { + $bodyScope = $prevScope->generalizeWith($bodyScope); + } + $count++; + } while ($count < self::LOOP_SCOPE_ITERATIONS); + } $bodyScope = $bodyScope->mergeWith($scope); $bodyScopeMaybeRan = $bodyScope; - $bodyScope = $this->processExprNode($stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); - $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback)->filterOutLoopExitPoints(); + $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); + $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints(); $finalScope = $finalScopeResult->getScope()->filterByFalseyValue($stmt->cond); foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { $finalScope = $finalScope->mergeWith($continueExitPoint->getScope()); @@ -913,11 +1111,11 @@ private function processStmtNode( $finalScope = $finalScope->mergeWith($breakExitPoint->getScope()); } - $beforeCondBooleanType = $scope->getType($stmt->cond)->toBoolean(); - $condBooleanType = $bodyScopeMaybeRan->getType($stmt->cond)->toBoolean(); - $isIterableAtLeastOnce = $beforeCondBooleanType instanceof ConstantBooleanType && $beforeCondBooleanType->getValue(); - $alwaysIterates = $condBooleanType instanceof ConstantBooleanType && $condBooleanType->getValue(); - $neverIterates = $condBooleanType instanceof ConstantBooleanType && !$condBooleanType->getValue(); + $beforeCondBooleanType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean(); + $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScopeMaybeRan->getType($stmt->cond) : $bodyScopeMaybeRan->getNativeType($stmt->cond))->toBoolean(); + $isIterableAtLeastOnce = $beforeCondBooleanType->isTrue()->yes(); + $alwaysIterates = $condBooleanType->isTrue()->yes() && $context->isTopLevel(); + $neverIterates = $condBooleanType->isFalse()->yes() && $context->isTopLevel(); $nodeCallback(new BreaklessWhileLoopNode($stmt, $finalScopeResult->getExitPoints()), $bodyScopeMaybeRan); if ($alwaysIterates) { @@ -936,8 +1134,10 @@ private function processStmtNode( } $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); if (!$neverIterates) { $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints()); } return new StatementResult( @@ -946,6 +1146,7 @@ private function processStmtNode( $isAlwaysTerminating, $finalScopeResult->getExitPointsForOuterLoop(), $throwPoints, + $impurePoints, ); } elseif ($stmt instanceof Do_) { $finalScope = null; @@ -953,41 +1154,45 @@ private function processStmtNode( $count = 0; $hasYield = false; $throwPoints = []; + $impurePoints = []; + + if ($context->isTopLevel()) { + do { + $prevScope = $bodyScope; + $bodyScope = $bodyScope->mergeWith($scope); + $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { + }, $context->enterDeep())->filterOutLoopExitPoints(); + $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); + $bodyScope = $bodyScopeResult->getScope(); + foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + } + $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope); + foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { + $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); + } + $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, static function (): void { + }, ExpressionContext::createDeep())->getTruthyScope(); + if ($bodyScope->equals($prevScope)) { + break; + } - do { - $prevScope = $bodyScope; - $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { - })->filterOutLoopExitPoints(); - $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); - $bodyScope = $bodyScopeResult->getScope(); - foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); - } - $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope); - foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { - $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); - } - $bodyScope = $this->processExprNode($stmt->cond, $bodyScope, static function (): void { - }, ExpressionContext::createDeep())->getTruthyScope(); - if ($bodyScope->equals($prevScope)) { - break; - } - - if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $bodyScope = $prevScope->generalizeWith($bodyScope); - } - $count++; - } while (!$alwaysTerminating && $count < self::LOOP_SCOPE_ITERATIONS); + if ($count >= self::GENERALIZE_AFTER_ITERATION) { + $bodyScope = $prevScope->generalizeWith($bodyScope); + } + $count++; + } while ($count < self::LOOP_SCOPE_ITERATIONS); - $bodyScope = $bodyScope->mergeWith($scope); + $bodyScope = $bodyScope->mergeWith($scope); + } - $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback)->filterOutLoopExitPoints(); + $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints(); $bodyScope = $bodyScopeResult->getScope(); foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); } - $condBooleanType = $bodyScope->getType($stmt->cond)->toBoolean(); - $alwaysIterates = $condBooleanType instanceof ConstantBooleanType && $condBooleanType->getValue(); + $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScope->getType($stmt->cond) : $bodyScope->getNativeType($stmt->cond))->toBoolean(); + $alwaysIterates = $condBooleanType->isTrue()->yes() && $context->isTopLevel(); $nodeCallback(new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->getExitPoints()), $bodyScope); @@ -1001,12 +1206,13 @@ private function processStmtNode( $finalScope = $scope; } if (!$alwaysTerminating) { - $condResult = $this->processExprNode($stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep()); + $condResult = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep()); $hasYield = $condResult->hasYield(); $throwPoints = $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); $finalScope = $condResult->getFalseyScope(); } else { - $this->processExprNode($stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep()); + $this->processExprNode($stmt, $stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep()); } foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); @@ -1018,25 +1224,29 @@ private function processStmtNode( $alwaysTerminating, $bodyScopeResult->getExitPointsForOuterLoop(), array_merge($throwPoints, $bodyScopeResult->getThrowPoints()), + array_merge($impurePoints, $bodyScopeResult->getImpurePoints()), ); } elseif ($stmt instanceof For_) { $initScope = $scope; $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($stmt->init as $initExpr) { - $initResult = $this->processExprNode($initExpr, $initScope, $nodeCallback, ExpressionContext::createTopLevel()); + $initResult = $this->processExprNode($stmt, $initExpr, $initScope, $nodeCallback, ExpressionContext::createTopLevel()); $initScope = $initResult->getScope(); $hasYield = $hasYield || $initResult->hasYield(); $throwPoints = array_merge($throwPoints, $initResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $initResult->getImpurePoints()); } $bodyScope = $initScope; $isIterableAtLeastOnce = TrinaryLogic::createYes(); foreach ($stmt->cond as $condExpr) { - $condResult = $this->processExprNode($condExpr, $bodyScope, static function (): void { + $condResult = $this->processExprNode($stmt, $condExpr, $bodyScope, static function (): void { }, ExpressionContext::createDeep()); $initScope = $condResult->getScope(); - $condTruthiness = $condResult->getScope()->getType($condExpr)->toBoolean(); + $condResultScope = $condResult->getScope(); + $condTruthiness = ($this->treatPhpDocTypesAsCertain ? $condResultScope->getType($condExpr) : $condResultScope->getNativeType($condExpr))->toBoolean(); if ($condTruthiness instanceof ConstantBooleanType) { $condTruthinessTrinary = TrinaryLogic::createFromBoolean($condTruthiness->getValue()); } else { @@ -1045,48 +1255,51 @@ private function processStmtNode( $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthinessTrinary); $hasYield = $hasYield || $condResult->hasYield(); $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints()); $bodyScope = $condResult->getTruthyScope(); } - $count = 0; - do { - $prevScope = $bodyScope; - $bodyScope = $bodyScope->mergeWith($initScope); - foreach ($stmt->cond as $condExpr) { - $bodyScope = $this->processExprNode($condExpr, $bodyScope, static function (): void { - }, ExpressionContext::createDeep())->getTruthyScope(); - } - $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { - })->filterOutLoopExitPoints(); - $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); - $bodyScope = $bodyScopeResult->getScope(); - foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); - } - foreach ($stmt->loop as $loopExpr) { - $exprResult = $this->processExprNode($loopExpr, $bodyScope, static function (): void { - }, ExpressionContext::createTopLevel()); - $bodyScope = $exprResult->getScope(); - $hasYield = $hasYield || $exprResult->hasYield(); - $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); - } + if ($context->isTopLevel()) { + $count = 0; + do { + $prevScope = $bodyScope; + $bodyScope = $bodyScope->mergeWith($initScope); + foreach ($stmt->cond as $condExpr) { + $bodyScope = $this->processExprNode($stmt, $condExpr, $bodyScope, static function (): void { + }, ExpressionContext::createDeep())->getTruthyScope(); + } + $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { + }, $context->enterDeep())->filterOutLoopExitPoints(); + $bodyScope = $bodyScopeResult->getScope(); + foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + } + foreach ($stmt->loop as $loopExpr) { + $exprResult = $this->processExprNode($stmt, $loopExpr, $bodyScope, static function (): void { + }, ExpressionContext::createTopLevel()); + $bodyScope = $exprResult->getScope(); + $hasYield = $hasYield || $exprResult->hasYield(); + $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); + } - if ($bodyScope->equals($prevScope)) { - break; - } + if ($bodyScope->equals($prevScope)) { + break; + } - if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $bodyScope = $prevScope->generalizeWith($bodyScope); - } - $count++; - } while (!$alwaysTerminating && $count < self::LOOP_SCOPE_ITERATIONS); + if ($count >= self::GENERALIZE_AFTER_ITERATION) { + $bodyScope = $prevScope->generalizeWith($bodyScope); + } + $count++; + } while ($count < self::LOOP_SCOPE_ITERATIONS); + } $bodyScope = $bodyScope->mergeWith($initScope); foreach ($stmt->cond as $condExpr) { - $bodyScope = $this->processExprNode($condExpr, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); + $bodyScope = $this->processExprNode($stmt, $condExpr, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); } - $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback)->filterOutLoopExitPoints(); + $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints(); $finalScope = $finalScopeResult->getScope(); foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope); @@ -1094,7 +1307,7 @@ private function processStmtNode( $loopScope = $finalScope; foreach ($stmt->loop as $loopExpr) { - $loopScope = $this->processExprNode($loopExpr, $loopScope, $nodeCallback, ExpressionContext::createTopLevel())->getScope(); + $loopScope = $this->processExprNode($stmt, $loopExpr, $loopScope, $nodeCallback, ExpressionContext::createTopLevel())->getScope(); } $finalScope = $finalScope->generalizeWith($loopScope); foreach ($stmt->cond as $condExpr) { @@ -1130,9 +1343,10 @@ private function processStmtNode( false/* $finalScopeResult->isAlwaysTerminating() && $isAlwaysIterable*/, $finalScopeResult->getExitPointsForOuterLoop(), array_merge($throwPoints, $finalScopeResult->getThrowPoints()), + array_merge($impurePoints, $finalScopeResult->getImpurePoints()), ); } elseif ($stmt instanceof Switch_) { - $condResult = $this->processExprNode($stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); + $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $condResult->getScope(); $scopeForBranches = $scope; $finalScope = null; @@ -1142,21 +1356,23 @@ private function processStmtNode( $hasYield = $condResult->hasYield(); $exitPointsForOuterLoop = []; $throwPoints = $condResult->getThrowPoints(); + $impurePoints = $condResult->getImpurePoints(); foreach ($stmt->cases as $caseNode) { if ($caseNode->cond !== null) { $condExpr = new BinaryOp\Equal($stmt->cond, $caseNode->cond); - $caseResult = $this->processExprNode($caseNode->cond, $scopeForBranches, $nodeCallback, ExpressionContext::createDeep()); + $caseResult = $this->processExprNode($stmt, $caseNode->cond, $scopeForBranches, $nodeCallback, ExpressionContext::createDeep()); $scopeForBranches = $caseResult->getScope(); $hasYield = $hasYield || $caseResult->hasYield(); $throwPoints = array_merge($throwPoints, $caseResult->getThrowPoints()); - $branchScope = $scopeForBranches->filterByTruthyValue($condExpr); + $impurePoints = array_merge($impurePoints, $caseResult->getImpurePoints()); + $branchScope = $caseResult->getTruthyScope()->filterByTruthyValue($condExpr); } else { $hasDefaultCase = true; $branchScope = $scopeForBranches; } $branchScope = $branchScope->mergeWith($prevScope); - $branchScopeResult = $this->processStmtNodes($caseNode, $caseNode->stmts, $branchScope, $nodeCallback); + $branchScopeResult = $this->processStmtNodes($caseNode, $caseNode->stmts, $branchScope, $nodeCallback, $context); $branchScope = $branchScopeResult->getScope(); $branchFinalScopeResult = $branchScopeResult->filterOutLoopExitPoints(); $hasYield = $hasYield || $branchFinalScopeResult->hasYield(); @@ -1169,6 +1385,7 @@ private function processStmtNode( } $exitPointsForOuterLoop = array_merge($exitPointsForOuterLoop, $branchFinalScopeResult->getExitPointsForOuterLoop()); $throwPoints = array_merge($throwPoints, $branchFinalScopeResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $branchFinalScopeResult->getImpurePoints()); if ($branchScopeResult->isAlwaysTerminating()) { $alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating(); $prevScope = null; @@ -1198,9 +1415,9 @@ private function processStmtNode( $finalScope = $scope->mergeWith($finalScope); } - return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPointsForOuterLoop, $throwPoints); + return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPointsForOuterLoop, $throwPoints, $impurePoints); } elseif ($stmt instanceof TryCatch) { - $branchScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback); + $branchScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context); $branchScope = $branchScopeResult->getScope(); $finalScope = $branchScopeResult->isAlwaysTerminating() ? null : $branchScope; @@ -1226,64 +1443,112 @@ private function processStmtNode( } $throwPoints = $branchScopeResult->getThrowPoints(); + $impurePoints = $branchScopeResult->getImpurePoints(); $throwPointsForLater = []; $pastCatchTypes = new NeverType(); foreach ($stmt->catches as $catchNode) { $nodeCallback($catchNode, $scope); - $catchType = TypeCombinator::union(...array_map(static fn (Name $name): Type => new ObjectType($name->toString()), $catchNode->types)); - $originalCatchType = $catchType; - $isThrowable = $originalCatchType instanceof TypeWithClassName && strtolower($originalCatchType->getClassName()) === 'throwable'; - $catchType = TypeCombinator::remove($catchType, $pastCatchTypes); + $originalCatchTypes = array_map(static fn (Name $name): Type => new ObjectType($name->toString()), $catchNode->types); + $catchTypes = array_map(static fn (Type $type): Type => TypeCombinator::remove($type, $pastCatchTypes), $originalCatchTypes); + + $originalCatchType = TypeCombinator::union(...$originalCatchTypes); + $catchType = TypeCombinator::union(...$catchTypes); $pastCatchTypes = TypeCombinator::union($pastCatchTypes, $originalCatchType); + $matchingThrowPoints = []; - $newThrowPoints = []; - foreach ($throwPoints as $throwPoint) { - if (!$throwPoint->isExplicit() && !$catchType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) { + $matchingCatchTypes = array_fill_keys(array_keys($originalCatchTypes), false); + + // throwable matches all + foreach ($originalCatchTypes as $catchTypeIndex => $catchTypeItem) { + if (!$catchTypeItem->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) { continue; } - $isSuperType = $catchType->isSuperTypeOf($throwPoint->getType()); - if ($isSuperType->no()) { - continue; + + foreach ($throwPoints as $throwPointIndex => $throwPoint) { + $matchingThrowPoints[$throwPointIndex] = $throwPoint; + $matchingCatchTypes[$catchTypeIndex] = true; } - $matchingThrowPoints[] = $throwPoint; } - $hasExplicit = count($matchingThrowPoints) > 0; - foreach ($throwPoints as $throwPoint) { - $isSuperType = $catchType->isSuperTypeOf($throwPoint->getType()); - if (!$hasExplicit && !$isSuperType->no()) { - $matchingThrowPoints[] = $throwPoint; - } - if ($isSuperType->yes()) { - continue; + + // explicit only + if (count($matchingThrowPoints) === 0) { + foreach ($throwPoints as $throwPointIndex => $throwPoint) { + foreach ($catchTypes as $catchTypeIndex => $catchTypeItem) { + if ($catchTypeItem->isSuperTypeOf($throwPoint->getType())->no()) { + continue; + } + + $matchingCatchTypes[$catchTypeIndex] = true; + if (!$throwPoint->isExplicit()) { + continue; + } + $matchingThrowPoints[$throwPointIndex] = $throwPoint; + } } - if ($isThrowable) { - continue; + } + + // implicit only + if (count($matchingThrowPoints) === 0) { + foreach ($throwPoints as $throwPointIndex => $throwPoint) { + if ($throwPoint->isExplicit()) { + continue; + } + + foreach ($catchTypes as $catchTypeIndex => $catchTypeItem) { + if ($catchTypeItem->isSuperTypeOf($throwPoint->getType())->no()) { + continue; + } + + $matchingThrowPoints[$throwPointIndex] = $throwPoint; + } } - $newThrowPoints[] = $throwPoint->subtractCatchType($catchType); } - $throwPoints = $newThrowPoints; + // include previously removed throw points if (count($matchingThrowPoints) === 0) { - $throwableThrowPoints = []; if ($originalCatchType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) { foreach ($branchScopeResult->getThrowPoints() as $originalThrowPoint) { if (!$originalThrowPoint->canContainAnyThrowable()) { continue; } - $throwableThrowPoints[] = $originalThrowPoint; + $matchingThrowPoints[] = $originalThrowPoint; + $matchingCatchTypes = array_fill_keys(array_keys($originalCatchTypes), true); + } + } + } + + // emit error + if ($this->detectDeadTypeInMultiCatch) { + foreach ($matchingCatchTypes as $catchTypeIndex => $matched) { + if ($matched) { + continue; } + $nodeCallback(new CatchWithUnthrownExceptionNode($catchNode, $catchTypes[$catchTypeIndex], $originalCatchTypes[$catchTypeIndex]), $scope); } + } - if (count($throwableThrowPoints) === 0) { + if (count($matchingThrowPoints) === 0) { + if (!$this->detectDeadTypeInMultiCatch) { $nodeCallback(new CatchWithUnthrownExceptionNode($catchNode, $catchType, $originalCatchType), $scope); + } + continue; + } + + // recompute throw points + $newThrowPoints = []; + foreach ($throwPoints as $throwPoint) { + $newThrowPoint = $throwPoint->subtractCatchType($originalCatchType); + + if ($newThrowPoint->getType() instanceof NeverType) { continue; } - $matchingThrowPoints = $throwableThrowPoints; + $newThrowPoints[] = $newThrowPoint; } + $throwPoints = $newThrowPoints; $catchScope = null; foreach ($matchingThrowPoints as $matchingThrowPoint) { @@ -1303,13 +1568,14 @@ private function processStmtNode( $variableName = $catchNode->var->name; } - $catchScopeResult = $this->processStmtNodes($catchNode, $catchNode->stmts, $catchScope->enterCatchType($catchType, $variableName), $nodeCallback); + $catchScopeResult = $this->processStmtNodes($catchNode, $catchNode->stmts, $catchScope->enterCatchType($catchType, $variableName), $nodeCallback, $context); $catchScopeForFinally = $catchScopeResult->getScope(); $finalScope = $catchScopeResult->isAlwaysTerminating() ? $finalScope : $catchScopeResult->getScope()->mergeWith($finalScope); $alwaysTerminating = $alwaysTerminating && $catchScopeResult->isAlwaysTerminating(); $hasYield = $hasYield || $catchScopeResult->hasYield(); $catchThrowPoints = $catchScopeResult->getThrowPoints(); + $impurePoints = array_merge($impurePoints, $catchScopeResult->getImpurePoints()); $throwPointsForLater = array_merge($throwPointsForLater, $catchThrowPoints); if ($finallyScope !== null) { @@ -1347,10 +1613,11 @@ private function processStmtNode( if ($finallyScope !== null && $stmt->finally !== null) { $originalFinallyScope = $finallyScope; - $finallyResult = $this->processStmtNodes($stmt->finally, $stmt->finally->stmts, $finallyScope, $nodeCallback); + $finallyResult = $this->processStmtNodes($stmt->finally, $stmt->finally->stmts, $finallyScope, $nodeCallback, $context); $alwaysTerminating = $alwaysTerminating || $finallyResult->isAlwaysTerminating(); $hasYield = $hasYield || $finallyResult->hasYield(); $throwPointsForLater = array_merge($throwPointsForLater, $finallyResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $finallyResult->getImpurePoints()); $finallyScope = $finallyResult->getScope(); $finalScope = $finallyResult->isAlwaysTerminating() ? $finalScope : $finalScope->processFinallyScope($finallyScope, $originalFinallyScope); if (count($finallyResult->getExitPoints()) > 0) { @@ -1362,96 +1629,194 @@ private function processStmtNode( $exitPoints = array_merge($exitPoints, $finallyResult->getExitPoints()); } - return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, array_merge($throwPoints, $throwPointsForLater)); + return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, array_merge($throwPoints, $throwPointsForLater), $impurePoints); } elseif ($stmt instanceof Unset_) { $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($stmt->vars as $var) { $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var); - $scope = $this->processExprNode($var, $scope, $nodeCallback, ExpressionContext::createDeep())->getScope(); + $exprResult = $this->processExprNode($stmt, $var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $scope = $exprResult->getScope(); $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var); - $scope = $scope->unsetExpression($var); + $hasYield = $hasYield || $exprResult->hasYield(); + $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); + if ($var instanceof ArrayDimFetch && $var->dim !== null) { + $cloningTraverser = new NodeTraverser(); + $cloningTraverser->addVisitor(new CloningVisitor()); + + /** @var Expr $clonedVar */ + [$clonedVar] = $cloningTraverser->traverse([$var->var]); + + $traverser = new NodeTraverser(); + $traverser->addVisitor(new class () extends NodeVisitorAbstract { + + public function leaveNode(Node $node): ?ExistingArrayDimFetch + { + if (!$node instanceof ArrayDimFetch || $node->dim === null) { + return null; + } + + return new ExistingArrayDimFetch($node->var, $node->dim); + } + + }); + + /** @var Expr $clonedVar */ + [$clonedVar] = $traverser->traverse([$clonedVar]); + $scope = $this->processAssignVar( + $scope, + $stmt, + $clonedVar, + new UnsetOffsetExpr($var->var, $var->dim), + static function (Node $node, Scope $scope) use ($nodeCallback): void { + if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { + return; + } + + $nodeCallback($node, $scope); + }, + ExpressionContext::createDeep(), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), + false, + )->getScope(); + } else { + $scope = $scope->invalidateExpression($var); + } + } } elseif ($stmt instanceof Node\Stmt\Use_) { $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($stmt->uses as $use) { - $this->processStmtNode($use, $scope, $nodeCallback); + $nodeCallback($use, $scope); } } elseif ($stmt instanceof Node\Stmt\Global_) { $hasYield = false; $throwPoints = []; + $impurePoints = [ + new ImpurePoint( + $scope, + $stmt, + 'global', + 'global variable', + true, + ), + ]; $vars = []; foreach ($stmt->vars as $var) { if (!$var instanceof Variable) { throw new ShouldNotHappenException(); } $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var); - $this->processExprNode($var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $varResult = $this->processExprNode($stmt, $var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints()); $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var); if (!is_string($var->name)) { continue; } - $scope = $scope->assignVariable($var->name, new MixedType()); + $scope = $scope->assignVariable($var->name, new MixedType(), new MixedType()); $vars[] = $var->name; } $scope = $this->processVarAnnotation($scope, $vars, $stmt); } elseif ($stmt instanceof Static_) { $hasYield = false; $throwPoints = []; + $impurePoints = [ + new ImpurePoint( + $scope, + $stmt, + 'static', + 'static variable', + true, + ), + ]; $vars = []; foreach ($stmt->vars as $var) { - $scope = $this->processStmtNode($var, $scope, $nodeCallback)->getScope(); if (!is_string($var->var->name)) { - continue; + throw new ShouldNotHappenException(); + } + + if ($var->default !== null) { + $defaultExprResult = $this->processExprNode($stmt, $var->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = array_merge($impurePoints, $defaultExprResult->getImpurePoints()); } + $scope = $scope->enterExpressionAssign($var->var); + $varResult = $this->processExprNode($stmt, $var->var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints()); + $scope = $scope->exitExpressionAssign($var->var); + + $scope = $scope->assignVariable($var->var->name, new MixedType(), new MixedType()); $vars[] = $var->var->name; } $scope = $this->processVarAnnotation($scope, $vars, $stmt); - } elseif ($stmt instanceof StaticVar) { + } elseif ($stmt instanceof Node\Stmt\Const_) { $hasYield = false; $throwPoints = []; - if (!is_string($stmt->var->name)) { - throw new ShouldNotHappenException(); - } - if ($stmt->default !== null) { - $this->processExprNode($stmt->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = []; + foreach ($stmt->consts as $const) { + $nodeCallback($const, $scope); + $constResult = $this->processExprNode($stmt, $const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints()); + if ($const->namespacedName !== null) { + $constantName = new Name\FullyQualified($const->namespacedName->toString()); + } else { + $constantName = new Name\FullyQualified($const->name->toString()); + } + $scope = $scope->assignExpression(new ConstFetch($constantName), $scope->getType($const->value), $scope->getNativeType($const->value)); } - $scope = $scope->enterExpressionAssign($stmt->var); - $this->processExprNode($stmt->var, $scope, $nodeCallback, ExpressionContext::createDeep()); - $scope = $scope->exitExpressionAssign($stmt->var); - $scope = $scope->assignVariable($stmt->var->name, new MixedType()); - } elseif ($stmt instanceof Node\Stmt\Const_ || $stmt instanceof Node\Stmt\ClassConst) { + } elseif ($stmt instanceof Node\Stmt\ClassConst) { $hasYield = false; $throwPoints = []; - if ($stmt instanceof Node\Stmt\ClassConst) { - $this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback); - } + $impurePoints = []; + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); foreach ($stmt->consts as $const) { $nodeCallback($const, $scope); - $this->processExprNode($const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); - if ($scope->getNamespace() !== null) { - $constName = [$scope->getNamespace(), $const->name->toString()]; - } else { - $constName = $const->name->toString(); + $constResult = $this->processExprNode($stmt, $const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints()); + if ($scope->getClassReflection() === null) { + throw new ShouldNotHappenException(); } - $scope = $scope->assignExpression(new ConstFetch(new Name\FullyQualified($constName)), $scope->getType($const->value)); + $scope = $scope->assignExpression( + new Expr\ClassConstFetch(new Name\FullyQualified($scope->getClassReflection()->getName()), $const->name), + $scope->getType($const->value), + $scope->getNativeType($const->value), + ); + } + } elseif ($stmt instanceof Node\Stmt\EnumCase) { + $hasYield = false; + $throwPoints = []; + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); + $impurePoints = []; + if ($stmt->expr !== null) { + $exprResult = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $impurePoints = $exprResult->getImpurePoints(); } } elseif ($stmt instanceof Node\Stmt\Nop) { - $scope = $this->processStmtVarAnnotation($scope, $stmt, null); $hasYield = false; $throwPoints = $overridingThrowPoints ?? []; + $impurePoints = []; + } elseif ($stmt instanceof Node\Stmt\GroupUse) { + $hasYield = false; + $throwPoints = []; + foreach ($stmt->uses as $use) { + $nodeCallback($use, $scope); + } + $impurePoints = []; } else { $hasYield = false; $throwPoints = $overridingThrowPoints ?? []; + $impurePoints = []; } - return new StatementResult($scope, $hasYield, false, [], $throwPoints); + return new StatementResult($scope, $hasYield, false, [], $throwPoints, $impurePoints); } /** @@ -1476,7 +1841,7 @@ private function getOverridingThrowPoints(Node\Stmt $statement, MutatingScope $s $throwsTag = $resolvedPhpDoc->getThrowsTag(); if ($throwsTag !== null) { $throwsType = $throwsTag->getType(); - if ($throwsType instanceof VoidType) { + if ($throwsType->isVoid()->yes()) { return []; } @@ -1528,13 +1893,18 @@ private function createAstClassReflection(Node\Stmt\ClassLike $stmt, string $cla $this->stubPhpDocProvider, $this->phpDocInheritanceResolver, $this->phpVersion, + $this->signatureMapProvider, $this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsPropertyClassReflectionExtension(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsMethodsClassReflectionExtension(), $betterReflectionClass->getName(), $betterReflectionClass instanceof ReflectionEnum && PHP_VERSION_ID >= 80000 ? new $enumAdapter($betterReflectionClass) : new ReflectionClass($betterReflectionClass), null, null, null, + $this->universalObjectCratesClasses, sprintf('%s:%d', $scope->getFile(), $stmt->getStartLine()), ); } @@ -1580,20 +1950,33 @@ private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Clo private function ensureShallowNonNullability(MutatingScope $scope, Scope $originalScope, Expr $exprToSpecify): EnsuredNonNullabilityResult { $exprType = $scope->getType($exprToSpecify); + $isNull = $exprType->isNull(); + if ($isNull->yes()) { + return new EnsuredNonNullabilityResult($scope, []); + } + + // keep certainty + $certainty = TrinaryLogic::createYes(); + $hasExpressionType = $originalScope->hasExpressionType($exprToSpecify); + if (!$hasExpressionType->no()) { + $certainty = $hasExpressionType; + } + $exprTypeWithoutNull = TypeCombinator::removeNull($exprType); if ($exprType->equals($exprTypeWithoutNull)) { $originalExprType = $originalScope->getType($exprToSpecify); - $originalNativeType = $originalScope->getNativeType($exprToSpecify); if (!$originalExprType->equals($exprTypeWithoutNull)) { + $originalNativeType = $originalScope->getNativeType($exprToSpecify); + return new EnsuredNonNullabilityResult($scope, [ - new EnsuredNonNullabilityResultExpression($exprToSpecify, $originalExprType, $originalNativeType), + new EnsuredNonNullabilityResultExpression($exprToSpecify, $originalExprType, $originalNativeType, $certainty), ]); } return new EnsuredNonNullabilityResult($scope, []); } $nativeType = $scope->getNativeType($exprToSpecify); - $scope = $scope->assignExpression( + $scope = $scope->specifyExpressionType( $exprToSpecify, $exprTypeWithoutNull, TypeCombinator::removeNull($nativeType), @@ -1602,7 +1985,7 @@ private function ensureShallowNonNullability(MutatingScope $scope, Scope $origin return new EnsuredNonNullabilityResult( $scope, [ - new EnsuredNonNullabilityResultExpression($exprToSpecify, $exprType, $nativeType), + new EnsuredNonNullabilityResultExpression($exprToSpecify, $exprType, $nativeType, $certainty), ], ); } @@ -1628,10 +2011,11 @@ private function ensureNonNullability(MutatingScope $scope, Expr $expr): Ensured private function revertNonNullability(MutatingScope $scope, array $specifiedExpressions): MutatingScope { foreach ($specifiedExpressions as $specifiedExpressionResult) { - $scope = $scope->assignExpression( + $scope = $scope->specifyExpressionType( $specifiedExpressionResult->getExpression(), $specifiedExpressionResult->getOriginalType(), $specifiedExpressionResult->getOriginalNativeType(), + $specifiedExpressionResult->getCertainty(), ); } @@ -1652,8 +2036,7 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr } } - $directClassNames = TypeUtils::getDirectClassNames($methodCalledOnType); - foreach ($directClassNames as $referencedClass) { + foreach ($methodCalledOnType->getObjectClassNames() as $referencedClass) { if (!$this->reflectionProvider->hasClass($referencedClass)) { continue; } @@ -1693,7 +2076,7 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function processExprNode(Expr $expr, MutatingScope $scope, callable $nodeCallback, ExpressionContext $context): ExpressionResult + public function processExprNode(Node\Stmt $stmt, Expr $expr, MutatingScope $scope, callable $nodeCallback, ExpressionContext $context): ExpressionResult { if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) { if ($expr instanceof FuncCall) { @@ -1708,7 +2091,7 @@ private function processExprNode(Expr $expr, MutatingScope $scope, callable $nod throw new ShouldNotHappenException(); } - return $this->processExprNode($newExpr, $scope, $nodeCallback, $context); + return $this->processExprNode($stmt, $newExpr, $scope, $nodeCallback, $context); } $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $context); @@ -1716,17 +2099,21 @@ private function processExprNode(Expr $expr, MutatingScope $scope, callable $nod if ($expr instanceof Variable) { $hasYield = false; $throwPoints = []; + $impurePoints = []; if ($expr->name instanceof Expr) { - return $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); + return $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); + } elseif (in_array($expr->name, Scope::SUPERGLOBAL_VARIABLES, true)) { + $impurePoints[] = new ImpurePoint($scope, $expr, 'superglobal', 'access to superglobal variable', true); } } elseif ($expr instanceof Assign || $expr instanceof AssignRef) { $result = $this->processAssignVar( $scope, + $stmt, $expr->var, $expr->expr, $nodeCallback, $context, - function (MutatingScope $scope) use ($expr, $nodeCallback, $context): ExpressionResult { + function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): ExpressionResult { if ($expr instanceof AssignRef) { $scope = $scope->enterExpressionAssign($expr->expr); } @@ -1735,48 +2122,70 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $context = $context->enterRightSideAssign( $expr->var->name, $scope->getType($expr->expr), + $scope->getNativeType($expr->expr), ); } - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); if ($expr instanceof AssignRef) { $scope = $scope->exitExpressionAssign($expr->expr); } - return new ExpressionResult($scope, $hasYield, $throwPoints); + return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints); }, true, ); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $vars = $this->getAssignedVariables($expr->var); if (count($vars) > 0) { $varChangedScope = false; - $scope = $this->processVarAnnotation($scope, $vars, $expr, $varChangedScope); + $scope = $this->processVarAnnotation($scope, $vars, $stmt, $varChangedScope); if (!$varChangedScope) { - $scope = $this->processStmtVarAnnotation($scope, new Node\Stmt\Expression($expr, [ - 'comments' => $expr->getAttribute('comments'), - ]), null); + $scope = $this->processStmtVarAnnotation($scope, $stmt, null, $nodeCallback); } } } elseif ($expr instanceof Expr\AssignOp) { $result = $this->processAssignVar( $scope, + $stmt, $expr->var, $expr, $nodeCallback, $context, - fn (MutatingScope $scope): ExpressionResult => $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()), + function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): ExpressionResult { + $originalScope = $scope; + if ($expr instanceof Expr\AssignOp\Coalesce) { + $scope = $scope->filterByFalseyValue( + new BinaryOp\NotIdentical($expr->var, new ConstFetch(new Name('null'))), + ); + } + + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + if ($expr instanceof Expr\AssignOp\Coalesce) { + return new ExpressionResult( + $result->getScope()->mergeWith($originalScope), + $result->hasYield(), + $result->getThrowPoints(), + $result->getImpurePoints(), + ); + } + + return $result; + }, $expr instanceof Expr\AssignOp\Coalesce, ); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); if ( ($expr instanceof Expr\AssignOp\Div || $expr instanceof Expr\AssignOp\Mod) && !$scope->getType($expr->expr)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() @@ -1787,35 +2196,79 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $parametersAcceptor = null; $functionReflection = null; $throwPoints = []; + $impurePoints = []; if ($expr->name instanceof Expr) { $nameType = $scope->getType($expr->name); - if ($nameType->isCallable()->yes()) { + if (!$nameType->isCallable()->no()) { $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $expr->getArgs(), $nameType->getCallableParametersAcceptors($scope), + null, ); } - $nameResult = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); - $throwPoints = $nameResult->getThrowPoints(); + + $nameResult = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); $scope = $nameResult->getScope(); + $throwPoints = $nameResult->getThrowPoints(); + $impurePoints = $nameResult->getImpurePoints(); + if ( + $nameType->isObject()->yes() + && $nameType->isCallable()->yes() + && (new ObjectType(Closure::class))->isSuperTypeOf($nameType)->no() + ) { + $invokeResult = $this->processExprNode( + $stmt, + new MethodCall($expr->name, '__invoke', $expr->getArgs(), $expr->getAttributes()), + $scope, + static function (): void { + }, + $context->enterDeep(), + ); + $throwPoints = array_merge($throwPoints, $invokeResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $invokeResult->getImpurePoints()); + } elseif ($parametersAcceptor instanceof CallableParametersAcceptor) { + $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $expr, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $expr), $parametersAcceptor->getThrowPoints()); + if (!$this->implicitThrows) { + $callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit())); + } + $throwPoints = array_merge($throwPoints, $callableThrowPoints); + $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $parametersAcceptor->getImpurePoints())); + + $scope = $this->processImmediatelyCalledCallable($scope, $parametersAcceptor->getInvalidateExpressions(), $parametersAcceptor->getUsedVariables()); + } } elseif ($this->reflectionProvider->hasFunction($expr->name, $scope)) { $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $expr->getArgs(), $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + $impurePoint = SimpleImpurePoint::createFromVariant($functionReflection, $parametersAcceptor); + if ($impurePoint !== null) { + $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()); + } + } else { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'functionCall', + 'call to unknown function', + false, ); } + if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr; } - $result = $this->processArgs($functionReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context); + $result = $this->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); - if (isset($functionReflection)) { + if ($functionReflection !== null) { $functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $expr, $scope); if ($functionThrowPoint !== null) { $throwPoints[] = $functionThrowPoint; @@ -1825,7 +2278,7 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression } if ( - isset($functionReflection) + $functionReflection !== null && in_array($functionReflection->getName(), ['json_encode', 'json_decode'], true) ) { $scope = $scope->invalidateExpression(new FuncCall(new Name('json_last_error'), [])) @@ -1835,154 +2288,65 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression } if ( - isset($functionReflection) + $functionReflection !== null + && $functionReflection->getName() === 'file_put_contents' + && count($expr->getArgs()) > 0 + ) { + $scope = $scope->invalidateExpression(new FuncCall(new Name('file_get_contents'), [$expr->getArgs()[0]])) + ->invalidateExpression(new FuncCall(new Name\FullyQualified('file_get_contents'), [$expr->getArgs()[0]])); + } + + if ( + $functionReflection !== null && in_array($functionReflection->getName(), ['array_pop', 'array_shift'], true) && count($expr->getArgs()) >= 1 ) { $arrayArg = $expr->getArgs()[0]->value; - $arrayArgType = $scope->getType($arrayArg); - $scope = $scope->invalidateExpression($arrayArg); - $functionName = $functionReflection->getName(); - $arrayArgType = TypeTraverser::map($arrayArgType, static function (Type $type, callable $traverse) use ($functionName): Type { - if ($type instanceof UnionType || $type instanceof IntersectionType) { - return $traverse($type); - } - if ($type instanceof ConstantArrayType) { - return $functionName === 'array_pop' ? $type->removeLast() : $type->removeFirst(); - } - if ($type->isIterableAtLeastOnce()->yes()) { - return $type->toArray(); - } - return $type; - }); + $arrayArgType = $scope->getType($arrayArg); + $arrayArgNativeType = $scope->getNativeType($arrayArg); - $scope = $scope->assignExpression( + $isArrayPop = $functionReflection->getName() === 'array_pop'; + $scope = $scope->invalidateExpression($arrayArg)->assignExpression( $arrayArg, - $arrayArgType, - $arrayArgType, + $isArrayPop ? $arrayArgType->popArray() : $arrayArgType->shiftArray(), + $isArrayPop ? $arrayArgNativeType->popArray() : $arrayArgNativeType->shiftArray(), ); } if ( - isset($functionReflection) + $functionReflection !== null && in_array($functionReflection->getName(), ['array_push', 'array_unshift'], true) && count($expr->getArgs()) >= 2 ) { - $arrayArg = $expr->getArgs()[0]->value; - $arrayType = $scope->getType($arrayArg); - $callArgs = array_slice($expr->getArgs(), 1); - - /** - * @param Arg[] $callArgs - * @param callable(?Type, Type, bool): void $setOffsetValueType - */ - $setOffsetValueTypes = static function (Scope $scope, array $callArgs, callable $setOffsetValueType, ?bool &$nonConstantArrayWasUnpacked = null): void { - foreach ($callArgs as $callArg) { - $callArgType = $scope->getType($callArg->value); - if ($callArg->unpack) { - if ($callArgType instanceof ConstantArrayType) { - $iterableValueTypes = $callArgType->getValueTypes(); - } else { - $iterableValueTypes = [$callArgType->getIterableValueType()]; - $nonConstantArrayWasUnpacked = true; - } - - $isOptional = !$callArgType->isIterableAtLeastOnce()->yes(); - foreach ($iterableValueTypes as $iterableValueType) { - if ($iterableValueType instanceof UnionType) { - foreach ($iterableValueType->getTypes() as $innerType) { - $setOffsetValueType(null, $innerType, $isOptional); - } - } else { - $setOffsetValueType(null, $iterableValueType, $isOptional); - } - } - continue; - } - $setOffsetValueType(null, $callArgType, false); - } - }; - - $constantArrays = TypeUtils::getOldConstantArrays($arrayType); - if (count($constantArrays) > 0) { - $newArrayTypes = []; - $prepend = $functionReflection->getName() === 'array_unshift'; - foreach ($constantArrays as $constantArray) { - $arrayTypeBuilder = $prepend ? ConstantArrayTypeBuilder::createEmpty() : ConstantArrayTypeBuilder::createFromConstantArray($constantArray); - - $setOffsetValueTypes( - $scope, - $callArgs, - static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayTypeBuilder): void { - $arrayTypeBuilder->setOffsetValueType($offsetType, $valueType, $optional); - }, - $nonConstantArrayWasUnpacked, - ); - - if ($prepend) { - $keyTypes = $constantArray->getKeyTypes(); - $valueTypes = $constantArray->getValueTypes(); - foreach ($keyTypes as $k => $keyType) { - $arrayTypeBuilder->setOffsetValueType( - $keyType instanceof ConstantStringType ? $keyType : null, - $valueTypes[$k], - $constantArray->isOptionalKey($k), - ); - } - } - - $constantArray = $arrayTypeBuilder->getArray(); - - if ($constantArray instanceof ConstantArrayType && $nonConstantArrayWasUnpacked) { - $constantArray = $constantArray->isIterableAtLeastOnce()->yes() - ? TypeCombinator::intersect($constantArray->generalizeKeys(), new NonEmptyArrayType()) - : $constantArray->generalizeKeys(); - } - - $newArrayTypes[] = $constantArray; - } - - $arrayType = TypeCombinator::union(...$newArrayTypes); - } else { - $setOffsetValueTypes( - $scope, - $callArgs, - static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayType): void { - $isIterableAtLeastOnce = $arrayType->isIterableAtLeastOnce()->yes() || !$optional; - $arrayType = $arrayType->setOffsetValueType($offsetType, $valueType); - if ($isIterableAtLeastOnce) { - return; - } - - $arrayType = new ArrayType($arrayType->getIterableKeyType(), $arrayType->getIterableValueType()); - }, - ); - } + $arrayType = $this->getArrayFunctionAppendingType($functionReflection, $scope, $expr); + $arrayNativeType = $this->getArrayFunctionAppendingType($functionReflection, $scope->doNotTreatPhpDocTypesAsCertain(), $expr); - $scope = $scope->invalidateExpression($arrayArg)->assignExpression($arrayArg, $arrayType, $arrayType); + $arrayArg = $expr->getArgs()[0]->value; + $scope = $scope->invalidateExpression($arrayArg)->assignExpression($arrayArg, $arrayType, $arrayNativeType); } if ( - isset($functionReflection) + $functionReflection !== null && in_array($functionReflection->getName(), ['fopen', 'file_get_contents'], true) ) { - $scope = $scope->assignVariable('http_response_header', new ArrayType(new IntegerType(), new StringType())); + $scope = $scope->assignVariable('http_response_header', new ArrayType(new IntegerType(), new StringType()), new ArrayType(new IntegerType(), new StringType())); } - if (isset($functionReflection) && $functionReflection->getName() === 'shuffle') { + if ( + $functionReflection !== null + && $functionReflection->getName() === 'shuffle' + ) { $arrayArg = $expr->getArgs()[0]->value; - $arrayArgType = $scope->getType($arrayArg); - - if ($arrayArgType instanceof ConstantArrayType) { - $arrayArgType = $arrayArgType->getValuesArray()->generalizeToArray(); - } - - $scope = $scope->assignExpression($arrayArg, $arrayArgType, $arrayArgType); + $scope = $scope->assignExpression( + $arrayArg, + $scope->getType($arrayArg)->shuffleArray(), + $scope->getNativeType($arrayArg)->shuffleArray(), + ); } if ( - isset($functionReflection) + $functionReflection !== null && $functionReflection->getName() === 'array_splice' && count($expr->getArgs()) >= 1 ) { @@ -1990,7 +2354,8 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $arrayArgType = $scope->getType($arrayArg); $valueType = $arrayArgType->getIterableValueType(); if (count($expr->getArgs()) >= 4) { - $valueType = TypeCombinator::union($valueType, $scope->getType($expr->getArgs()[3]->value)->getIterableValueType()); + $replacementType = $scope->getType($expr->getArgs()[3]->value)->toArray(); + $valueType = TypeCombinator::union($valueType, $replacementType->getIterableValueType()); } $scope = $scope->invalidateExpression($arrayArg)->assignExpression( $arrayArg, @@ -1999,22 +2364,85 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra ); } - if (isset($functionReflection) && $functionReflection->getName() === 'extract') { - $scope = $scope->afterExtractCall(); + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['sort', 'rsort', 'usort'], true) + && count($expr->getArgs()) >= 1 + ) { + $arrayArg = $expr->getArgs()[0]->value; + $scope = $scope->assignExpression( + $arrayArg, + $this->getArraySortPreserveListFunctionType($scope->getType($arrayArg)), + $this->getArraySortPreserveListFunctionType($scope->getNativeType($arrayArg)), + ); } - if (isset($functionReflection) && ($functionReflection->getName() === 'clearstatcache' || $functionReflection->getName() === 'unlink')) { - $scope = $scope->afterClearstatcacheCall(); + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['natcasesort', 'natsort', 'arsort', 'asort', 'ksort', 'krsort', 'uasort', 'uksort'], true) + && count($expr->getArgs()) >= 1 + ) { + $arrayArg = $expr->getArgs()[0]->value; + $scope = $scope->assignExpression( + $arrayArg, + $this->getArraySortDoNotPreserveListFunctionType($scope->getType($arrayArg)), + $this->getArraySortDoNotPreserveListFunctionType($scope->getNativeType($arrayArg)), + ); } - if (isset($functionReflection) && str_starts_with($functionReflection->getName(), 'openssl')) { - $scope = $scope->afterOpenSslCall($functionReflection->getName()); + if ( + $functionReflection !== null + && $functionReflection->getName() === 'extract' + ) { + $extractedArg = $expr->getArgs()[0]->value; + $extractedType = $scope->getType($extractedArg); + $constantArrays = $extractedType->getConstantArrays(); + if (count($constantArrays) > 0) { + $properties = []; + $optionalProperties = []; + $refCount = []; + foreach ($constantArrays as $constantArray) { + foreach ($constantArray->getKeyTypes() as $i => $keyType) { + if ($keyType->isString()->no()) { + // integers as variable names not allowed + continue; + } + $key = (string) $keyType->getValue(); + $valueType = $constantArray->getValueTypes()[$i]; + $optional = $constantArray->isOptionalKey($i); + if ($optional) { + $optionalProperties[] = $key; + } + if (isset($properties[$key])) { + $properties[$key] = TypeCombinator::union($properties[$key], $valueType); + $refCount[$key]++; + } else { + $properties[$key] = $valueType; + $refCount[$key] = 1; + } + } + } + foreach ($properties as $name => $type) { + $optional = in_array($name, $optionalProperties, true) || $refCount[$name] < count($constantArrays); + $scope = $scope->assignVariable($name, $type, $type, $optional ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes()); + } + } else { + $scope = $scope->afterExtractCall(); + } } - if (isset($functionReflection) && $functionReflection->hasSideEffects()->yes()) { - foreach ($expr->getArgs() as $arg) { - $scope = $scope->invalidateExpression($arg->value, true); - } + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['clearstatcache', 'unlink'], true) + ) { + $scope = $scope->afterClearstatcacheCall(); + } + + if ( + $functionReflection !== null + && str_starts_with($functionReflection->getName(), 'openssl') + ) { + $scope = $scope->afterOpenSslCall($functionReflection->getName()); } } elseif ($expr instanceof MethodCall) { @@ -2025,24 +2453,28 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra && strtolower($expr->name->name) === 'call' && isset($expr->getArgs()[0]) ) { - $closureCallScope = $scope->enterClosureCall($scope->getType($expr->getArgs()[0]->value)); + $closureCallScope = $scope->enterClosureCall( + $scope->getType($expr->getArgs()[0]->value), + $scope->getNativeType($expr->getArgs()[0]->value), + ); } - $result = $this->processExprNode($expr->var, $closureCallScope ?? $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->var, $closureCallScope ?? $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); if (isset($closureCallScope)) { $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope); } $parametersAcceptor = null; $methodReflection = null; + $calledOnType = $scope->getType($expr->var); if ($expr->name instanceof Expr) { - $methodNameResult = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); + $methodNameResult = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = array_merge($throwPoints, $methodNameResult->getThrowPoints()); $scope = $methodNameResult->getScope(); } else { - $calledOnType = $scope->getType($expr->var); $methodName = $expr->name->name; $methodReflection = $scope->getMethodReflection($calledOnType, $methodName); if ($methodReflection !== null) { @@ -2050,6 +2482,7 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $scope, $expr->getArgs(), $methodReflection->getVariants(), + $methodReflection->getNamedArgumentsVariants(), ); $methodThrowPoint = $this->getMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope); @@ -2058,17 +2491,74 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra } } } + + if ($methodReflection !== null) { + $impurePoint = SimpleImpurePoint::createFromVariant($methodReflection, $parametersAcceptor); + if ($impurePoint !== null) { + $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()); + } + } else { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'methodCall', + 'call to unknown method', + false, + ); + } + if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr; } - $result = $this->processArgs($methodReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context); + + $result = $this->processArgs( + $stmt, + $methodReflection, + $methodReflection !== null ? $scope->getNakedMethod($calledOnType, $methodReflection->getName()) : null, + $parametersAcceptor, + $expr->getArgs(), + $scope, + $nodeCallback, + $context, + ); $scope = $result->getScope(); + if ($methodReflection !== null) { $hasSideEffects = $methodReflection->hasSideEffects(); if ($hasSideEffects->yes() || $methodReflection->getName() === '__construct') { + $nodeCallback(new InvalidateExprNode($expr->var), $scope); $scope = $scope->invalidateExpression($expr->var, true); - foreach ($expr->getArgs() as $arg) { - $scope = $scope->invalidateExpression($arg->value, true); + } + if ($parametersAcceptor !== null) { + $selfOutType = $methodReflection->getSelfOutType(); + if ($selfOutType !== null) { + $scope = $scope->assignExpression( + $expr->var, + TemplateTypeHelper::resolveTemplateTypes( + $selfOutType, + $parametersAcceptor->getResolvedTemplateTypeMap(), + $parametersAcceptor instanceof ParametersAcceptorWithPhpDocs ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createCovariant(), + ), + $scope->getNativeType($expr->var), + ); + } + } + + if ( + $scope->isInClass() + && $scope->getClassReflection()->getName() === $methodReflection->getDeclaringClass()->getName() + /*&& ( + // should not be allowed but in practice has to be + $scope->getClassReflection()->isFinal() + || $methodReflection->isFinal()->yes() + || $methodReflection->isPrivate() + )*/ + && TypeUtils::findThisType($calledOnType) !== null + ) { + $calledMethodScope = $this->processCalledMethod($methodReflection); + if ($calledMethodScope !== null) { + $scope = $scope->mergeInitializedProperties($calledMethodScope); } } } else { @@ -2076,36 +2566,40 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra } $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); } elseif ($expr instanceof Expr\NullsafeMethodCall) { $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $scope, $expr->var); - $exprResult = $this->processExprNode(new MethodCall($expr->var, $expr->name, $expr->args, $expr->getAttributes()), $nonNullabilityResult->getScope(), $nodeCallback, $context); + $exprResult = $this->processExprNode($stmt, new MethodCall($expr->var, $expr->name, $expr->args, array_merge($expr->getAttributes(), ['virtualNullsafeMethodCall' => true])), $nonNullabilityResult->getScope(), $nodeCallback, $context); $scope = $this->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); return new ExpressionResult( $scope, $exprResult->hasYield(), $exprResult->getThrowPoints(), + $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } elseif ($expr instanceof StaticCall) { $hasYield = false; $throwPoints = []; + $impurePoints = []; if ($expr->class instanceof Expr) { - $objectClasses = TypeUtils::getDirectClassNames($scope->getType($expr->class)); + $objectClasses = $scope->getType($expr->class)->getObjectClassNames(); if (count($objectClasses) !== 1) { - $objectClasses = TypeUtils::getDirectClassNames($scope->getType(new New_($expr->class))); + $objectClasses = $scope->getType(new New_($expr->class))->getObjectClassNames(); } if (count($objectClasses) === 1) { - $objectExprResult = $this->processExprNode(new StaticCall(new Name($objectClasses[0]), $expr->name, []), $scope, static function (): void { + $objectExprResult = $this->processExprNode($stmt, new StaticCall(new Name($objectClasses[0]), $expr->name, []), $scope, static function (): void { }, $context->enterDeep()); $additionalThrowPoints = $objectExprResult->getThrowPoints(); } else { $additionalThrowPoints = [ThrowPoint::createImplicit($scope, $expr)]; } - $classResult = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep()); + $classResult = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $classResult->hasYield(); $throwPoints = array_merge($throwPoints, $classResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $classResult->getImpurePoints()); foreach ($additionalThrowPoints as $throwPoint) { $throwPoints[] = $throwPoint; } @@ -2115,25 +2609,23 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $parametersAcceptor = null; $methodReflection = null; if ($expr->name instanceof Expr) { - $result = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); } elseif ($expr->class instanceof Name) { $className = $scope->resolveName($expr->class); if ($this->reflectionProvider->hasClass($className)) { $classReflection = $this->reflectionProvider->getClass($className); - if (is_string($expr->name)) { - $methodName = $expr->name; - } else { - $methodName = $expr->name->name; - } + $methodName = $expr->name->name; if ($classReflection->hasMethod($methodName)) { $methodReflection = $classReflection->getMethod($methodName, $scope); $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $expr->getArgs(), $methodReflection->getVariants(), + $methodReflection->getNamedArgumentsVariants(), ); $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope); @@ -2145,35 +2637,42 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra && strtolower($methodName) === 'bind' ) { $thisType = null; + $nativeThisType = null; if (isset($expr->getArgs()[1])) { $argType = $scope->getType($expr->getArgs()[1]->value); - if ($argType instanceof NullType) { + if ($argType->isNull()->yes()) { $thisType = null; } else { $thisType = $argType; } + + $nativeArgType = $scope->getNativeType($expr->getArgs()[1]->value); + if ($nativeArgType->isNull()->yes()) { + $nativeThisType = null; + } else { + $nativeThisType = $nativeArgType; + } } - $scopeClass = 'static'; + $scopeClasses = ['static']; if (isset($expr->getArgs()[2])) { $argValue = $expr->getArgs()[2]->value; $argValueType = $scope->getType($argValue); - $directClassNames = TypeUtils::getDirectClassNames($argValueType); - if (count($directClassNames) === 1) { - $scopeClass = $directClassNames[0]; - $thisType = new ObjectType($scopeClass); - } elseif ($argValueType instanceof ConstantStringType) { - $scopeClass = $argValueType->getValue(); - $thisType = new ObjectType($scopeClass); - } elseif ( - $argValueType instanceof GenericClassStringType - && $argValueType->getGenericType() instanceof TypeWithClassName - ) { - $scopeClass = $argValueType->getGenericType()->getClassName(); - $thisType = $argValueType->getGenericType(); + $scopeClasses = []; + $directClassNames = $argValueType->getObjectClassNames(); + if (count($directClassNames) > 0) { + $scopeClasses = $directClassNames; + $thisTypes = []; + foreach ($directClassNames as $directClassName) { + $thisTypes[] = new ObjectType($directClassName); + } + $thisType = TypeCombinator::union(...$thisTypes); + } else { + $thisType = $argValueType->getClassStringObjectType(); + $scopeClasses = $thisType->getObjectClassNames(); } } - $closureBindScope = $scope->enterClosureBind($thisType, $scopeClass); + $closureBindScope = $scope->enterClosureBind($thisType, $nativeThisType, $scopeClasses); } } else { $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); @@ -2182,12 +2681,29 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); } } + + if ($methodReflection !== null) { + $impurePoint = SimpleImpurePoint::createFromVariant($methodReflection, $parametersAcceptor); + if ($impurePoint !== null) { + $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()); + } + } else { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'methodCall', + 'call to unknown method', + false, + ); + } + if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr; } - $result = $this->processArgs($methodReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context, $closureBindScope ?? null); + $result = $this->processArgs($stmt, $methodReflection, null, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context, $closureBindScope ?? null); $scope = $result->getScope(); $scopeFunction = $scope->getFunction(); + if ( $methodReflection !== null && !$methodReflection->isStatic() @@ -2206,129 +2722,148 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $scope = $scope->invalidateExpression(new Variable('this'), true); } - if ($methodReflection !== null) { - if ($methodReflection->hasSideEffects()->yes() || $methodReflection->getName() === '__construct') { - foreach ($expr->getArgs() as $arg) { - $scope = $scope->invalidateExpression($arg->value, true); - } - } - } - $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); } elseif ($expr instanceof PropertyFetch) { - $result = $this->processExprNode($expr->var, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->var, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); if ($expr->name instanceof Expr) { - $result = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); + $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); } } elseif ($expr instanceof Expr\NullsafePropertyFetch) { $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $scope, $expr->var); - $exprResult = $this->processExprNode(new PropertyFetch($expr->var, $expr->name, $expr->getAttributes()), $nonNullabilityResult->getScope(), $nodeCallback, $context); + $exprResult = $this->processExprNode($stmt, new PropertyFetch($expr->var, $expr->name, array_merge($expr->getAttributes(), ['virtualNullsafePropertyFetch' => true])), $nonNullabilityResult->getScope(), $nodeCallback, $context); $scope = $this->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); return new ExpressionResult( $scope, $exprResult->hasYield(), $exprResult->getThrowPoints(), + $exprResult->getImpurePoints(), static fn (): MutatingScope => $scope->filterByTruthyValue($expr), static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } elseif ($expr instanceof StaticPropertyFetch) { $hasYield = false; $throwPoints = []; + $impurePoints = []; if ($expr->class instanceof Expr) { - $result = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); } if ($expr->name instanceof Expr) { - $result = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); } } elseif ($expr instanceof Expr\Closure) { - return $this->processClosureNode($expr, $scope, $nodeCallback, $context, null); - } elseif ($expr instanceof Expr\ClosureUse) { - $this->processExprNode($expr->var, $scope, $nodeCallback, $context); - $hasYield = false; - $throwPoints = []; + $processClosureResult = $this->processClosureNode($stmt, $expr, $scope, $nodeCallback, $context, null); + + return new ExpressionResult( + $processClosureResult->getScope(), + false, + [], + [], + ); } elseif ($expr instanceof Expr\ArrowFunction) { - return $this->processArrowFunctionNode($expr, $scope, $nodeCallback, $context, null); + $result = $this->processArrowFunctionNode($stmt, $expr, $scope, $nodeCallback, null); + return new ExpressionResult( + $result->getScope(), + $result->hasYield(), + [], + [], + ); } elseif ($expr instanceof ErrorSuppress) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); } elseif ($expr instanceof Exit_) { $hasYield = false; $throwPoints = []; + $kind = $expr->getAttribute('kind', Exit_::KIND_EXIT); + $identifier = $kind === Exit_::KIND_DIE ? 'die' : 'exit'; + $impurePoints = [ + new ImpurePoint($scope, $expr, $identifier, $identifier, true), + ]; if ($expr->expr !== null) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); } } elseif ($expr instanceof Node\Scalar\Encapsed) { $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($expr->parts as $part) { - $result = $this->processExprNode($part, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $part, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); } } elseif ($expr instanceof ArrayDimFetch) { $hasYield = false; $throwPoints = []; + $impurePoints = []; if ($expr->dim !== null) { - $result = $this->processExprNode($expr->dim, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->dim, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); } - $result = $this->processExprNode($expr->var, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->var, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); } elseif ($expr instanceof Array_) { $itemNodes = []; $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($expr->items as $arrayItem) { $itemNodes[] = new LiteralArrayItem($scope, $arrayItem); if ($arrayItem === null) { continue; } - $result = $this->processExprNode($arrayItem, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $hasYield || $result->hasYield(); - $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); - $scope = $result->getScope(); + $nodeCallback($arrayItem, $scope); + if ($arrayItem->key !== null) { + $keyResult = $this->processExprNode($stmt, $arrayItem->key, $scope, $nodeCallback, $context->enterDeep()); + $hasYield = $hasYield || $keyResult->hasYield(); + $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); + $scope = $keyResult->getScope(); + } + + $valueResult = $this->processExprNode($stmt, $arrayItem->value, $scope, $nodeCallback, $context->enterDeep()); + $hasYield = $hasYield || $valueResult->hasYield(); + $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); + $scope = $valueResult->getScope(); } $nodeCallback(new LiteralArrayNode($expr, $itemNodes), $scope); - } elseif ($expr instanceof ArrayItem) { - $hasYield = false; - $throwPoints = []; - if ($expr->key !== null) { - $result = $this->processExprNode($expr->key, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $result->hasYield(); - $throwPoints = $result->getThrowPoints(); - $scope = $result->getScope(); - } - $result = $this->processExprNode($expr->value, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $hasYield || $result->hasYield(); - $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); - $scope = $result->getScope(); } elseif ($expr instanceof BooleanAnd || $expr instanceof BinaryOp\LogicalAnd) { - $leftResult = $this->processExprNode($expr->left, $scope, $nodeCallback, $context->enterDeep()); - $rightResult = $this->processExprNode($expr->right, $leftResult->getTruthyScope(), $nodeCallback, $context); + $leftResult = $this->processExprNode($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep()); + $rightResult = $this->processExprNode($stmt, $expr->right, $leftResult->getTruthyScope(), $nodeCallback, $context); $rightExprType = $rightResult->getScope()->getType($expr->right); if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { $leftMergedWithRightScope = $leftResult->getFalseyScope(); @@ -2342,12 +2877,13 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), + array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), static fn (): MutatingScope => $rightResult->getScope()->filterByTruthyValue($expr), static fn (): MutatingScope => $leftMergedWithRightScope->filterByFalseyValue($expr), ); } elseif ($expr instanceof BooleanOr || $expr instanceof BinaryOp\LogicalOr) { - $leftResult = $this->processExprNode($expr->left, $scope, $nodeCallback, $context->enterDeep()); - $rightResult = $this->processExprNode($expr->right, $leftResult->getFalseyScope(), $nodeCallback, $context); + $leftResult = $this->processExprNode($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep()); + $rightResult = $this->processExprNode($stmt, $expr->right, $leftResult->getFalseyScope(), $nodeCallback, $context); $rightExprType = $rightResult->getScope()->getType($expr->right); if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { $leftMergedWithRightScope = $leftResult->getTruthyScope(); @@ -2361,18 +2897,19 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), + array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), static fn (): MutatingScope => $leftMergedWithRightScope->filterByTruthyValue($expr), static fn (): MutatingScope => $rightResult->getScope()->filterByFalseyValue($expr), ); } elseif ($expr instanceof Coalesce) { $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->left); $condScope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->left); - $condResult = $this->processExprNode($expr->left, $condScope, $nodeCallback, $context->enterDeep()); + $condResult = $this->processExprNode($stmt, $expr->left, $condScope, $nodeCallback, $context->enterDeep()); $scope = $this->revertNonNullability($condResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->left); - $rightScope = $scope->filterByFalseyValue(new Expr\Isset_([$expr->left])); - $rightResult = $this->processExprNode($expr->right, $rightScope, $nodeCallback, $context->enterDeep()); + $rightScope = $scope->filterByFalseyValue($expr); + $rightResult = $this->processExprNode($stmt, $expr->right, $rightScope, $nodeCallback, $context->enterDeep()); $rightExprType = $scope->getType($expr->right); if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { $scope = $scope->filterByTruthyValue(new Expr\Isset_([$expr->left])); @@ -2382,12 +2919,14 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $hasYield = $condResult->hasYield() || $rightResult->hasYield(); $throwPoints = array_merge($condResult->getThrowPoints(), $rightResult->getThrowPoints()); + $impurePoints = array_merge($condResult->getImpurePoints(), $rightResult->getImpurePoints()); } elseif ($expr instanceof BinaryOp) { - $result = $this->processExprNode($expr->left, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); - $result = $this->processExprNode($expr->right, $scope, $nodeCallback, $context->enterDeep()); + $impurePoints = $result->getImpurePoints(); + $result = $this->processExprNode($stmt, $expr->right, $scope, $nodeCallback, $context->enterDeep()); if ( ($expr instanceof BinaryOp\Div || $expr instanceof BinaryOp\Mod) && !$scope->getType($expr->right)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() @@ -2397,73 +2936,127 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); } elseif ($expr instanceof Expr\Include_) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + $impurePoints = $result->getImpurePoints(); + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + in_array($expr->type, [Expr\Include_::TYPE_INCLUDE, Expr\Include_::TYPE_INCLUDE_ONCE], true) ? 'include' : 'require', + in_array($expr->type, [Expr\Include_::TYPE_INCLUDE, Expr\Include_::TYPE_INCLUDE_ONCE], true) ? 'include' : 'require', + true, + ); $hasYield = $result->hasYield(); + $scope = $result->getScope()->afterExtractCall(); + } elseif ($expr instanceof Expr\Print_) { + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + $impurePoints[] = new ImpurePoint($scope, $expr, 'print', 'print', true); + $hasYield = $result->hasYield(); + + $scope = $result->getScope(); + } elseif ($expr instanceof Cast\String_) { + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + $hasYield = $result->hasYield(); + + $exprType = $scope->getType($expr->expr); + $toStringMethod = $scope->getMethodReflection($exprType, '__toString'); + if ($toStringMethod !== null) { + if (!$toStringMethod->hasSideEffects()->no()) { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'methodCall', + sprintf('call to method %s::%s()', $toStringMethod->getDeclaringClass()->getDisplayName(), $toStringMethod->getName()), + $toStringMethod->isPure()->no(), + ); + } + } + $scope = $result->getScope(); } elseif ( $expr instanceof Expr\BitwiseNot || $expr instanceof Cast || $expr instanceof Expr\Clone_ - || $expr instanceof Expr\Print_ || $expr instanceof Expr\UnaryMinus || $expr instanceof Expr\UnaryPlus ) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $hasYield = $result->hasYield(); $scope = $result->getScope(); } elseif ($expr instanceof Expr\Eval_) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + $impurePoints = $result->getImpurePoints(); + $impurePoints[] = new ImpurePoint($scope, $expr, 'eval', 'eval', true); $hasYield = $result->hasYield(); $scope = $result->getScope(); } elseif ($expr instanceof Expr\YieldFrom) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); + $impurePoints = $result->getImpurePoints(); + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'yieldFrom', + 'yield from', + true, + ); $hasYield = true; $scope = $result->getScope(); } elseif ($expr instanceof BooleanNot) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); } elseif ($expr instanceof Expr\ClassConstFetch) { $hasYield = false; $throwPoints = []; + $impurePoints = []; if ($expr->class instanceof Expr) { - $result = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); } } elseif ($expr instanceof Expr\Empty_) { $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->expr); $scope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->expr); - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->expr); } elseif ($expr instanceof Expr\Isset_) { $hasYield = false; $throwPoints = []; + $impurePoints = []; $nonNullabilityResults = []; foreach ($expr->vars as $var) { $nonNullabilityResult = $this->ensureNonNullability($scope, $var); $scope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $var); - $result = $this->processExprNode($var, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $var, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $nonNullabilityResults[] = $nonNullabilityResult; } foreach (array_reverse($expr->vars) as $var) { @@ -2473,47 +3066,54 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); } } elseif ($expr instanceof Instanceof_) { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); if ($expr->class instanceof Expr) { - $result = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); } } elseif ($expr instanceof List_) { // only in assign and foreach, processed elsewhere - return new ExpressionResult($scope, false, []); + return new ExpressionResult($scope, false, [], []); } elseif ($expr instanceof New_) { $parametersAcceptor = null; $constructorReflection = null; $hasYield = false; $throwPoints = []; - if ($expr->class instanceof Expr) { - $objectClasses = TypeUtils::getDirectClassNames($scope->getType($expr)); - if (count($objectClasses) === 1) { - $objectExprResult = $this->processExprNode(new New_(new Name($objectClasses[0])), $scope, static function (): void { - }, $context->enterDeep()); - $additionalThrowPoints = $objectExprResult->getThrowPoints(); + $impurePoints = []; + $className = null; + if ($expr->class instanceof Expr || $expr->class instanceof Name) { + if ($expr->class instanceof Expr) { + $objectClasses = $scope->getType($expr)->getObjectClassNames(); + if (count($objectClasses) === 1) { + $objectExprResult = $this->processExprNode($stmt, new New_(new Name($objectClasses[0])), $scope, static function (): void { + }, $context->enterDeep()); + $className = $objectClasses[0]; + $additionalThrowPoints = $objectExprResult->getThrowPoints(); + } else { + $additionalThrowPoints = [ThrowPoint::createImplicit($scope, $expr)]; + } + + $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); + $scope = $result->getScope(); + $hasYield = $result->hasYield(); + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + foreach ($additionalThrowPoints as $throwPoint) { + $throwPoints[] = $throwPoint; + } } else { - $additionalThrowPoints = [ThrowPoint::createImplicit($scope, $expr)]; + $className = $scope->resolveName($expr->class); } - $result = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep()); - $scope = $result->getScope(); - $hasYield = $result->hasYield(); - $throwPoints = $result->getThrowPoints(); - foreach ($additionalThrowPoints as $throwPoint) { - $throwPoints[] = $throwPoint; - } - } elseif ($expr->class instanceof Class_) { - $this->reflectionProvider->getAnonymousClassReflection($expr->class, $scope); // populates $expr->class->name - $this->processStmtNode($expr->class, $scope, $nodeCallback); - } else { - $className = $scope->resolveName($expr->class); - if ($this->reflectionProvider->hasClass($className)) { + $classReflection = null; + if ($className !== null && $this->reflectionProvider->hasClass($className)) { $classReflection = $this->reflectionProvider->getClass($className); if ($classReflection->hasConstructor()) { $constructorReflection = $classReflection->getConstructor(); @@ -2521,14 +3121,9 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $scope, $expr->getArgs(), $constructorReflection->getVariants(), + $constructorReflection->getNamedArgumentsVariants(), ); - $hasSideEffects = $constructorReflection->hasSideEffects(); - if ($hasSideEffects->yes()) { - foreach ($expr->getArgs() as $arg) { - $scope = $scope->invalidateExpression($arg->value, true); - } - } - $constructorThrowPoint = $this->getConstructorThrowPoint($constructorReflection, $parametersAcceptor, $classReflection, $expr, $expr->class, $expr->getArgs(), $scope); + $constructorThrowPoint = $this->getConstructorThrowPoint($constructorReflection, $parametersAcceptor, $classReflection, $expr, new Name\FullyQualified($className), $expr->getArgs(), $scope); if ($constructorThrowPoint !== null) { $throwPoints[] = $constructorThrowPoint; } @@ -2536,24 +3131,86 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra } else { $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); } + + if ($constructorReflection !== null) { + if (!$constructorReflection->hasSideEffects()->no()) { + $certain = $constructorReflection->isPure()->no(); + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'new', + sprintf('instantiation of class %s', $constructorReflection->getDeclaringClass()->getDisplayName()), + $certain, + ); + } + } elseif ($classReflection === null) { + $impurePoints[] = new ImpurePoint( + $scope, + $expr, + 'new', + 'instantiation of unknown class', + false, + ); + } + + if ($parametersAcceptor !== null) { + $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr; + } + + } else { + $classReflection = $this->reflectionProvider->getAnonymousClassReflection($expr->class, $scope); // populates $expr->class->name + $constructorResult = null; + $this->processStmtNode($expr->class, $scope, static function (Node $node, Scope $scope) use ($nodeCallback, $classReflection, &$constructorResult): void { + $nodeCallback($node, $scope); + if (!$node instanceof MethodReturnStatementsNode) { + return; + } + if ($constructorResult !== null) { + return; + } + $currentClassReflection = $node->getClassReflection(); + if ($currentClassReflection->getName() !== $classReflection->getName()) { + return; + } + if (!$currentClassReflection->hasConstructor()) { + return; + } + if ($currentClassReflection->getConstructor()->getName() !== $node->getMethodReflection()->getName()) { + return; + } + $constructorResult = $node; + }, StatementContext::createTopLevel()); + if ($constructorResult !== null) { + $throwPoints = array_merge($throwPoints, $constructorResult->getStatementResult()->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $constructorResult->getImpurePoints()); + } + if ($classReflection->hasConstructor()) { + $constructorReflection = $classReflection->getConstructor(); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $expr->getArgs(), + $constructorReflection->getVariants(), + $constructorReflection->getNamedArgumentsVariants(), + ); + } } - if ($parametersAcceptor !== null) { - $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr; - } - $result = $this->processArgs($constructorReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context); + + $result = $this->processArgs($stmt, $constructorReflection, null, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); } elseif ( $expr instanceof Expr\PreInc || $expr instanceof Expr\PostInc || $expr instanceof Expr\PreDec || $expr instanceof Expr\PostDec ) { - $result = $this->processExprNode($expr->var, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->var, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); - $throwPoints = []; + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $newExpr = $expr; if ($expr instanceof Expr\PostInc) { @@ -2564,49 +3221,64 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $scope = $this->processAssignVar( $scope, + $stmt, $expr->var, $newExpr, static function (Node $node, Scope $scope) use ($nodeCallback): void { - if (!$node instanceof PropertyAssignNode) { + if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { return; } $nodeCallback($node, $scope); }, $context, - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), false, )->getScope(); } elseif ($expr instanceof Ternary) { - $ternaryCondResult = $this->processExprNode($expr->cond, $scope, $nodeCallback, $context->enterDeep()); + $ternaryCondResult = $this->processExprNode($stmt, $expr->cond, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $ternaryCondResult->getThrowPoints(); + $impurePoints = $ternaryCondResult->getImpurePoints(); $ifTrueScope = $ternaryCondResult->getTruthyScope(); $ifFalseScope = $ternaryCondResult->getFalseyScope(); $ifTrueType = null; if ($expr->if !== null) { - $ifResult = $this->processExprNode($expr->if, $ifTrueScope, $nodeCallback, $context); + $ifResult = $this->processExprNode($stmt, $expr->if, $ifTrueScope, $nodeCallback, $context); $throwPoints = array_merge($throwPoints, $ifResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $ifResult->getImpurePoints()); $ifTrueScope = $ifResult->getScope(); $ifTrueType = $ifTrueScope->getType($expr->if); } - $elseResult = $this->processExprNode($expr->else, $ifFalseScope, $nodeCallback, $context); + $elseResult = $this->processExprNode($stmt, $expr->else, $ifFalseScope, $nodeCallback, $context); $throwPoints = array_merge($throwPoints, $elseResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $elseResult->getImpurePoints()); $ifFalseScope = $elseResult->getScope(); - $ifFalseType = $ifFalseScope->getType($expr->else); - if ($ifTrueType instanceof NeverType && $ifTrueType->isExplicit()) { - $finalScope = $ifFalseScope; - } elseif ($ifFalseType instanceof NeverType && $ifFalseType->isExplicit()) { + $condType = $scope->getType($expr->cond); + if ($condType->isTrue()->yes()) { $finalScope = $ifTrueScope; + } elseif ($condType->isFalse()->yes()) { + $finalScope = $ifFalseScope; } else { - $finalScope = $ifTrueScope->mergeWith($ifFalseScope); + if ($ifTrueType instanceof NeverType && $ifTrueType->isExplicit()) { + $finalScope = $ifFalseScope; + } else { + $ifFalseType = $ifFalseScope->getType($expr->else); + + if ($ifFalseType instanceof NeverType && $ifFalseType->isExplicit()) { + $finalScope = $ifTrueScope; + } else { + $finalScope = $ifTrueScope->mergeWith($ifFalseScope); + } + } } return new ExpressionResult( $finalScope, $ternaryCondResult->hasYield(), $throwPoints, + $impurePoints, static fn (): MutatingScope => $finalScope->filterByTruthyValue($expr), static fn (): MutatingScope => $finalScope->filterByFalseyValue($expr), ); @@ -2615,24 +3287,36 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $throwPoints = [ ThrowPoint::createImplicit($scope, $expr), ]; + $impurePoints = [ + new ImpurePoint( + $scope, + $expr, + 'yield', + 'yield', + true, + ), + ]; if ($expr->key !== null) { - $keyResult = $this->processExprNode($expr->key, $scope, $nodeCallback, $context->enterDeep()); + $keyResult = $this->processExprNode($stmt, $expr->key, $scope, $nodeCallback, $context->enterDeep()); $scope = $keyResult->getScope(); $throwPoints = $keyResult->getThrowPoints(); + $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); } if ($expr->value !== null) { - $valueResult = $this->processExprNode($expr->value, $scope, $nodeCallback, $context->enterDeep()); + $valueResult = $this->processExprNode($stmt, $expr->value, $scope, $nodeCallback, $context->enterDeep()); $scope = $valueResult->getScope(); $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); } $hasYield = true; } elseif ($expr instanceof Expr\Match_) { $deepContext = $context->enterDeep(); - $condResult = $this->processExprNode($expr->cond, $scope, $nodeCallback, $deepContext); + $condResult = $this->processExprNode($stmt, $expr->cond, $scope, $nodeCallback, $deepContext); $scope = $condResult->getScope(); $hasYield = $condResult->hasYield(); $throwPoints = $condResult->getThrowPoints(); - $matchScope = $scope; + $impurePoints = $condResult->getImpurePoints(); + $matchScope = $scope->enterMatch($expr); $armNodes = []; $hasDefaultCond = false; $hasAlwaysTrueCond = false; @@ -2640,11 +3324,12 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { if ($arm->conds === null) { $hasDefaultCond = true; $matchArmBody = new MatchExpressionArmBody($matchScope, $arm->body); - $armNodes[] = new MatchExpressionArm($matchArmBody, [], $arm->getLine()); - $armResult = $this->processExprNode($arm->body, $matchScope, $nodeCallback, ExpressionContext::createTopLevel()); + $armNodes[] = new MatchExpressionArm($matchArmBody, [], $arm->getStartLine()); + $armResult = $this->processExprNode($stmt, $arm->body, $matchScope, $nodeCallback, ExpressionContext::createTopLevel()); $matchScope = $armResult->getScope(); $hasYield = $hasYield || $armResult->hasYield(); $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints()); $scope = $scope->mergeWith($matchScope); continue; } @@ -2653,33 +3338,49 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { throw new ShouldNotHappenException(); } - $filteringExpr = null; + $filteringExprs = []; $armCondScope = $matchScope; $condNodes = []; foreach ($arm->conds as $armCond) { - $condNodes[] = new MatchExpressionArmCondition($armCond, $armCondScope, $armCond->getLine()); - $armCondResult = $this->processExprNode($armCond, $armCondScope, $nodeCallback, $deepContext); + $condNodes[] = new MatchExpressionArmCondition($armCond, $armCondScope, $armCond->getStartLine()); + $armCondResult = $this->processExprNode($stmt, $armCond, $armCondScope, $nodeCallback, $deepContext); $hasYield = $hasYield || $armCondResult->hasYield(); $throwPoints = array_merge($throwPoints, $armCondResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $armCondResult->getImpurePoints()); $armCondExpr = new BinaryOp\Identical($expr->cond, $armCond); - $armCondType = $armCondResult->getScope()->getType($armCondExpr); - if ($armCondType instanceof ConstantBooleanType && $armCondType->getValue()) { + $armCondResultScope = $armCondResult->getScope(); + $armCondType = $this->treatPhpDocTypesAsCertain ? $armCondResultScope->getType($armCondExpr) : $armCondResultScope->getNativeType($armCondExpr); + if ($armCondType->isTrue()->yes()) { $hasAlwaysTrueCond = true; } $armCondScope = $armCondResult->getScope()->filterByFalseyValue($armCondExpr); - if ($filteringExpr === null) { - $filteringExpr = $armCondExpr; - continue; - } + $filteringExprs[] = $armCond; + } - $filteringExpr = new BinaryOp\BooleanOr($filteringExpr, $armCondExpr); + if (count($filteringExprs) === 1) { + $filteringExpr = new BinaryOp\Identical($expr->cond, $filteringExprs[0]); + } else { + $items = []; + foreach ($filteringExprs as $filteringExpr) { + $items[] = new ArrayItem($filteringExpr); + } + $filteringExpr = new FuncCall( + new Name\FullyQualified('in_array'), + [ + new Arg($expr->cond), + new Arg(new Array_($items)), + new Arg(new ConstFetch(new Name\FullyQualified('true'))), + ], + ); } - $bodyScope = $matchScope->filterByTruthyValue($filteringExpr); + $bodyScope = $this->processExprNode($stmt, $filteringExpr, $matchScope, static function (): void { + }, $deepContext)->getTruthyScope(); $matchArmBody = new MatchExpressionArmBody($bodyScope, $arm->body); - $armNodes[] = new MatchExpressionArm($matchArmBody, $condNodes, $arm->getLine()); + $armNodes[] = new MatchExpressionArm($matchArmBody, $condNodes, $arm->getStartLine()); $armResult = $this->processExprNode( + $stmt, $arm->body, $bodyScope, $nodeCallback, @@ -2689,6 +3390,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $scope = $scope->mergeWith($armScope); $hasYield = $hasYield || $armResult->hasYield(); $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints()); $matchScope = $matchScope->filterByFalseyValue($filteringExpr); } @@ -2698,69 +3400,260 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { } $nodeCallback(new MatchExpressionNode($expr->cond, $armNodes, $expr, $matchScope), $scope); + } elseif ($expr instanceof AlwaysRememberedExpr) { + $result = $this->processExprNode($stmt, $expr->getExpr(), $scope, $nodeCallback, $context); + $hasYield = $result->hasYield(); + $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + $scope = $result->getScope(); } elseif ($expr instanceof Expr\Throw_) { $hasYield = false; - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $throwPoints[] = ThrowPoint::createExplicit($scope, $scope->getType($expr->expr), $expr, false); } elseif ($expr instanceof FunctionCallableNode) { $throwPoints = []; + $impurePoints = []; $hasYield = false; if ($expr->getName() instanceof Expr) { - $result = $this->processExprNode($expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); } } elseif ($expr instanceof MethodCallableNode) { - $result = $this->processExprNode($expr->getVar(), $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $expr->getVar(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); if ($expr->getName() instanceof Expr) { - $nameResult = $this->processExprNode($expr->getVar(), $scope, $nodeCallback, ExpressionContext::createDeep()); + $nameResult = $this->processExprNode($stmt, $expr->getVar(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $nameResult->getScope(); $hasYield = $hasYield || $nameResult->hasYield(); $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $nameResult->getImpurePoints()); } } elseif ($expr instanceof StaticMethodCallableNode) { $throwPoints = []; + $impurePoints = []; $hasYield = false; if ($expr->getClass() instanceof Expr) { - $classResult = $this->processExprNode($expr->getClass(), $scope, $nodeCallback, ExpressionContext::createDeep()); + $classResult = $this->processExprNode($stmt, $expr->getClass(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $classResult->getScope(); $hasYield = $classResult->hasYield(); $throwPoints = $classResult->getThrowPoints(); + $impurePoints = $classResult->getImpurePoints(); } if ($expr->getName() instanceof Expr) { - $nameResult = $this->processExprNode($expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep()); + $nameResult = $this->processExprNode($stmt, $expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $nameResult->getScope(); $hasYield = $hasYield || $nameResult->hasYield(); $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $nameResult->getImpurePoints()); } } elseif ($expr instanceof InstantiationCallableNode) { $throwPoints = []; + $impurePoints = []; $hasYield = false; if ($expr->getClass() instanceof Expr) { - $classResult = $this->processExprNode($expr->getClass(), $scope, $nodeCallback, ExpressionContext::createDeep()); + $classResult = $this->processExprNode($stmt, $expr->getClass(), $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $classResult->getScope(); $hasYield = $classResult->hasYield(); $throwPoints = $classResult->getThrowPoints(); + $impurePoints = $classResult->getImpurePoints(); } + } elseif ($expr instanceof Node\Scalar) { + $hasYield = false; + $throwPoints = []; + $impurePoints = []; + } elseif ($expr instanceof ConstFetch) { + $hasYield = false; + $throwPoints = []; + $impurePoints = []; + $nodeCallback($expr->name, $scope); } else { $hasYield = false; $throwPoints = []; + $impurePoints = []; } return new ExpressionResult( $scope, $hasYield, $throwPoints, + $impurePoints, static fn (): MutatingScope => $scope->filterByTruthyValue($expr), static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } + private function getArrayFunctionAppendingType(FunctionReflection $functionReflection, Scope $scope, FuncCall $expr): Type + { + $arrayArg = $expr->getArgs()[0]->value; + $arrayType = $scope->getType($arrayArg); + $callArgs = array_slice($expr->getArgs(), 1); + + /** + * @param Arg[] $callArgs + * @param callable(?Type, Type, bool): void $setOffsetValueType + */ + $setOffsetValueTypes = static function (Scope $scope, array $callArgs, callable $setOffsetValueType, ?bool &$nonConstantArrayWasUnpacked = null): void { + foreach ($callArgs as $callArg) { + $callArgType = $scope->getType($callArg->value); + if ($callArg->unpack) { + $constantArrays = $callArgType->getConstantArrays(); + if (count($constantArrays) === 1) { + $iterableValueTypes = $constantArrays[0]->getValueTypes(); + } else { + $iterableValueTypes = [$callArgType->getIterableValueType()]; + $nonConstantArrayWasUnpacked = true; + } + + $isOptional = !$callArgType->isIterableAtLeastOnce()->yes(); + foreach ($iterableValueTypes as $iterableValueType) { + if ($iterableValueType instanceof UnionType) { + foreach ($iterableValueType->getTypes() as $innerType) { + $setOffsetValueType(null, $innerType, $isOptional); + } + } else { + $setOffsetValueType(null, $iterableValueType, $isOptional); + } + } + continue; + } + $setOffsetValueType(null, $callArgType, false); + } + }; + + $constantArrays = $arrayType->getConstantArrays(); + if (count($constantArrays) > 0) { + $newArrayTypes = []; + $prepend = $functionReflection->getName() === 'array_unshift'; + foreach ($constantArrays as $constantArray) { + $arrayTypeBuilder = $prepend ? ConstantArrayTypeBuilder::createEmpty() : ConstantArrayTypeBuilder::createFromConstantArray($constantArray); + + $setOffsetValueTypes( + $scope, + $callArgs, + static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayTypeBuilder): void { + $arrayTypeBuilder->setOffsetValueType($offsetType, $valueType, $optional); + }, + $nonConstantArrayWasUnpacked, + ); + + if ($prepend) { + $keyTypes = $constantArray->getKeyTypes(); + $valueTypes = $constantArray->getValueTypes(); + foreach ($keyTypes as $k => $keyType) { + $arrayTypeBuilder->setOffsetValueType( + count($keyType->getConstantStrings()) === 1 ? $keyType->getConstantStrings()[0] : null, + $valueTypes[$k], + $constantArray->isOptionalKey($k), + ); + } + } + + $constantArray = $arrayTypeBuilder->getArray(); + + if ($constantArray->isConstantArray()->yes() && $nonConstantArrayWasUnpacked) { + $array = new ArrayType($constantArray->generalize(GeneralizePrecision::lessSpecific())->getIterableKeyType(), $constantArray->getIterableValueType()); + $isList = $constantArray->isList()->yes(); + $constantArray = $constantArray->isIterableAtLeastOnce()->yes() + ? TypeCombinator::intersect($array, new NonEmptyArrayType()) + : $array; + $constantArray = $isList + ? AccessoryArrayListType::intersectWith($constantArray) + : $constantArray; + } + + $newArrayTypes[] = $constantArray; + } + + return TypeCombinator::union(...$newArrayTypes); + } + + $setOffsetValueTypes( + $scope, + $callArgs, + static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayType): void { + $isIterableAtLeastOnce = $arrayType->isIterableAtLeastOnce()->yes() || !$optional; + $arrayType = $arrayType->setOffsetValueType($offsetType, $valueType); + if ($isIterableAtLeastOnce) { + return; + } + + $arrayType = TypeCombinator::union($arrayType, new ConstantArrayType([], [])); + }, + ); + + return $arrayType; + } + + private function getArraySortPreserveListFunctionType(Type $type): Type + { + $isIterableAtLeastOnce = $type->isIterableAtLeastOnce(); + if ($isIterableAtLeastOnce->no()) { + return $type; + } + + return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($isIterableAtLeastOnce): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if (!$type instanceof ArrayType) { + return $type; + } + + $newArrayType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $type->getIterableValueType())); + if ($isIterableAtLeastOnce->yes()) { + $newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType()); + } + + return $newArrayType; + }); + } + + private function getArraySortDoNotPreserveListFunctionType(Type $type): Type + { + $isIterableAtLeastOnce = $type->isIterableAtLeastOnce(); + if ($isIterableAtLeastOnce->no()) { + return $type; + } + + return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($isIterableAtLeastOnce): Type { + if ($type instanceof UnionType) { + return $traverse($type); + } + + $constantArrays = $type->getConstantArrays(); + if (count($constantArrays) > 0) { + $types = []; + foreach ($constantArrays as $constantArray) { + $types[] = new ConstantArrayType( + $constantArray->getKeyTypes(), + $constantArray->getValueTypes(), + $constantArray->getNextAutoIndexes(), + $constantArray->getOptionalKeys(), + $constantArray->isList()->and(TrinaryLogic::createMaybe()), + ); + } + + return TypeCombinator::union(...$types); + } + + $newArrayType = new ArrayType($type->getIterableKeyType(), $type->getIterableValueType()); + if ($isIterableAtLeastOnce->yes()) { + $newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType()); + } + + return $newArrayType; + }); + } + private function getFunctionThrowPoint( FunctionReflection $functionReflection, ?ParametersAcceptor $parametersAcceptor, @@ -2797,7 +3690,7 @@ private function getFunctionThrowPoint( } if ($throwType !== null) { - if (!$throwType instanceof VoidType) { + if (!$throwType->isVoid()->yes()) { return ThrowPoint::createExplicit($scope, $throwType, $funcCall, true); } } elseif ($this->implicitThrows) { @@ -2855,7 +3748,7 @@ private function getMethodThrowPoint(MethodReflection $methodReflection, Paramet } if ($throwType !== null) { - if (!$throwType instanceof VoidType) { + if (!$throwType->isVoid()->yes()) { return ThrowPoint::createExplicit($scope, $throwType, $methodCall, true); } } elseif ($this->implicitThrows) { @@ -2892,7 +3785,7 @@ private function getConstructorThrowPoint(MethodReflection $constructorReflectio if ($constructorReflection->getThrowType() !== null) { $throwType = $constructorReflection->getThrowType(); - if (!$throwType instanceof VoidType) { + if (!$throwType->isVoid()->yes()) { return ThrowPoint::createExplicit($scope, $throwType, $new, true); } } elseif ($this->implicitThrows) { @@ -2924,7 +3817,7 @@ private function getStaticMethodThrowPoint(MethodReflection $methodReflection, P if ($methodReflection->getThrowType() !== null) { $throwType = $methodReflection->getThrowType(); - if (!$throwType instanceof VoidType) { + if (!$throwType->isVoid()->yes()) { return ThrowPoint::createExplicit($scope, $throwType, $methodCall, true); } } elseif ($this->implicitThrows) { @@ -2990,56 +3883,27 @@ private function callNodeCallbackWithExpression( * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processClosureNode( + Node\Stmt $stmt, Expr\Closure $expr, MutatingScope $scope, callable $nodeCallback, ExpressionContext $context, ?Type $passedToType, - ): ExpressionResult + ): ProcessClosureResult { foreach ($expr->params as $param) { - $this->processParamNode($param, $scope, $nodeCallback); + $this->processParamNode($stmt, $param, $scope, $nodeCallback); } $byRefUses = []; - $callableParameters = null; $closureCallArgs = $expr->getAttribute(ClosureArgVisitor::ATTRIBUTE_NAME); - - if ($closureCallArgs !== null) { - $acceptors = $scope->getType($expr)->getCallableParametersAcceptors($scope); - if (count($acceptors) === 1) { - $callableParameters = $acceptors[0]->getParameters(); - - foreach ($callableParameters as $index => $callableParameter) { - if (!isset($closureCallArgs[$index])) { - continue; - } - - $type = $scope->getType($closureCallArgs[$index]->value); - $callableParameters[$index] = new NativeParameterReflection( - $callableParameter->getName(), - $callableParameter->isOptional(), - $type, - $callableParameter->passedByReference(), - $callableParameter->isVariadic(), - $callableParameter->getDefaultValue(), - ); - } - } - } elseif ($passedToType !== null && !$passedToType->isCallable()->no()) { - if ($passedToType instanceof UnionType) { - $passedToType = TypeCombinator::union(...array_filter( - $passedToType->getTypes(), - static fn (Type $type) => $type->isCallable()->yes(), - )); - } - - $acceptors = $passedToType->getCallableParametersAcceptors($scope); - if (count($acceptors) === 1) { - $callableParameters = $acceptors[0]->getParameters(); - } - } + $callableParameters = $this->createCallableParameters( + $scope, + $expr, + $closureCallArgs, + $passedToType, + ); $useScope = $scope; foreach ($expr->uses as $use) { @@ -3049,9 +3913,11 @@ private function processClosureNode( $inAssignRightSideVariableName = $context->getInAssignRightSideVariableName(); $inAssignRightSideType = $context->getInAssignRightSideType(); + $inAssignRightSideNativeType = $context->getInAssignRightSideNativeType(); if ( $inAssignRightSideVariableName === $use->var->name && $inAssignRightSideType !== null + && $inAssignRightSideNativeType !== null ) { if ($inAssignRightSideType instanceof ClosureType) { $variableType = $inAssignRightSideType; @@ -3063,10 +3929,20 @@ private function processClosureNode( $variableType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideType); } } - $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType); + if ($inAssignRightSideNativeType instanceof ClosureType) { + $variableNativeType = $inAssignRightSideNativeType; + } else { + $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName); + if ($alreadyHasVariableType->no()) { + $variableNativeType = TypeCombinator::union(new NullType(), $inAssignRightSideNativeType); + } else { + $variableNativeType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideNativeType); + } + } + $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType, $variableNativeType); } } - $this->processExprNode($use, $useScope, $nodeCallback, $context); + $this->processExprNode($stmt, $use->var, $useScope, $nodeCallback, $context); if (!$use->byRef) { continue; } @@ -3087,13 +3963,34 @@ private function processClosureNode( $nodeCallback(new InClosureNode($closureType, $expr), $closureScope); + $executionEnds = []; $gatheredReturnStatements = []; $gatheredYieldStatements = []; - $closureStmtsCallback = static function (Node $node, Scope $scope) use ($nodeCallback, &$gatheredReturnStatements, &$gatheredYieldStatements, &$closureScope): void { + $closureImpurePoints = []; + $invalidateExpressions = []; + $closureStmtsCallback = static function (Node $node, Scope $scope) use ($nodeCallback, &$executionEnds, &$gatheredReturnStatements, &$gatheredYieldStatements, &$closureScope, &$closureImpurePoints, &$invalidateExpressions): void { $nodeCallback($node, $scope); if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { return; } + if ($node instanceof PropertyAssignNode) { + $closureImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + return; + } + if ($node instanceof ExecutionEndNode) { + $executionEnds[] = $node; + return; + } + if ($node instanceof InvalidateExprNode) { + $invalidateExpressions[] = $node; + return; + } if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) { $gatheredYieldStatements[] = $node; } @@ -3104,15 +4001,17 @@ private function processClosureNode( $gatheredReturnStatements[] = new ReturnStatement($scope, $node); }; if (count($byRefUses) === 0) { - $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback); + $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback, StatementContext::createTopLevel()); $nodeCallback(new ClosureReturnStatementsNode( $expr, $gatheredReturnStatements, $gatheredYieldStatements, $statementResult, + $executionEnds, + array_merge($statementResult->getImpurePoints(), $closureImpurePoints), ), $closureScope); - return new ExpressionResult($scope, false, []); + return new ProcessClosureResult($scope, $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions); } $count = 0; @@ -3120,7 +4019,7 @@ private function processClosureNode( $prevScope = $closureScope; $intermediaryClosureScopeResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, static function (): void { - }); + }, StatementContext::createTopLevel()); $intermediaryClosureScope = $intermediaryClosureScopeResult->getScope(); foreach ($intermediaryClosureScopeResult->getExitPoints() as $exitPoint) { $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope()); @@ -3136,49 +4035,105 @@ private function processClosureNode( $count++; } while ($count < self::LOOP_SCOPE_ITERATIONS); - $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback); + $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback, StatementContext::createTopLevel()); $nodeCallback(new ClosureReturnStatementsNode( $expr, $gatheredReturnStatements, $gatheredYieldStatements, $statementResult, + $executionEnds, + array_merge($statementResult->getImpurePoints(), $closureImpurePoints), ), $closureScope); - return new ExpressionResult($scope->processClosureScope($closureScope, null, $byRefUses), false, []); + return new ProcessClosureResult($scope->processClosureScope($closureScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions); + } + + /** + * @param InvalidateExprNode[] $invalidatedExpressions + * @param string[] $uses + */ + private function processImmediatelyCalledCallable(MutatingScope $scope, array $invalidatedExpressions, array $uses): MutatingScope + { + if ($scope->isInClass()) { + $uses[] = 'this'; + } + + $finder = new NodeFinder(); + foreach ($invalidatedExpressions as $invalidateExpression) { + $found = false; + foreach ($uses as $use) { + $result = $finder->findFirst([$invalidateExpression->getExpr()], static fn ($node) => $node instanceof Variable && $node->name === $use); + if ($result === null) { + continue; + } + + $found = true; + break; + } + + if (!$found) { + continue; + } + + $scope = $scope->invalidateExpression($invalidateExpression->getExpr(), true); + } + + return $scope; } /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processArrowFunctionNode( + Node\Stmt $stmt, Expr\ArrowFunction $expr, MutatingScope $scope, callable $nodeCallback, - ExpressionContext $context, ?Type $passedToType, ): ExpressionResult { foreach ($expr->params as $param) { - $this->processParamNode($param, $scope, $nodeCallback); + $this->processParamNode($stmt, $param, $scope, $nodeCallback); } if ($expr->returnType !== null) { $nodeCallback($expr->returnType, $scope); } - $callableParameters = null; $arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME); + $arrowFunctionScope = $scope->enterArrowFunction($expr, $this->createCallableParameters( + $scope, + $expr, + $arrowFunctionCallArgs, + $passedToType, + )); + $arrowFunctionType = $arrowFunctionScope->getAnonymousFunctionReflection(); + if (!$arrowFunctionType instanceof ClosureType) { + throw new ShouldNotHappenException(); + } + $nodeCallback(new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope); + $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $nodeCallback, ExpressionContext::createTopLevel()); - if ($arrowFunctionCallArgs !== null) { - $acceptors = $scope->getType($expr)->getCallableParametersAcceptors($scope); + return new ExpressionResult($scope, false, $exprResult->getThrowPoints(), $exprResult->getImpurePoints()); + } + + /** + * @param Node\Arg[] $args + * @return ParameterReflection[]|null + */ + private function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array + { + $callableParameters = null; + if ($args !== null) { + $acceptors = $scope->getType($closureExpr)->getCallableParametersAcceptors($scope); if (count($acceptors) === 1) { $callableParameters = $acceptors[0]->getParameters(); foreach ($callableParameters as $index => $callableParameter) { - if (!isset($arrowFunctionCallArgs[$index])) { + if (!isset($args[$index])) { continue; } - $type = $scope->getType($arrowFunctionCallArgs[$index]->value); + $type = $scope->getType($args[$index]->value); $callableParameters[$index] = new NativeParameterReflection( $callableParameter->getName(), $callableParameter->isOptional(), @@ -3198,28 +4153,56 @@ private function processArrowFunctionNode( } $acceptors = $passedToType->getCallableParametersAcceptors($scope); - if (count($acceptors) === 1) { - $callableParameters = $acceptors[0]->getParameters(); + if (count($acceptors) > 0) { + foreach ($acceptors as $acceptor) { + if ($callableParameters === null) { + $callableParameters = array_map(static fn (ParameterReflection $callableParameter) => new NativeParameterReflection( + $callableParameter->getName(), + $callableParameter->isOptional(), + $callableParameter->getType(), + $callableParameter->passedByReference(), + $callableParameter->isVariadic(), + $callableParameter->getDefaultValue(), + ), $acceptor->getParameters()); + continue; + } + + $newParameters = []; + foreach ($acceptor->getParameters() as $i => $callableParameter) { + if (!array_key_exists($i, $callableParameters)) { + $newParameters[] = $callableParameter; + continue; + } + + $newParameters[] = $callableParameters[$i]->union(new NativeParameterReflection( + $callableParameter->getName(), + $callableParameter->isOptional(), + $callableParameter->getType(), + $callableParameter->passedByReference(), + $callableParameter->isVariadic(), + $callableParameter->getDefaultValue(), + )); + } + + $callableParameters = $newParameters; + } } } - $arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters); - $nodeCallback(new InArrowFunctionNode($expr), $arrowFunctionScope); - $this->processExprNode($expr->expr, $arrowFunctionScope, $nodeCallback, ExpressionContext::createTopLevel()); - - return new ExpressionResult($scope, false, []); + return $callableParameters; } /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processParamNode( + Node\Stmt $stmt, Node\Param $param, MutatingScope $scope, callable $nodeCallback, ): void { - $this->processAttributeGroups($param->attrGroups, $scope, $nodeCallback); + $this->processAttributeGroups($stmt, $param->attrGroups, $scope, $nodeCallback); $nodeCallback($param, $scope); if ($param->type !== null) { $nodeCallback($param->type, $scope); @@ -3228,7 +4211,7 @@ private function processParamNode( return; } - $this->processExprNode($param->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + $this->processExprNode($stmt, $param->default, $scope, $nodeCallback, ExpressionContext::createDeep()); } /** @@ -3236,6 +4219,7 @@ private function processParamNode( * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processAttributeGroups( + Node\Stmt $stmt, array $attrGroups, MutatingScope $scope, callable $nodeCallback, @@ -3244,7 +4228,7 @@ private function processAttributeGroups( foreach ($attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { foreach ($attr->args as $arg) { - $this->processExprNode($arg->value, $scope, $nodeCallback, ExpressionContext::createDeep()); + $this->processExprNode($stmt, $arg->value, $scope, $nodeCallback, ExpressionContext::createDeep()); $nodeCallback($arg, $scope); } $nodeCallback($attr, $scope); @@ -3259,7 +4243,9 @@ private function processAttributeGroups( * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processArgs( + Node\Stmt $stmt, $calleeReflection, + ?ExtendedMethodReflection $nakedMethodReflection, ?ParametersAcceptor $parametersAcceptor, array $args, MutatingScope $scope, @@ -3272,64 +4258,249 @@ private function processArgs( $parameters = $parametersAcceptor->getParameters(); } - if ($calleeReflection !== null) { - $scope = $scope->pushInFunctionCall($calleeReflection); - } - $hasYield = false; $throwPoints = []; + $impurePoints = []; foreach ($args as $i => $arg) { - $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; - $nodeCallback($originalArg, $scope); + $assignByReference = false; + $parameter = null; + $parameterType = null; + $parameterNativeType = null; if (isset($parameters) && $parametersAcceptor !== null) { - $assignByReference = false; if (isset($parameters[$i])) { $assignByReference = $parameters[$i]->passedByReference()->createsNewVariable(); $parameterType = $parameters[$i]->getType(); + + if ($parameters[$i] instanceof ParameterReflectionWithPhpDocs) { + $parameterNativeType = $parameters[$i]->getNativeType(); + } + $parameter = $parameters[$i]; } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { $lastParameter = $parameters[count($parameters) - 1]; $assignByReference = $lastParameter->passedByReference()->createsNewVariable(); $parameterType = $lastParameter->getType(); + + if ($lastParameter instanceof ParameterReflectionWithPhpDocs) { + $parameterNativeType = $lastParameter->getNativeType(); + } + $parameter = $lastParameter; } + } - if ($assignByReference) { - $argValue = $arg->value; - if ($argValue instanceof Variable && is_string($argValue->name)) { - $scope = $scope->assignVariable($argValue->name, new MixedType()); + $lookForUnset = false; + if ($assignByReference) { + if ($arg->value instanceof Variable) { + $isBuiltin = false; + if ($calleeReflection instanceof FunctionReflection && $calleeReflection->isBuiltin()) { + $isBuiltin = true; + } elseif ($calleeReflection instanceof ExtendedMethodReflection && $calleeReflection->getDeclaringClass()->isBuiltin()) { + $isBuiltin = true; + } + if ( + $isBuiltin + || ($parameterNativeType === null || !$parameterNativeType->isNull()->no()) + ) { + $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $arg->value); + $lookForUnset = true; + } + } + } + + if ($calleeReflection !== null) { + $scope = $scope->pushInFunctionCall($calleeReflection, $parameter); + } + + $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; + $nodeCallback($originalArg, $scope); + + $originalScope = $scope; + $scopeToPass = $scope; + if ($i === 0 && $closureBindScope !== null) { + $scopeToPass = $closureBindScope; + } + + if ($parameter instanceof ParameterReflectionWithPhpDocs) { + $parameterCallImmediately = $parameter->isImmediatelyInvokedCallable(); + if ($parameterCallImmediately->maybe()) { + $callCallbackImmediately = $calleeReflection instanceof FunctionReflection; + } else { + $callCallbackImmediately = $parameterCallImmediately->yes(); + } + } else { + $callCallbackImmediately = $calleeReflection instanceof FunctionReflection; + } + if ($arg->value instanceof Expr\Closure) { + $restoreThisScope = null; + if ( + $closureBindScope === null + && $parameter instanceof ParameterReflectionWithPhpDocs + && $parameter->getClosureThisType() !== null + && !$arg->value->static + ) { + $restoreThisScope = $scopeToPass; + $scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType()); + } + + $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context); + $closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context, $parameterType ?? null); + if ($callCallbackImmediately) { + $throwPoints = array_merge($throwPoints, array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $arg->value), $closureResult->getThrowPoints())); + $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints()); + } + + $uses = []; + foreach ($arg->value->uses as $use) { + if (!is_string($use->var->name)) { + continue; + } + + $uses[] = $use->var->name; + } + + $scope = $closureResult->getScope(); + $invalidateExpressions = $closureResult->getInvalidateExpressions(); + if ($restoreThisScope !== null) { + $nodeFinder = new NodeFinder(); + $cb = static fn ($expr) => $expr instanceof Variable && $expr->name === 'this'; + foreach ($invalidateExpressions as $j => $invalidateExprNode) { + $foundThis = $nodeFinder->findFirst([$invalidateExprNode->getExpr()], $cb); + if ($foundThis === null) { + continue; + } + + unset($invalidateExpressions[$j]); + } + $invalidateExpressions = array_values($invalidateExpressions); + $scope = $scope->restoreThis($restoreThisScope); + } + + $scope = $this->processImmediatelyCalledCallable($scope, $invalidateExpressions, $uses); + } elseif ($arg->value instanceof Expr\ArrowFunction) { + if ( + $closureBindScope === null + && $parameter instanceof ParameterReflectionWithPhpDocs + && $parameter->getClosureThisType() !== null + && !$arg->value->static + ) { + $scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType()); + } + + $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context); + $arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $parameterType ?? null); + if ($callCallbackImmediately) { + $throwPoints = array_merge($throwPoints, array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $arg->value), $arrowFunctionResult->getThrowPoints())); + $impurePoints = array_merge($impurePoints, $arrowFunctionResult->getImpurePoints()); + } + } else { + $exprType = $scope->getType($arg->value); + $exprResult = $this->processExprNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context->enterDeep()); + $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); + $scope = $exprResult->getScope(); + $hasYield = $hasYield || $exprResult->hasYield(); + + if ($exprType->isCallable()->yes()) { + $acceptors = $exprType->getCallableParametersAcceptors($scope); + if (count($acceptors) === 1) { + $scope = $this->processImmediatelyCalledCallable($scope, $acceptors[0]->getInvalidateExpressions(), $acceptors[0]->getUsedVariables()); + if ($callCallbackImmediately) { + $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $arg->value), $acceptors[0]->getThrowPoints()); + if (!$this->implicitThrows) { + $callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit())); + } + $throwPoints = array_merge($throwPoints, $callableThrowPoints); + $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $arg->value, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $acceptors[0]->getImpurePoints())); + } } } } - $originalScope = $scope; - $scopeToPass = $scope; - if ($i === 0 && $closureBindScope !== null) { - $scopeToPass = $closureBindScope; + if ($assignByReference && $lookForUnset) { + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $arg->value); } - if ($arg->value instanceof Expr\Closure) { - $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context); - $result = $this->processClosureNode($arg->value, $scopeToPass, $nodeCallback, $context, $parameterType ?? null); - } elseif ($arg->value instanceof Expr\ArrowFunction) { - $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context); - $result = $this->processArrowFunctionNode($arg->value, $scopeToPass, $nodeCallback, $context, $parameterType ?? null); - } else { - $result = $this->processExprNode($arg->value, $scopeToPass, $nodeCallback, $context->enterDeep()); + if ($calleeReflection !== null) { + $scope = $scope->popInFunctionCall(); } - $scope = $result->getScope(); - $hasYield = $hasYield || $result->hasYield(); - $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + if ($i !== 0 || $closureBindScope === null) { continue; } $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope); } + foreach ($args as $i => $arg) { + if (!isset($parameters) || $parametersAcceptor === null) { + continue; + } - if ($calleeReflection !== null) { - $scope = $scope->popInFunctionCall(); + $byRefType = new MixedType(); + $assignByReference = false; + $currentParameter = null; + if (isset($parameters[$i])) { + $currentParameter = $parameters[$i]; + } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { + $currentParameter = $parameters[count($parameters) - 1]; + } + + if ($currentParameter !== null) { + $assignByReference = $currentParameter->passedByReference()->createsNewVariable(); + if ($assignByReference) { + if ($currentParameter instanceof ParameterReflectionWithPhpDocs && $currentParameter->getOutType() !== null) { + $byRefType = $currentParameter->getOutType(); + } elseif ( + $calleeReflection instanceof MethodReflection + && !$calleeReflection->getDeclaringClass()->isBuiltin() + && $this->paramOutType + ) { + $byRefType = $currentParameter->getType(); + } elseif ( + $calleeReflection instanceof FunctionReflection + && !$calleeReflection->isBuiltin() + && $this->paramOutType + ) { + $byRefType = $currentParameter->getType(); + } + } + } + + if ($assignByReference) { + $argValue = $arg->value; + if ($argValue instanceof Variable && is_string($argValue->name)) { + $nodeCallback(new VariableAssignNode($argValue, new TypeExpr($byRefType), false), $scope); + $scope = $scope->assignVariable($argValue->name, $byRefType, new MixedType()); + } else { + $scope = $scope->invalidateExpression($argValue); + } + } elseif ($calleeReflection !== null && $calleeReflection->hasSideEffects()->yes()) { + $argType = $scope->getType($arg->value); + if (!$argType->isObject()->no()) { + $nakedReturnType = null; + if ($nakedMethodReflection !== null) { + $nakedParametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $args, + $nakedMethodReflection->getVariants(), + $nakedMethodReflection->getNamedArgumentsVariants(), + ); + $nakedReturnType = $nakedParametersAcceptor->getReturnType(); + } + if ( + $nakedReturnType === null + || !(new ThisType($nakedMethodReflection->getDeclaringClass()))->isSuperTypeOf($nakedReturnType)->yes() + || $nakedMethodReflection->isPure()->no() + ) { + $nodeCallback(new InvalidateExprNode($arg->value), $scope); + $scope = $scope->invalidateExpression($arg->value, true); + } + } elseif (!(new ResourceType())->isSuperTypeOf($argType)->no()) { + $nodeCallback(new InvalidateExprNode($arg->value), $scope); + $scope = $scope->invalidateExpression($arg->value, true); + } + } } - return new ExpressionResult($scope, $hasYield, $throwPoints); + return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints); } /** @@ -3338,6 +4509,7 @@ private function processArgs( */ private function processAssignVar( MutatingScope $scope, + Node\Stmt $stmt, Expr $var, Expr $assignedExpr, callable $nodeCallback, @@ -3349,19 +4521,49 @@ private function processAssignVar( $nodeCallback($var, $enterExpressionAssign ? $scope->enterExpressionAssign($var) : $scope); $hasYield = false; $throwPoints = []; + $impurePoints = []; $isAssignOp = $assignedExpr instanceof Expr\AssignOp && !$enterExpressionAssign; if ($var instanceof Variable && is_string($var->name)) { $result = $processExprCallback($scope); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); + if (in_array($var->name, Scope::SUPERGLOBAL_VARIABLES, true)) { + $impurePoints[] = new ImpurePoint($scope, $var, 'superglobal', 'assign to superglobal variable', true); + } $assignedExpr = $this->unwrapAssign($assignedExpr); $type = $scope->getType($assignedExpr); - $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createTruthy()); - $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createFalsey()); $conditionalExpressions = []; + if ($assignedExpr instanceof Ternary) { + $if = $assignedExpr->if; + if ($if === null) { + $if = $assignedExpr->cond; + } + $condScope = $this->processExprNode($stmt, $assignedExpr->cond, $scope, static function (): void { + }, ExpressionContext::createDeep())->getScope(); + $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($condScope, $assignedExpr->cond, TypeSpecifierContext::createTruthy()); + $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($condScope, $assignedExpr->cond, TypeSpecifierContext::createFalsey()); + $truthyScope = $condScope->filterBySpecifiedTypes($truthySpecifiedTypes); + $falsyScope = $condScope->filterBySpecifiedTypes($falseySpecifiedTypes); + $truthyType = $truthyScope->getType($if); + $falseyType = $falsyScope->getType($assignedExpr->else); + + if ( + $truthyType->isSuperTypeOf($falseyType)->no() + && $falseyType->isSuperTypeOf($truthyType)->no() + ) { + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); + } + } + + $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createTruthy()); + $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createFalsey()); - $truthyType = TypeCombinator::remove($type, StaticTypeFactory::falsey()); + $truthyType = TypeCombinator::removeFalsey($type); $falseyType = TypeCombinator::intersect($type, StaticTypeFactory::falsey()); $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType); @@ -3369,12 +4571,13 @@ private function processAssignVar( $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); - $scope = $result->getScope()->assignVariable($var->name, $type); + $nodeCallback(new VariableAssignNode($var, $assignedExpr, $isAssignOp), $result->getScope()); + $scope = $result->getScope()->assignVariable($var->name, $type, $scope->getNativeType($assignedExpr)); foreach ($conditionalExpressions as $exprString => $holders) { $scope = $scope->addConditionalExpressions($exprString, $holders); } } elseif ($var instanceof ArrayDimFetch) { - $dimExprStack = []; + $dimFetchStack = []; $originalVar = $var; $assignedPropertyExpr = $assignedExpr; while ($var instanceof ArrayDimFetch) { @@ -3387,7 +4590,7 @@ private function processAssignVar( $var->dim, $assignedPropertyExpr, ); - $dimExprStack[] = $var->dim; + $dimFetchStack[] = $var; $var = $var->var; } @@ -3395,9 +4598,10 @@ private function processAssignVar( if ($enterExpressionAssign) { $scope = $scope->enterExpressionAssign($var); } - $result = $this->processExprNode($var, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $var, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); + $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); if ($enterExpressionAssign) { $scope = $scope->exitExpressionAssign($var); @@ -3405,17 +4609,29 @@ private function processAssignVar( // 2. eval dimensions $offsetTypes = []; - foreach (array_reverse($dimExprStack) as $dimExpr) { + $offsetNativeTypes = []; + $dimFetchStack = array_reverse($dimFetchStack); + $lastDimKey = array_key_last($dimFetchStack); + foreach ($dimFetchStack as $key => $dimFetch) { + $dimExpr = $dimFetch->dim; + + // Callback was already called for last dim at the beginning of the method. + if ($key !== $lastDimKey) { + $nodeCallback($dimFetch, $enterExpressionAssign ? $scope->enterExpressionAssign($dimFetch) : $scope); + } + if ($dimExpr === null) { $offsetTypes[] = null; + $offsetNativeTypes[] = null; } else { $offsetTypes[] = $scope->getType($dimExpr); + $offsetNativeTypes[] = $scope->getNativeType($dimExpr); if ($enterExpressionAssign) { $scope->enterExpressionAssign($dimExpr); } - $result = $this->processExprNode($dimExpr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $dimExpr, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $scope = $result->getScope(); @@ -3427,22 +4643,31 @@ private function processAssignVar( } $valueToWrite = $scope->getType($assignedExpr); + $nativeValueToWrite = $scope->getNativeType($assignedExpr); $originalValueToWrite = $valueToWrite; + $originalNativeValueToWrite = $valueToWrite; // 3. eval assigned expr $result = $processExprCallback($scope); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); $varType = $scope->getType($var); + $varNativeType = $scope->getNativeType($var); // 4. compose types if ($varType instanceof ErrorType) { $varType = new ConstantArrayType([], []); } + if ($varNativeType instanceof ErrorType) { + $varNativeType = new ConstantArrayType([], []); + } $offsetValueType = $varType; + $offsetNativeValueType = $varNativeType; $offsetValueTypeStack = [$offsetValueType]; + $offsetValueNativeTypeStack = [$offsetNativeValueType]; foreach (array_slice($offsetTypes, 0, -1) as $offsetType) { if ($offsetType === null) { $offsetValueType = new ConstantArrayType([], []); @@ -3456,6 +4681,19 @@ private function processAssignVar( $offsetValueTypeStack[] = $offsetValueType; } + foreach (array_slice($offsetNativeTypes, 0, -1) as $offsetNativeType) { + if ($offsetNativeType === null) { + $offsetNativeValueType = new ConstantArrayType([], []); + + } else { + $offsetNativeValueType = $offsetNativeValueType->getOffsetValueType($offsetNativeType); + if ($offsetNativeValueType instanceof ErrorType) { + $offsetNativeValueType = new ConstantArrayType([], []); + } + } + + $offsetValueNativeTypeStack[] = $offsetNativeValueType; + } foreach (array_reverse($offsetTypes) as $i => $offsetType) { /** @var Type $offsetValueType */ @@ -3466,24 +4704,45 @@ private function processAssignVar( new ObjectType(ArrayAccess::class), new NullType(), ]; - if ($offsetType !== null && (new IntegerType())->isSuperTypeOf($offsetType)->yes()) { + if ($offsetType !== null && $offsetType->isInteger()->yes()) { $types[] = new StringType(); } $offsetValueType = TypeCombinator::intersect($offsetValueType, TypeCombinator::union(...$types)); } $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0); } + foreach (array_reverse($offsetNativeTypes) as $i => $offsetNativeType) { + /** @var Type $offsetNativeValueType */ + $offsetNativeValueType = array_pop($offsetValueNativeTypeStack); + if (!$offsetNativeValueType instanceof MixedType) { + $types = [ + new ArrayType(new MixedType(), new MixedType()), + new ObjectType(ArrayAccess::class), + new NullType(), + ]; + if ($offsetNativeType !== null && $offsetNativeType->isInteger()->yes()) { + $types[] = new StringType(); + } + $offsetNativeValueType = TypeCombinator::intersect($offsetNativeValueType, TypeCombinator::union(...$types)); + } + $nativeValueToWrite = $offsetNativeValueType->setOffsetValueType($offsetNativeType, $nativeValueToWrite, $i === 0); + } if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { if ($var instanceof Variable && is_string($var->name)) { - $scope = $scope->assignVariable($var->name, $valueToWrite); + $nodeCallback(new VariableAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite); } else { if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) { + $scope = $scope->assignInitializedProperty($scope->getType($var->var), $var->name->toString()); + } } $scope = $scope->assignExpression( $var, $valueToWrite, + $nativeValueToWrite, ); } @@ -3493,17 +4752,24 @@ private function processAssignVar( $scope = $scope->assignExpression( $originalVar, $originalValueToWrite, + $originalNativeValueToWrite, ); } } } else { - if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { + if ($var instanceof Variable) { + $nodeCallback(new VariableAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + } elseif ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) { + $scope = $scope->assignInitializedProperty($scope->getType($var->var), $var->name->toString()); + } } } if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) { $throwPoints = array_merge($throwPoints, $this->processExprNode( + $stmt, new MethodCall($var, 'offsetSet'), $scope, static function (): void { @@ -3512,24 +4778,27 @@ static function (): void { )->getThrowPoints()); } } elseif ($var instanceof PropertyFetch) { - $objectResult = $this->processExprNode($var->var, $scope, $nodeCallback, $context); + $objectResult = $this->processExprNode($stmt, $var->var, $scope, $nodeCallback, $context); $hasYield = $objectResult->hasYield(); $throwPoints = $objectResult->getThrowPoints(); + $impurePoints = $objectResult->getImpurePoints(); $scope = $objectResult->getScope(); $propertyName = null; if ($var->name instanceof Node\Identifier) { $propertyName = $var->name->name; } else { - $propertyNameResult = $this->processExprNode($var->name, $scope, $nodeCallback, $context); + $propertyNameResult = $this->processExprNode($stmt, $var->name, $scope, $nodeCallback, $context); $hasYield = $hasYield || $propertyNameResult->hasYield(); $throwPoints = array_merge($throwPoints, $propertyNameResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $propertyNameResult->getImpurePoints()); $scope = $propertyNameResult->getScope(); } $result = $processExprCallback($scope); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); $propertyHolderType = $scope->getType($var->var); @@ -3538,23 +4807,29 @@ static function (): void { $assignedExprType = $scope->getType($assignedExpr); $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); if ($propertyReflection->canChangeTypeAfterAssignment()) { - $scope = $scope->assignExpression($var, $assignedExprType); + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); } $declaringClass = $propertyReflection->getDeclaringClass(); - if ( - $declaringClass->hasNativeProperty($propertyName) - && !$declaringClass->getNativeProperty($propertyName)->getNativeType()->accepts($assignedExprType, true)->yes() - ) { - $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(TypeError::class), $assignedExpr, false); + if ($declaringClass->hasNativeProperty($propertyName)) { + $nativeProperty = $declaringClass->getNativeProperty($propertyName); + if ( + !$nativeProperty->getNativeType()->accepts($assignedExprType, true)->yes() + ) { + $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(TypeError::class), $assignedExpr, false); + } + if ($enterExpressionAssign) { + $scope = $scope->assignInitializedProperty($propertyHolderType, $propertyName); + } } } else { // fallback $assignedExprType = $scope->getType($assignedExpr); $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); - $scope = $scope->assignExpression($var, $assignedExprType); + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); // simulate dynamic property assign by __set to get throw points if (!$propertyHolderType->hasMethod('__set')->no()) { $throwPoints = array_merge($throwPoints, $this->processExprNode( + $stmt, new MethodCall($var->var, '__set'), $scope, static function (): void { @@ -3568,25 +4843,25 @@ static function (): void { if ($var->class instanceof Node\Name) { $propertyHolderType = $scope->resolveTypeByName($var->class); } else { - $this->processExprNode($var->class, $scope, $nodeCallback, $context); + $this->processExprNode($stmt, $var->class, $scope, $nodeCallback, $context); $propertyHolderType = $scope->getType($var->class); } $propertyName = null; if ($var->name instanceof Node\Identifier) { $propertyName = $var->name->name; - $hasYield = false; - $throwPoints = []; } else { - $propertyNameResult = $this->processExprNode($var->name, $scope, $nodeCallback, $context); + $propertyNameResult = $this->processExprNode($stmt, $var->name, $scope, $nodeCallback, $context); $hasYield = $propertyNameResult->hasYield(); $throwPoints = $propertyNameResult->getThrowPoints(); + $impurePoints = $propertyNameResult->getImpurePoints(); $scope = $propertyNameResult->getScope(); } $result = $processExprCallback($scope); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); if ($propertyName !== null) { @@ -3594,18 +4869,19 @@ static function (): void { $assignedExprType = $scope->getType($assignedExpr); $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) { - $scope = $scope->assignExpression($var, $assignedExprType); + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); } } else { // fallback $assignedExprType = $scope->getType($assignedExpr); $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); - $scope = $scope->assignExpression($var, $assignedExprType); + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); } } elseif ($var instanceof List_ || $var instanceof Array_) { $result = $processExprCallback($scope); $hasYield = $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $scope = $result->getScope(); foreach ($var->items as $i => $arrayItem) { if ($arrayItem === null) { @@ -3617,9 +4893,19 @@ static function (): void { $itemScope = $itemScope->enterExpressionAssign($arrayItem->value); } $itemScope = $this->lookForSetAllowedUndefinedExpressions($itemScope, $arrayItem->value); - $itemResult = $this->processExprNode($arrayItem, $itemScope, $nodeCallback, $context->enterDeep()); - $hasYield = $hasYield || $itemResult->hasYield(); - $throwPoints = array_merge($throwPoints, $itemResult->getThrowPoints()); + $nodeCallback($arrayItem, $itemScope); + if ($arrayItem->key !== null) { + $keyResult = $this->processExprNode($stmt, $arrayItem->key, $itemScope, $nodeCallback, $context->enterDeep()); + $hasYield = $hasYield || $keyResult->hasYield(); + $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); + $itemScope = $keyResult->getScope(); + } + + $valueResult = $this->processExprNode($stmt, $arrayItem->value, $itemScope, $nodeCallback, $context->enterDeep()); + $hasYield = $hasYield || $valueResult->hasYield(); + $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); if ($arrayItem->key === null) { $dimExpr = new Node\Scalar\LNumber($i); @@ -3628,20 +4914,89 @@ static function (): void { } $result = $this->processAssignVar( $scope, + $stmt, $arrayItem->value, new GetOffsetValueTypeExpr($assignedExpr, $dimExpr), $nodeCallback, $context, - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), $enterExpressionAssign, ); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + } + } elseif ($var instanceof ExistingArrayDimFetch) { + $dimFetchStack = []; + $assignedPropertyExpr = $assignedExpr; + while ($var instanceof ExistingArrayDimFetch) { + $varForSetOffsetValue = $var->getVar(); + if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) { + $varForSetOffsetValue = new OriginalPropertyTypeExpr($varForSetOffsetValue); + } + $assignedPropertyExpr = new SetExistingOffsetValueTypeExpr( + $varForSetOffsetValue, + $var->getDim(), + $assignedPropertyExpr, + ); + $dimFetchStack[] = $var; + $var = $var->getVar(); + } + + $offsetTypes = []; + $offsetNativeTypes = []; + foreach (array_reverse($dimFetchStack) as $dimFetch) { + $dimExpr = $dimFetch->getDim(); + $offsetTypes[] = $scope->getType($dimExpr); + $offsetNativeTypes[] = $scope->getNativeType($dimExpr); + } + + $valueToWrite = $scope->getType($assignedExpr); + $nativeValueToWrite = $scope->getNativeType($assignedExpr); + $varType = $scope->getType($var); + $varNativeType = $scope->getNativeType($var); + + $offsetValueType = $varType; + $offsetNativeValueType = $varNativeType; + $offsetValueTypeStack = [$offsetValueType]; + $offsetValueNativeTypeStack = [$offsetNativeValueType]; + foreach (array_slice($offsetTypes, 0, -1) as $offsetType) { + $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); + $offsetValueTypeStack[] = $offsetValueType; + } + foreach (array_slice($offsetNativeTypes, 0, -1) as $offsetNativeType) { + $offsetNativeValueType = $offsetNativeValueType->getOffsetValueType($offsetNativeType); + $offsetValueNativeTypeStack[] = $offsetNativeValueType; + } + + foreach (array_reverse($offsetTypes) as $offsetType) { + /** @var Type $offsetValueType */ + $offsetValueType = array_pop($offsetValueTypeStack); + $valueToWrite = $offsetValueType->setExistingOffsetValueType($offsetType, $valueToWrite); + } + foreach (array_reverse($offsetNativeTypes) as $offsetNativeType) { + /** @var Type $offsetNativeValueType */ + $offsetNativeValueType = array_pop($offsetValueNativeTypeStack); + $nativeValueToWrite = $offsetNativeValueType->setExistingOffsetValueType($offsetNativeType, $nativeValueToWrite); + } + + if ($var instanceof Variable && is_string($var->name)) { + $nodeCallback(new VariableAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite); + } else { + if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { + $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + } + $scope = $scope->assignExpression( + $var, + $valueToWrite, + $nativeValueToWrite, + ); } } - return new ExpressionResult($scope, $hasYield, $throwPoints); + return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints); } private function unwrapAssign(Expr $expr): Expr @@ -3667,13 +5022,18 @@ private function processSureTypesForConditionalExpressionsAfterAssign(Scope $sco continue; } + if ($expr->name === $variableName) { + continue; + } + if (!isset($conditionalExpressions[$exprString])) { $conditionalExpressions[$exprString] = []; } $holder = new ConditionalExpressionHolder([ - '$' . $variableName => $variableType, - ], VariableTypeHolder::createYes( + '$' . $variableName => ExpressionTypeHolder::createYes(new Variable($variableName), $variableType), + ], ExpressionTypeHolder::createYes( + $expr, TypeCombinator::intersect($scope->getType($expr), $exprType), )); $conditionalExpressions[$exprString][$holder->getKey()] = $holder; @@ -3696,13 +5056,18 @@ private function processSureNotTypesForConditionalExpressionsAfterAssign(Scope $ continue; } + if ($expr->name === $variableName) { + continue; + } + if (!isset($conditionalExpressions[$exprString])) { $conditionalExpressions[$exprString] = []; } $holder = new ConditionalExpressionHolder([ - '$' . $variableName => $variableType, - ], VariableTypeHolder::createYes( + '$' . $variableName => ExpressionTypeHolder::createYes(new Variable($variableName), $variableType), + ], ExpressionTypeHolder::createYes( + $expr, TypeCombinator::remove($scope->getType($expr), $exprType), )); $conditionalExpressions[$exprString][$holder->getKey()] = $holder; @@ -3711,7 +5076,10 @@ private function processSureNotTypesForConditionalExpressionsAfterAssign(Scope $ return $conditionalExpressions; } - private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, ?Expr $defaultExpr): MutatingScope + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, ?Expr $defaultExpr, callable $nodeCallback): MutatingScope { $function = $scope->getFunction(); $variableLessTags = []; @@ -3762,12 +5130,28 @@ private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, $certainty = TrinaryLogic::createYes(); } - $scope = $scope->assignVariable($name, $varTag->getType(), $certainty); + $variableNode = new Variable($name, $stmt->getAttributes()); + $originalType = $scope->getVariableType($name); + if (!$originalType->equals($varTag->getType())) { + $nodeCallback(new VarTagChangedExpressionTypeNode($varTag, $variableNode), $scope); + } + + $scope = $scope->assignVariable( + $name, + $varTag->getType(), + $scope->getNativeType($variableNode), + $certainty, + ); } } if (count($variableLessTags) === 1 && $defaultExpr !== null) { - $scope = $scope->assignExpression($defaultExpr, $variableLessTags[0]->getType()); + $originalType = $scope->getType($defaultExpr); + $varTag = $variableLessTags[0]; + if (!$originalType->equals($varTag->getType())) { + $nodeCallback(new VarTagChangedExpressionTypeNode($varTag, $defaultExpr), $scope); + } + $scope = $scope->assignExpression($defaultExpr, $varTag->getType(), new MixedType()); } return $scope; @@ -3776,7 +5160,7 @@ private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, /** * @param array $variableNames */ - private function processVarAnnotation(MutatingScope $scope, array $variableNames, Node $node, bool &$changed = false): MutatingScope + private function processVarAnnotation(MutatingScope $scope, array $variableNames, Node\Stmt $node, bool &$changed = false): MutatingScope { $function = $scope->getFunction(); $varTags = []; @@ -3808,24 +5192,24 @@ private function processVarAnnotation(MutatingScope $scope, array $variableNames $variableType = $varTags[$variableName]->getType(); $changed = true; - $scope = $scope->assignVariable($variableName, $variableType); + $scope = $scope->assignVariable($variableName, $variableType, new MixedType()); } if (count($variableNames) === 1 && count($varTags) === 1 && isset($varTags[0])) { $variableType = $varTags[0]->getType(); $changed = true; - $scope = $scope->assignVariable($variableNames[0], $variableType); + $scope = $scope->assignVariable($variableNames[0], $variableType, new MixedType()); } return $scope; } - private function enterForeach(MutatingScope $scope, Foreach_ $stmt): MutatingScope + private function enterForeach(MutatingScope $scope, MutatingScope $originalScope, Foreach_ $stmt): MutatingScope { if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt); } - $iterateeType = $scope->getType($stmt->expr); + $iterateeType = $originalScope->getType($stmt->expr); if ( ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)) && ($stmt->keyVar === null || ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name))) @@ -3835,6 +5219,7 @@ private function enterForeach(MutatingScope $scope, Foreach_ $stmt): MutatingSco $keyVarName = $stmt->keyVar->name; } $scope = $scope->enterForeach( + $originalScope, $stmt->expr, $stmt->valueVar->name, $keyVarName, @@ -3846,54 +5231,69 @@ private function enterForeach(MutatingScope $scope, Foreach_ $stmt): MutatingSco } else { $scope = $this->processAssignVar( $scope, + $stmt, $stmt->valueVar, new GetIterableValueTypeExpr($stmt->expr), static function (): void { }, ExpressionContext::createDeep(), - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), true, )->getScope(); $vars = $this->getAssignedVariables($stmt->valueVar); if ( $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name) ) { - $scope = $scope->enterForeachKey($stmt->expr, $stmt->keyVar->name); + $scope = $scope->enterForeachKey($originalScope, $stmt->expr, $stmt->keyVar->name); $vars[] = $stmt->keyVar->name; } elseif ($stmt->keyVar !== null) { $scope = $this->processAssignVar( $scope, + $stmt, $stmt->keyVar, new GetIterableKeyTypeExpr($stmt->expr), static function (): void { }, ExpressionContext::createDeep(), - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []), true, )->getScope(); $vars = array_merge($vars, $this->getAssignedVariables($stmt->keyVar)); } } + $constantArrays = $iterateeType->getConstantArrays(); if ( $stmt->getDocComment() === null - && $iterateeType instanceof ConstantArrayType + && $iterateeType->isConstantArray()->yes() + && count($constantArrays) === 1 && $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name) && $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name) ) { - $conditionalHolders = []; - foreach ($iterateeType->getKeyTypes() as $i => $keyType) { - $valueType = $iterateeType->getValueTypes()[$i]; + $valueConditionalHolders = []; + $arrayDimFetchConditionalHolders = []; + foreach ($constantArrays[0]->getKeyTypes() as $i => $keyType) { + $valueType = $constantArrays[0]->getValueTypes()[$i]; $holder = new ConditionalExpressionHolder([ - '$' . $stmt->keyVar->name => $keyType, - ], new VariableTypeHolder($valueType, TrinaryLogic::createYes())); - $conditionalHolders[$holder->getKey()] = $holder; + '$' . $stmt->keyVar->name => ExpressionTypeHolder::createYes(new Variable($stmt->keyVar->name), $keyType), + ], new ExpressionTypeHolder($stmt->valueVar, $valueType, TrinaryLogic::createYes())); + $valueConditionalHolders[$holder->getKey()] = $holder; + $arrayDimFetchHolder = new ConditionalExpressionHolder([ + '$' . $stmt->keyVar->name => ExpressionTypeHolder::createYes(new Variable($stmt->keyVar->name), $keyType), + ], new ExpressionTypeHolder(new ArrayDimFetch($stmt->expr, $stmt->keyVar), $valueType, TrinaryLogic::createYes())); + $arrayDimFetchConditionalHolders[$arrayDimFetchHolder->getKey()] = $arrayDimFetchHolder; } $scope = $scope->addConditionalExpressions( '$' . $stmt->valueVar->name, - $conditionalHolders, + $valueConditionalHolders, ); + if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { + $scope = $scope->addConditionalExpressions( + sprintf('$%s[$%s]', $stmt->expr->name, $stmt->keyVar->name), + $arrayDimFetchConditionalHolders, + ); + } } return $this->processVarAnnotation($scope, $vars, $stmt); @@ -3930,13 +5330,25 @@ private function processTraitUse(Node\Stmt\TraitUse $node, MutatingScope $classS if (!isset($this->analysedFiles[$fileName])) { continue; } + $adaptations = []; + foreach ($node->adaptations as $adaptation) { + if ($adaptation->trait === null) { + $adaptations[] = $adaptation; + continue; + } + if ($adaptation->trait->toLowerString() !== $trait->toLowerString()) { + continue; + } + + $adaptations[] = $adaptation; + } $parserNodes = $this->parser->parseFile($fileName); - $this->processNodesForTraitUse($parserNodes, $traitReflection, $classScope, $node->adaptations, $nodeCallback); + $this->processNodesForTraitUse($parserNodes, $traitReflection, $classScope, $adaptations, $nodeCallback); } } /** - * @param Node[]|Node|scalar $node + * @param Node[]|Node|scalar|null $node * @param Node\Stmt\TraitUseAdaptation[] $adaptations * @param callable(Node $node, Scope $scope): void $nodeCallback */ @@ -3945,16 +5357,22 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection if ($node instanceof Node) { if ($node instanceof Node\Stmt\Trait_ && $traitReflection->getName() === (string) $node->namespacedName && $traitReflection->getNativeReflection()->getStartLine() === $node->getStartLine()) { $methodModifiers = []; + $methodNames = []; foreach ($adaptations as $adaptation) { if (!$adaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) { continue; } - if ($adaptation->newModifier === null) { + $methodName = $adaptation->method->toLowerString(); + if ($adaptation->newModifier !== null) { + $methodModifiers[$methodName] = $adaptation->newModifier; + } + + if ($adaptation->newName === null) { continue; } - $methodModifiers[$adaptation->method->toLowerString()] = $adaptation->newModifier; + $methodNames[$methodName] = $adaptation->newName; } $stmts = $node->stmts; @@ -3963,15 +5381,23 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection continue; } $methodName = $stmt->name->toLowerString(); - if (!array_key_exists($methodName, $methodModifiers)) { + $methodAst = clone $stmt; + $stmts[$i] = $methodAst; + if (array_key_exists($methodName, $methodModifiers)) { + $methodAst->flags = ($methodAst->flags & ~ Node\Stmt\Class_::VISIBILITY_MODIFIER_MASK) | $methodModifiers[$methodName]; + } + + if (!array_key_exists($methodName, $methodNames)) { continue; } - $methodAst = clone $stmt; - $methodAst->flags = ($methodAst->flags & ~ Node\Stmt\Class_::VISIBILITY_MODIFIER_MASK) | $methodModifiers[$methodName]; - $stmts[$i] = $methodAst; + $methodAst->setAttribute('originalTraitMethodName', $methodAst->name->toLowerString()); + $methodAst->name = $methodNames[$methodName]; } - $this->processStmtNodes($node, $stmts, $scope->enterTrait($traitReflection), $nodeCallback); + + $traitScope = $scope->enterTrait($traitReflection); + $nodeCallback(new InTraitNode($node, $traitReflection), $traitScope); + $this->processStmtNodes($node, $stmts, $traitScope, $nodeCallback, StatementContext::createTopLevel()); return; } if ($node instanceof Node\Stmt\ClassLike) { @@ -3991,22 +5417,164 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection } } + private function processCalledMethod(MethodReflection $methodReflection): ?MutatingScope + { + $declaringClass = $methodReflection->getDeclaringClass(); + if ($declaringClass->isAnonymous()) { + return null; + } + if ($declaringClass->getFileName() === null) { + return null; + } + + $stackName = sprintf('%s::%s', $declaringClass->getName(), $methodReflection->getName()); + if (array_key_exists($stackName, $this->calledMethodResults)) { + return $this->calledMethodResults[$stackName]; + } + + if (array_key_exists($stackName, $this->calledMethodStack)) { + return null; + } + + if (count($this->calledMethodStack) > 0) { + return null; + } + + $this->calledMethodStack[$stackName] = true; + + $fileName = $this->fileHelper->normalizePath($declaringClass->getFileName()); + if (!isset($this->analysedFiles[$fileName])) { + return null; + } + $parserNodes = $this->parser->parseFile($fileName); + + $returnStatement = null; + $this->processNodesForCalledMethod($parserNodes, $fileName, $methodReflection, static function (Node $node, Scope $scope) use ($methodReflection, &$returnStatement): void { + if (!$node instanceof MethodReturnStatementsNode) { + return; + } + + if ($node->getClassReflection()->getName() !== $methodReflection->getDeclaringClass()->getName()) { + return; + } + + if ($returnStatement !== null) { + return; + } + + $returnStatement = $node; + }); + + $calledMethodEndScope = null; + if ($returnStatement !== null) { + foreach ($returnStatement->getExecutionEnds() as $executionEnd) { + $statementResult = $executionEnd->getStatementResult(); + $endNode = $executionEnd->getNode(); + if ($endNode instanceof Node\Stmt\Throw_) { + continue; + } + if ($endNode instanceof Node\Stmt\Expression) { + $exprType = $statementResult->getScope()->getType($endNode->expr); + if ($exprType instanceof NeverType && $exprType->isExplicit()) { + continue; + } + } + if ($calledMethodEndScope === null) { + $calledMethodEndScope = $statementResult->getScope(); + continue; + } + + $calledMethodEndScope = $calledMethodEndScope->mergeWith($statementResult->getScope()); + } + foreach ($returnStatement->getReturnStatements() as $statement) { + if ($calledMethodEndScope === null) { + $calledMethodEndScope = $statement->getScope(); + continue; + } + + $calledMethodEndScope = $calledMethodEndScope->mergeWith($statement->getScope()); + } + } + + unset($this->calledMethodStack[$stackName]); + + $this->calledMethodResults[$stackName] = $calledMethodEndScope; + + return $calledMethodEndScope; + } + + /** + * @param Node[]|Node|scalar|null $node + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processNodesForCalledMethod($node, string $fileName, MethodReflection $methodReflection, callable $nodeCallback): void + { + if ($node instanceof Node) { + $declaringClass = $methodReflection->getDeclaringClass(); + if ( + $node instanceof Node\Stmt\Class_ + && $node->namespacedName !== null + && $declaringClass->getName() === (string) $node->namespacedName + && $declaringClass->getNativeReflection()->getStartLine() === $node->getStartLine() + ) { + + $stmts = $node->stmts; + foreach ($stmts as $stmt) { + if (!$stmt instanceof Node\Stmt\ClassMethod) { + continue; + } + + if ($stmt->name->toString() !== $methodReflection->getName()) { + continue; + } + + if ($stmt->getEndLine() - $stmt->getStartLine() > 50) { + continue; + } + + $scope = $this->scopeFactory->create(ScopeContext::create($fileName))->enterClass($declaringClass); + $this->processStmtNode($stmt, $scope, $nodeCallback, StatementContext::createTopLevel()); + } + return; + } + if ($node instanceof Node\Stmt\ClassLike) { + return; + } + if ($node instanceof Node\FunctionLike) { + return; + } + foreach ($node->getSubNodeNames() as $subNodeName) { + $subNode = $node->{$subNodeName}; + $this->processNodesForCalledMethod($subNode, $fileName, $methodReflection, $nodeCallback); + } + } elseif (is_array($node)) { + foreach ($node as $subNode) { + $this->processNodesForCalledMethod($subNode, $fileName, $methodReflection, $nodeCallback); + } + } + } + /** - * @return array{TemplateTypeMap, Type[], ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, array<(string|int), VarTag>} + * @return array{TemplateTypeMap, array, array, array, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array, array<(string|int), VarTag>, bool} */ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array { $templateTypeMap = TemplateTypeMap::createEmpty(); $phpDocParameterTypes = []; + $phpDocImmediatelyInvokedCallableParameters = []; + $phpDocClosureThisTypeParameters = []; $phpDocReturnType = null; $phpDocThrowType = null; $deprecatedDescription = null; $isDeprecated = false; $isInternal = false; $isFinal = false; - $isPure = false; + $isPure = null; + $isAllowedPrivateMutation = false; $acceptsNamedArguments = true; $isReadOnly = $scope->isInClass() && $scope->getClassReflection()->isImmutable(); + $asserts = Assertions::createEmpty(); + $selfOutType = null; $docComment = $node->getDocComment() !== null ? $node->getDocComment()->getText() : null; @@ -4016,6 +5584,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n $trait = $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null; $resolvedPhpDoc = null; $functionName = null; + $phpDocParameterOutTypes = []; if ($node instanceof Node\Stmt\ClassMethod) { if (!$scope->isInClass()) { @@ -4091,6 +5660,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n $varTags = []; if ($resolvedPhpDoc !== null) { $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); + $phpDocImmediatelyInvokedCallableParameters = $resolvedPhpDoc->getParamsImmediatelyInvokedCallable(); foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) { if (array_key_exists($paramName, $phpDocParameterTypes)) { continue; @@ -4101,6 +5671,20 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n } $phpDocParameterTypes[$paramName] = $paramType; } + foreach ($resolvedPhpDoc->getParamClosureThisTags() as $paramName => $paramClosureThisTag) { + if (array_key_exists($paramName, $phpDocClosureThisTypeParameters)) { + continue; + } + $paramClosureThisType = $paramClosureThisTag->getType(); + if ($scope->isInClass()) { + $paramClosureThisType = $this->transformStaticType($scope->getClassReflection(), $paramClosureThisType); + } + $phpDocClosureThisTypeParameters[$paramName] = $paramClosureThisType; + } + + foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) { + $phpDocParameterOutTypes[$paramName] = $paramOutTag->getType(); + } if ($node instanceof Node\FunctionLike) { $nativeReturnType = $scope->getFunctionType($node->getReturnType(), false, false); $phpDocReturnType = $this->getPhpDocReturnType($resolvedPhpDoc, $nativeReturnType); @@ -4114,12 +5698,15 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n $isInternal = $resolvedPhpDoc->isInternal(); $isFinal = $resolvedPhpDoc->isFinal(); $isPure = $resolvedPhpDoc->isPure(); + $isAllowedPrivateMutation = $resolvedPhpDoc->isAllowedPrivateMutation(); $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments(); $isReadOnly = $isReadOnly || $resolvedPhpDoc->isReadOnly(); + $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); + $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null; $varTags = $resolvedPhpDoc->getVarTags(); } - return [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $varTags]; + return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation]; } private function transformStaticType(ClassReflection $declaringClass, Type $type): Type @@ -4158,4 +5745,23 @@ private function getPhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDoc, Type $ return null; } + /** + * @template T of Node + * @param array $nodes + * @return T|null + */ + private function getFirstUnreachableNode(array $nodes, bool $earlyBinding): ?Node + { + foreach ($nodes as $node) { + if ($node instanceof Node\Stmt\Nop) { + continue; + } + if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike)) { + continue; + } + return $node; + } + return null; + } + } diff --git a/src/Analyser/ProcessClosureResult.php b/src/Analyser/ProcessClosureResult.php new file mode 100644 index 0000000000..7423ac220d --- /dev/null +++ b/src/Analyser/ProcessClosureResult.php @@ -0,0 +1,53 @@ +scope; + } + + /** + * @return ThrowPoint[] + */ + public function getThrowPoints(): array + { + return $this->throwPoints; + } + + /** + * @return ImpurePoint[] + */ + public function getImpurePoints(): array + { + return $this->impurePoints; + } + + /** + * @return InvalidateExprNode[] + */ + public function getInvalidateExpressions(): array + { + return $this->invalidateExpressions; + } + +} diff --git a/src/Analyser/ResultCache/ResultCache.php b/src/Analyser/ResultCache/ResultCache.php index 401a0e84d6..8b592f68fe 100644 --- a/src/Analyser/ResultCache/ResultCache.php +++ b/src/Analyser/ResultCache/ResultCache.php @@ -3,19 +3,27 @@ namespace PHPStan\Analyser\ResultCache; use PHPStan\Analyser\Error; +use PHPStan\Analyser\FileAnalyserResult; use PHPStan\Collectors\CollectedData; use PHPStan\Dependency\RootExportedNode; +/** + * @phpstan-import-type LinesToIgnore from FileAnalyserResult + */ class ResultCache { /** * @param string[] $filesToAnalyse * @param mixed[] $meta - * @param array> $errors + * @param array> $errors + * @param array> $locallyIgnoredErrors + * @param array $linesToIgnore + * @param array $unmatchedLineIgnores * @param array> $collectedData * @param array> $dependencies * @param array> $exportedNodes + * @param array $projectExtensionFiles */ public function __construct( private array $filesToAnalyse, @@ -23,9 +31,13 @@ public function __construct( private int $lastFullAnalysisTime, private array $meta, private array $errors, + private array $locallyIgnoredErrors, + private array $linesToIgnore, + private array $unmatchedLineIgnores, private array $collectedData, private array $dependencies, private array $exportedNodes, + private array $projectExtensionFiles, ) { } @@ -57,13 +69,37 @@ public function getMeta(): array } /** - * @return array> + * @return array> */ public function getErrors(): array { return $this->errors; } + /** + * @return array> + */ + public function getLocallyIgnoredErrors(): array + { + return $this->locallyIgnoredErrors; + } + + /** + * @return array + */ + public function getLinesToIgnore(): array + { + return $this->linesToIgnore; + } + + /** + * @return array + */ + public function getUnmatchedLineIgnores(): array + { + return $this->unmatchedLineIgnores; + } + /** * @return array> */ @@ -88,4 +124,12 @@ public function getExportedNodes(): array return $this->exportedNodes; } + /** + * @return array + */ + public function getProjectExtensionFiles(): array + { + return $this->projectExtensionFiles; + } + } diff --git a/src/Analyser/ResultCache/ResultCacheClearer.php b/src/Analyser/ResultCache/ResultCacheClearer.php index d511a3fa92..d9cbee81c6 100644 --- a/src/Analyser/ResultCache/ResultCacheClearer.php +++ b/src/Analyser/ResultCache/ResultCacheClearer.php @@ -2,7 +2,6 @@ namespace PHPStan\Analyser\ResultCache; -use Symfony\Component\Finder\Finder; use function dirname; use function is_file; use function unlink; @@ -10,7 +9,7 @@ class ResultCacheClearer { - public function __construct(private string $cacheFilePath, private string $tempResultCachePath) + public function __construct(private string $cacheFilePath) { } @@ -26,12 +25,4 @@ public function clear(): string return $dir; } - public function clearTemporaryCaches(): void - { - $finder = new Finder(); - foreach ($finder->files()->name('*.php')->in($this->tempResultCachePath) as $tmpResultCacheFile) { - @unlink($tmpResultCacheFile->getPathname()); - } - } - } diff --git a/src/Analyser/ResultCache/ResultCacheManager.php b/src/Analyser/ResultCache/ResultCacheManager.php index 2df66e3210..0ebf90727c 100644 --- a/src/Analyser/ResultCache/ResultCacheManager.php +++ b/src/Analyser/ResultCache/ResultCacheManager.php @@ -2,16 +2,18 @@ namespace PHPStan\Analyser\ResultCache; -use Nette\DI\Definitions\Statement; use Nette\Neon\Neon; use PHPStan\Analyser\AnalyserResult; use PHPStan\Analyser\Error; +use PHPStan\Analyser\FileAnalyserResult; use PHPStan\Collectors\CollectedData; use PHPStan\Command\Output; use PHPStan\Dependency\ExportedNodeFetcher; use PHPStan\Dependency\RootExportedNode; +use PHPStan\DependencyInjection\ProjectConfigHelper; +use PHPStan\File\CouldNotReadFileException; use PHPStan\File\FileFinder; -use PHPStan\File\FileReader; +use PHPStan\File\FileHelper; use PHPStan\File\FileWriter; use PHPStan\Internal\ComposerHelper; use PHPStan\PhpDoc\StubFilesProvider; @@ -23,28 +25,30 @@ use function array_filter; use function array_key_exists; use function array_keys; -use function array_merge; use function array_unique; use function array_values; use function count; use function get_loaded_extensions; +use function implode; use function is_array; use function is_file; -use function is_string; use function ksort; -use function sha1; +use function sha1_file; use function sort; use function sprintf; -use function str_replace; +use function str_starts_with; use function time; use function unlink; use function var_export; use const PHP_VERSION_ID; +/** + * @phpstan-import-type LinesToIgnore from FileAnalyserResult + */ class ResultCacheManager { - private const CACHE_VERSION = 'v10-collectedData'; + private const CACHE_VERSION = 'v12-linesToIgnore'; /** @var array */ private array $fileHashes = []; @@ -58,15 +62,14 @@ class ResultCacheManager * @param string[] $bootstrapFiles * @param string[] $scanFiles * @param string[] $scanDirectories - * @param array $fileReplacements */ public function __construct( private ExportedNodeFetcher $exportedNodeFetcher, private FileFinder $scanFileFinder, private ReflectionProvider $reflectionProvider, private StubFilesProvider $stubFilesProvider, + private FileHelper $fileHelper, private string $cacheFilePath, - private string $tempResultCachePath, private array $analysedPaths, private array $composerAutoloaderProjectPaths, private string $usedLevel, @@ -74,7 +77,6 @@ public function __construct( private array $bootstrapFiles, private array $scanFiles, private array $scanDirectories, - private array $fileReplacements, private bool $checkDependenciesOfProjectExtensionFiles, ) { @@ -84,34 +86,27 @@ public function __construct( * @param string[] $allAnalysedFiles * @param mixed[]|null $projectConfigArray */ - public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?array $projectConfigArray, Output $output, ?string $resultCacheName = null): ResultCache + public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?array $projectConfigArray, Output $output): ResultCache { if ($debug) { if ($output->isDebug()) { $output->writeLineFormatted('Result cache not used because of debug mode.'); } - return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], []); } if ($onlyFiles) { if ($output->isDebug()) { $output->writeLineFormatted('Result cache not used because only files were passed as analysed paths.'); } - return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], []); } $cacheFilePath = $this->cacheFilePath; - if ($resultCacheName !== null) { - $tmpCacheFile = $this->tempResultCachePath . '/' . $resultCacheName . '.php'; - if (is_file($tmpCacheFile)) { - $cacheFilePath = $tmpCacheFile; - } - } - if (!is_file($cacheFilePath)) { if ($output->isDebug()) { $output->writeLineFormatted('Result cache not used because the cache file does not exist.'); } - return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], []); } try { @@ -123,7 +118,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? @unlink($cacheFilePath); - return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], []); } if (!is_array($data)) { @@ -132,15 +127,16 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? $output->writeLineFormatted('Result cache not used because the cache file is corrupted.'); } - return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $this->getMeta($allAnalysedFiles, $projectConfigArray), [], [], [], [], [], [], [], []); } $meta = $this->getMeta($allAnalysedFiles, $projectConfigArray); if ($this->isMetaDifferent($data['meta'], $meta)) { if ($output->isDebug()) { - $output->writeLineFormatted('Result cache not used because the metadata do not match.'); + $diffs = $this->getMetaKeyDifferences($data['meta'], $meta); + $output->writeLineFormatted('Result cache not used because the metadata do not match: ' . implode(', ', $diffs)); } - return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], [], [], [], []); } if (time() - $data['lastFullAnalysisTime'] >= 60 * 60 * 24 * 7) { @@ -148,15 +144,22 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? $output->writeLineFormatted('Result cache not used because it\'s more than 7 days since last full analysis.'); } // run full analysis if the result cache is older than 7 days - return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], [], [], [], []); } - foreach ($data['projectExtensionFiles'] as $extensionFile => $fileHash) { + /** + * @var string $fileHash + * @var bool $isAnalysed + */ + foreach ($data['projectExtensionFiles'] as $extensionFile => [$fileHash, $isAnalysed]) { + if (!$isAnalysed) { + continue; + } if (!is_file($extensionFile)) { if ($output->isDebug()) { $output->writeLineFormatted(sprintf('Result cache not used because extension file %s was not found.', $extensionFile)); } - return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], [], [], [], []); } if ($this->getFileHash($extensionFile) === $fileHash) { @@ -167,7 +170,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? $output->writeLineFormatted(sprintf('Result cache not used because extension file %s hash does not match.', $extensionFile)); } - return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], []); + return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], [], [], [], [], [], []); } $invertedDependencies = $data['dependencies']; @@ -175,9 +178,15 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? $filesToAnalyse = []; $invertedDependenciesToReturn = []; $errors = $data['errorsCallback'](); + $locallyIgnoredErrors = $data['locallyIgnoredErrorsCallback'](); + $linesToIgnore = $data['linesToIgnore']; + $unmatchedLineIgnores = $data['unmatchedLineIgnores']; $collectedData = $data['collectedDataCallback'](); $exportedNodes = $data['exportedNodesCallback'](); $filteredErrors = []; + $filteredLocallyIgnoredErrors = []; + $filteredLinesToIgnore = []; + $filteredUnmatchedLineIgnores = []; $filteredCollectedData = []; $filteredExportedNodes = []; $newFileAppeared = false; @@ -194,6 +203,15 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? if (array_key_exists($analysedFile, $errors)) { $filteredErrors[$analysedFile] = $errors[$analysedFile]; } + if (array_key_exists($analysedFile, $locallyIgnoredErrors)) { + $filteredLocallyIgnoredErrors[$analysedFile] = $locallyIgnoredErrors[$analysedFile]; + } + if (array_key_exists($analysedFile, $linesToIgnore)) { + $filteredLinesToIgnore[$analysedFile] = $linesToIgnore[$analysedFile]; + } + if (array_key_exists($analysedFile, $unmatchedLineIgnores)) { + $filteredUnmatchedLineIgnores[$analysedFile] = $unmatchedLineIgnores[$analysedFile]; + } if (array_key_exists($analysedFile, $collectedData)) { $filteredCollectedData[$analysedFile] = $collectedData[$analysedFile]; } @@ -263,7 +281,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? } } - return new ResultCache(array_unique($filesToAnalyse), false, $data['lastFullAnalysisTime'], $meta, $filteredErrors, $filteredCollectedData, $invertedDependenciesToReturn, $filteredExportedNodes); + return new ResultCache(array_unique($filesToAnalyse), false, $data['lastFullAnalysisTime'], $meta, $filteredErrors, $filteredLocallyIgnoredErrors, $filteredLinesToIgnore, $filteredUnmatchedLineIgnores, $filteredCollectedData, $invertedDependenciesToReturn, $filteredExportedNodes, $data['projectExtensionFiles']); } /** @@ -274,21 +292,51 @@ private function isMetaDifferent(array $cachedMeta, array $currentMeta): bool { $projectConfig = $currentMeta['projectConfig']; if ($projectConfig !== null) { + ksort($currentMeta['projectConfig']); + $currentMeta['projectConfig'] = Neon::encode($currentMeta['projectConfig']); } return $cachedMeta !== $currentMeta; } + /** + * @param mixed[] $cachedMeta + * @param mixed[] $currentMeta + * + * @return string[] + */ + private function getMetaKeyDifferences(array $cachedMeta, array $currentMeta): array + { + $diffs = []; + foreach ($cachedMeta as $key => $value) { + if (!array_key_exists($key, $currentMeta)) { + $diffs[] = $key; + continue; + } + + if ($value === $currentMeta[$key]) { + continue; + } + + $diffs[] = $key; + } + + if ($diffs === []) { + // when none of the keys is different, + // the order of the keys is the problem + $diffs[] = 'keyOrder'; + } + + return $diffs; + } + /** * @param array $cachedFileExportedNodes * @return bool|null null means nothing changed, true means new root symbol appeared, false means nested node changed */ private function exportedNodesChanged(string $analysedFile, array $cachedFileExportedNodes): ?bool { - if (array_key_exists($analysedFile, $this->fileReplacements)) { - $analysedFile = $this->fileReplacements[$analysedFile]; - } $fileExportedNodes = $this->exportedNodeFetcher->fetchNodes($analysedFile); $cachedSymbols = []; @@ -319,10 +367,7 @@ private function exportedNodesChanged(string $analysedFile, array $cachedFileExp return null; } - /** - * @param bool|string $save - */ - public function process(AnalyserResult $analyserResult, ResultCache $resultCache, Output $output, bool $onlyFiles, $save): ResultCacheProcessResult + public function process(AnalyserResult $analyserResult, ResultCache $resultCache, Output $output, bool $onlyFiles, bool $save): ResultCacheProcessResult { $internalErrors = $analyserResult->getInternalErrors(); $freshErrorsByFile = []; @@ -330,13 +375,22 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache $freshErrorsByFile[$error->getFilePath()][] = $error; } + $freshLocallyIgnoredErrorsByFile = []; + foreach ($analyserResult->getLocallyIgnoredErrors() as $error) { + $freshLocallyIgnoredErrorsByFile[$error->getFilePath()][] = $error; + } + $freshCollectedDataByFile = []; foreach ($analyserResult->getCollectedData() as $collectedData) { $freshCollectedDataByFile[$collectedData->getFilePath()][] = $collectedData; } $meta = $resultCache->getMeta(); - $doSave = function (array $errorsByFile, $collectedDataByFile, ?array $dependencies, array $exportedNodes, ?string $resultCacheName) use ($internalErrors, $resultCache, $output, $onlyFiles, $meta): bool { + $projectConfigArray = $meta['projectConfig']; + if ($projectConfigArray !== null) { + $meta['projectConfig'] = Neon::encode($projectConfigArray); + } + $doSave = function (array $errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, ?array $dependencies, array $exportedNodes, array $projectExtensionFiles) use ($internalErrors, $resultCache, $output, $onlyFiles, $meta): bool { if ($onlyFiles) { if ($output->isDebug()) { $output->writeLineFormatted('Result cache was not saved because only files were passed as analysed paths.'); @@ -371,7 +425,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache } } - $this->save($resultCache->getLastFullAnalysisTime(), $resultCacheName, $errorsByFile, $collectedDataByFile, $dependencies, $exportedNodes, $meta); + $this->save($resultCache->getLastFullAnalysisTime(), $errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, $dependencies, $exportedNodes, $projectExtensionFiles, $meta); if ($output->isDebug()) { $output->writeLineFormatted('Result cache is saved.'); @@ -383,7 +437,11 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache if ($resultCache->isFullAnalysis()) { $saved = false; if ($save !== false) { - $saved = $doSave($freshErrorsByFile, $freshCollectedDataByFile, $analyserResult->getDependencies(), $analyserResult->getExportedNodes(), is_string($save) ? $save : null); + $projectExtensionFiles = []; + if ($analyserResult->getDependencies() !== null) { + $projectExtensionFiles = $this->getProjectExtensionFiles($projectConfigArray, $analyserResult->getDependencies()); + } + $saved = $doSave($freshErrorsByFile, $freshLocallyIgnoredErrorsByFile, $analyserResult->getLinesToIgnore(), $analyserResult->getUnmatchedLineIgnores(), $freshCollectedDataByFile, $analyserResult->getDependencies(), $analyserResult->getExportedNodes(), $projectExtensionFiles); } else { if ($output->isDebug()) { $output->writeLineFormatted('Result cache was not saved because it was not requested.'); @@ -394,13 +452,36 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache } $errorsByFile = $this->mergeErrors($resultCache, $freshErrorsByFile); + $locallyIgnoredErrorsByFile = $this->mergeLocallyIgnoredErrors($resultCache, $freshLocallyIgnoredErrorsByFile); $collectedDataByFile = $this->mergeCollectedData($resultCache, $freshCollectedDataByFile); $dependencies = $this->mergeDependencies($resultCache, $analyserResult->getDependencies()); $exportedNodes = $this->mergeExportedNodes($resultCache, $analyserResult->getExportedNodes()); + $linesToIgnore = $this->mergeLinesToIgnore($resultCache, $analyserResult->getLinesToIgnore()); + $unmatchedLineIgnores = $this->mergeUnmatchedLineIgnores($resultCache, $analyserResult->getUnmatchedLineIgnores()); $saved = false; if ($save !== false) { - $saved = $doSave($errorsByFile, $collectedDataByFile, $dependencies, $exportedNodes, is_string($save) ? $save : null); + $projectExtensionFiles = []; + foreach ($resultCache->getProjectExtensionFiles() as $file => [$hash, $isAnalysed, $className]) { + if ($isAnalysed) { + continue; + } + + // keep the same file hashes from the old run + // so that the message "When you edit them and re-run PHPStan, the result cache will get stale." + // keeps being shown on subsequent runs + $projectExtensionFiles[$file] = [$hash, false, $className]; + } + if ($dependencies !== null) { + foreach ($this->getProjectExtensionFiles($projectConfigArray, $dependencies) as $file => [$hash, $isAnalysed, $className]) { + if (!$isAnalysed) { + continue; + } + + $projectExtensionFiles[$file] = [$hash, true, $className]; + } + } + $saved = $doSave($errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, $dependencies, $exportedNodes, $projectExtensionFiles); } $flatErrors = []; @@ -410,6 +491,13 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache } } + $flatLocallyIgnoredErrors = []; + foreach ($locallyIgnoredErrorsByFile as $fileErrors) { + foreach ($fileErrors as $fileError) { + $flatLocallyIgnoredErrors[] = $fileError; + } + } + $flatCollectedData = []; foreach ($collectedDataByFile as $fileCollectedData) { foreach ($fileCollectedData as $collectedData) { @@ -419,17 +507,23 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache return new ResultCacheProcessResult(new AnalyserResult( $flatErrors, + $analyserResult->getFilteredPhpErrors(), + $analyserResult->getAllPhpErrors(), + $flatLocallyIgnoredErrors, + $linesToIgnore, + $unmatchedLineIgnores, $internalErrors, $flatCollectedData, $dependencies, $exportedNodes, $analyserResult->hasReachedInternalErrorsCountLimit(), + $analyserResult->getPeakMemoryUsageBytes(), ), $saved); } /** - * @param array> $freshErrorsByFile - * @return array> + * @param array> $freshErrorsByFile + * @return array> */ private function mergeErrors(ResultCache $resultCache, array $freshErrorsByFile): array { @@ -445,6 +539,24 @@ private function mergeErrors(ResultCache $resultCache, array $freshErrorsByFile) return $errorsByFile; } + /** + * @param array> $freshLocallyIgnoredErrorsByFile + * @return array> + */ + private function mergeLocallyIgnoredErrors(ResultCache $resultCache, array $freshLocallyIgnoredErrorsByFile): array + { + $errorsByFile = $resultCache->getLocallyIgnoredErrors(); + foreach ($resultCache->getFilesToAnalyse() as $file) { + if (!array_key_exists($file, $freshLocallyIgnoredErrorsByFile)) { + unset($errorsByFile[$file]); + continue; + } + $errorsByFile[$file] = $freshLocallyIgnoredErrorsByFile[$file]; + } + + return $errorsByFile; + } + /** * @param array> $freshCollectedDataByFile * @return array> @@ -524,19 +636,64 @@ private function mergeExportedNodes(ResultCache $resultCache, array $freshExport } /** - * @param array> $errors + * @param array $freshLinesToIgnore + * @return array + */ + private function mergeLinesToIgnore(ResultCache $resultCache, array $freshLinesToIgnore): array + { + $newLinesToIgnore = $resultCache->getLinesToIgnore(); + foreach ($resultCache->getFilesToAnalyse() as $file) { + if (!array_key_exists($file, $freshLinesToIgnore)) { + unset($newLinesToIgnore[$file]); + continue; + } + + $newLinesToIgnore[$file] = $freshLinesToIgnore[$file]; + } + + return $newLinesToIgnore; + } + + /** + * @param array $freshUnmatchedLineIgnores + * @return array + */ + private function mergeUnmatchedLineIgnores(ResultCache $resultCache, array $freshUnmatchedLineIgnores): array + { + $newUnmatchedLineIgnores = $resultCache->getUnmatchedLineIgnores(); + foreach ($resultCache->getFilesToAnalyse() as $file) { + if (!array_key_exists($file, $freshUnmatchedLineIgnores)) { + unset($newUnmatchedLineIgnores[$file]); + continue; + } + + $newUnmatchedLineIgnores[$file] = $freshUnmatchedLineIgnores[$file]; + } + + return $newUnmatchedLineIgnores; + } + + /** + * @param array> $errors + * @param array> $locallyIgnoredErrors + * @param array $linesToIgnore + * @param array $unmatchedLineIgnores * @param array> $collectedData * @param array> $dependencies * @param array> $exportedNodes + * @param array $projectExtensionFiles * @param mixed[] $meta */ private function save( int $lastFullAnalysisTime, - ?string $resultCacheName, array $errors, + array $locallyIgnoredErrors, + array $linesToIgnore, + array $unmatchedLineIgnores, array $collectedData, array $dependencies, array $exportedNodes, + array $projectExtensionFiles, array $meta, ): void { @@ -571,6 +728,9 @@ private function save( } ksort($errors); + ksort($locallyIgnoredErrors); + ksort($linesToIgnore); + ksort($unmatchedLineIgnores); ksort($collectedData); ksort($invertedDependencies); @@ -580,118 +740,89 @@ private function save( $invertedDependencies[$file]['dependentFiles'] = $dependentFiles; } - $template = " %s, - 'meta' => %s, - 'projectExtensionFiles' => %s, - 'errorsCallback' => static function (): array { return %s; }, - 'collectedDataCallback' => static function (): array { return %s; }, - 'dependencies' => %s, - 'exportedNodesCallback' => static function (): array { return %s; }, -]; -"; - ksort($exportedNodes); $file = $this->cacheFilePath; - if ($resultCacheName !== null) { - $file = $this->tempResultCachePath . '/' . $resultCacheName . '.php'; - } - - $projectConfigArray = $meta['projectConfig']; - if ($projectConfigArray !== null) { - $meta['projectConfig'] = Neon::encode($projectConfigArray); - } FileWriter::write( $file, - sprintf( - $template, - var_export($lastFullAnalysisTime, true), - var_export($meta, true), - var_export($this->getProjectExtensionFiles($projectConfigArray, $dependencies), true), - var_export($errors, true), - var_export($collectedData, true), - var_export($invertedDependencies, true), - var_export($exportedNodes, true), - ), + " " . var_export($lastFullAnalysisTime, true) . ", + 'meta' => " . var_export($meta, true) . ", + 'projectExtensionFiles' => " . var_export($projectExtensionFiles, true) . ", + 'errorsCallback' => static function (): array { return " . var_export($errors, true) . "; }, + 'locallyIgnoredErrorsCallback' => static function (): array { return " . var_export($locallyIgnoredErrors, true) . "; }, + 'linesToIgnore' => " . var_export($linesToIgnore, true) . ", + 'unmatchedLineIgnores' => " . var_export($unmatchedLineIgnores, true) . ", + 'collectedDataCallback' => static function (): array { return " . var_export($collectedData, true) . "; }, + 'dependencies' => " . var_export($invertedDependencies, true) . ", + 'exportedNodesCallback' => static function (): array { return " . var_export($exportedNodes, true) . '; }, +]; +', ); } /** * @param mixed[]|null $projectConfig * @param array $dependencies - * @return array + * @return array */ private function getProjectExtensionFiles(?array $projectConfig, array $dependencies): array { $this->alreadyProcessed = []; $projectExtensionFiles = []; if ($projectConfig !== null) { - $services = array_merge( - $projectConfig['services'] ?? [], - $projectConfig['rules'] ?? [], - ); - foreach ($services as $service) { - $classes = $this->getClassesFromConfigDefinition($service); - if (is_array($service)) { - foreach (['class', 'factory', 'implement'] as $key) { - if (!isset($service[$key])) { - continue; - } + $vendorDirs = []; + foreach ($this->composerAutoloaderProjectPaths as $autoloaderProjectPath) { + $composer = ComposerHelper::getComposerConfig($autoloaderProjectPath); + if ($composer === null) { + continue; + } + $vendorDirectory = ComposerHelper::getVendorDirFromComposerConfig($autoloaderProjectPath, $composer); + $vendorDirs[] = $this->fileHelper->normalizePath($vendorDirectory); + } - $classes = array_merge($classes, $this->getClassesFromConfigDefinition($service[$key])); - } + $classes = ProjectConfigHelper::getServiceClassNames($projectConfig); + foreach ($classes as $class) { + if (!$this->reflectionProvider->hasClass($class)) { + continue; } - foreach (array_unique($classes) as $class) { - if (!$this->reflectionProvider->hasClass($class)) { - continue; - } + $classReflection = $this->reflectionProvider->getClass($class); + $fileName = $classReflection->getFileName(); + if ($fileName === null) { + continue; + } - $classReflection = $this->reflectionProvider->getClass($class); - $fileName = $classReflection->getFileName(); - if ($fileName === null) { - continue; - } + if (str_starts_with($fileName, 'phar://')) { + continue; + } - $allServiceFiles = $this->getAllDependencies($fileName, $dependencies); - foreach ($allServiceFiles as $serviceFile) { - if (array_key_exists($serviceFile, $projectExtensionFiles)) { - continue; + $allServiceFiles = $this->getAllDependencies($fileName, $dependencies); + if (count($allServiceFiles) === 0) { + $normalizedFileName = $this->fileHelper->normalizePath($fileName); + foreach ($vendorDirs as $vendorDir) { + if (str_starts_with($normalizedFileName, $vendorDir)) { + continue 2; } - - $projectExtensionFiles[$serviceFile] = $this->getFileHash($serviceFile); } + $projectExtensionFiles[$fileName] = [$this->getFileHash($fileName), false, $class]; + continue; } - } - } - return $projectExtensionFiles; - } - - /** - * @param mixed $definition - * @return string[] - */ - private function getClassesFromConfigDefinition($definition): array - { - if (is_string($definition)) { - return [$definition]; - } + foreach ($allServiceFiles as $serviceFile) { + if (array_key_exists($serviceFile, $projectExtensionFiles)) { + continue; + } - if ($definition instanceof Statement) { - $entity = $definition->entity; - if (is_string($entity)) { - return [$entity]; - } elseif (is_array($entity) && isset($entity[0]) && is_string($entity[0])) { - return [$entity[0]]; + $projectExtensionFiles[$serviceFile] = [$this->getFileHash($serviceFile), true, $class]; + } } } - return []; + return $projectExtensionFiles; } /** @@ -734,13 +865,20 @@ private function getMeta(array $allAnalysedFiles, ?array $projectConfigArray): a sort($extensions); if ($projectConfigArray !== null) { + unset($projectConfigArray['parameters']['editorUrl']); + unset($projectConfigArray['parameters']['editorUrlTitle']); + unset($projectConfigArray['parameters']['errorFormat']); unset($projectConfigArray['parameters']['ignoreErrors']); + unset($projectConfigArray['parameters']['reportUnmatchedIgnoredErrors']); unset($projectConfigArray['parameters']['tipsOfTheDay']); unset($projectConfigArray['parameters']['parallel']); unset($projectConfigArray['parameters']['internalErrorsCountLimit']); unset($projectConfigArray['parameters']['cache']); unset($projectConfigArray['parameters']['memoryLimitFile']); + unset($projectConfigArray['parameters']['pro']); unset($projectConfigArray['parametersSchema']); + + ksort($projectConfigArray); } return [ @@ -761,17 +899,14 @@ private function getMeta(array $allAnalysedFiles, ?array $projectConfigArray): a private function getFileHash(string $path): string { - if (array_key_exists($path, $this->fileReplacements)) { - $path = $this->fileReplacements[$path]; - } if (array_key_exists($path, $this->fileHashes)) { return $this->fileHashes[$path]; } - $contents = FileReader::read($path); - $contents = str_replace("\r\n", "\n", $contents); - - $hash = sha1($contents); + $hash = sha1_file($path); + if ($hash === false) { + throw new CouldNotReadFileException($path); + } $this->fileHashes[$path] = $hash; return $hash; diff --git a/src/Analyser/ResultCache/ResultCacheManagerFactory.php b/src/Analyser/ResultCache/ResultCacheManagerFactory.php index 333bc6136e..269f745015 100644 --- a/src/Analyser/ResultCache/ResultCacheManagerFactory.php +++ b/src/Analyser/ResultCache/ResultCacheManagerFactory.php @@ -5,9 +5,6 @@ interface ResultCacheManagerFactory { - /** - * @param array $fileReplacements - */ - public function create(array $fileReplacements): ResultCacheManager; + public function create(): ResultCacheManager; } diff --git a/src/Analyser/RuleErrorTransformer.php b/src/Analyser/RuleErrorTransformer.php index 468ffc5187..464e8cd776 100644 --- a/src/Analyser/RuleErrorTransformer.php +++ b/src/Analyser/RuleErrorTransformer.php @@ -53,7 +53,7 @@ public function transform( $ruleError instanceof FileRuleError && $ruleError->getFile() !== '' ) { - $fileName = $ruleError->getFile(); + $fileName = $ruleError->getFileDescription(); $filePath = $ruleError->getFile(); $traitFilePath = null; } diff --git a/src/Analyser/Scope.php b/src/Analyser/Scope.php index 9d57b4b453..e26aa8853a 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -9,9 +9,11 @@ use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\NamespaceAnswerer; +use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\PropertyReflection; use PHPStan\TrinaryLogic; @@ -22,18 +24,33 @@ interface Scope extends ClassMemberAccessAnswerer, NamespaceAnswerer { + public const SUPERGLOBAL_VARIABLES = [ + 'GLOBALS', + '_SERVER', + '_GET', + '_POST', + '_FILES', + '_COOKIE', + '_SESSION', + '_REQUEST', + '_ENV', + ]; + public function getFile(): string; public function getFileDescription(): string; public function isDeclareStrictTypes(): bool; + /** + * @phpstan-assert-if-true !null $this->getTraitReflection() + */ public function isInTrait(): bool; public function getTraitReflection(): ?ClassReflection; /** - * @return FunctionReflection|MethodReflection|null + * @return FunctionReflection|ExtendedMethodReflection|null */ public function getFunction(); @@ -56,10 +73,14 @@ public function hasConstant(Name $name): bool; public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?PropertyReflection; - public function getMethodReflection(Type $typeWithMethod, string $methodName): ?MethodReflection; + public function getMethodReflection(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection; public function getConstantReflection(Type $typeWithConstant, string $constantName): ?ConstantReflection; + public function getIterableKeyType(Type $iteratee): Type; + + public function getIterableValueType(Type $iteratee): Type; + public function isInAnonymousFunction(): bool; public function getAnonymousFunctionReflection(): ?ParametersAcceptor; @@ -68,14 +89,13 @@ public function getAnonymousFunctionReturnType(): ?Type; public function getType(Expr $node): Type; - /** - * Gets type of an expression with no regards to phpDocs. - * Works for function/method parameters only. - * - * @internal - */ public function getNativeType(Expr $expr): Type; + public function getKeepVoidType(Expr $node): Type; + + /** + * @deprecated Use getNativeType() + */ public function doNotTreatPhpDocTypesAsCertain(): self; public function resolveName(Name $name): string; @@ -87,14 +107,23 @@ public function resolveTypeByName(Name $name): TypeWithClassName; */ public function getTypeFromValue($value): Type; + /** @deprecated use hasExpressionType instead */ public function isSpecified(Expr $node): bool; + public function hasExpressionType(Expr $node): TrinaryLogic; + public function isInClassExists(string $className): bool; public function isInFunctionExists(string $functionName): bool; public function isInClosureBind(): bool; + /** @return list */ + public function getFunctionCallStack(): array; + + /** @return list */ + public function getFunctionCallStackWithParameters(): array; + public function isParameterValueNullable(Param $parameter): bool; /** diff --git a/src/Analyser/ScopeFactory.php b/src/Analyser/ScopeFactory.php index bfb0912e9a..c2401d375c 100644 --- a/src/Analyser/ScopeFactory.php +++ b/src/Analyser/ScopeFactory.php @@ -2,44 +2,17 @@ namespace PHPStan\Analyser; -use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Type\Type; - -interface ScopeFactory +/** @api */ +class ScopeFactory { - /** - * @api - * @param array $constantTypes - * @param VariableTypeHolder[] $variablesTypes - * @param VariableTypeHolder[] $moreSpecificTypes - * @param array $conditionalExpressions - * @param array $currentlyAssignedExpressions - * @param array $currentlyAllowedUndefinedExpressions - * @param array $nativeExpressionTypes - * @param array $inFunctionCallsStack - * - */ - public function create( - ScopeContext $context, - bool $declareStrictTypes = false, - array $constantTypes = [], - FunctionReflection|MethodReflection|null $function = null, - ?string $namespace = null, - array $variablesTypes = [], - array $moreSpecificTypes = [], - array $conditionalExpressions = [], - ?string $inClosureBindScopeClass = null, - ?ParametersAcceptor $anonymousFunctionReflection = null, - bool $inFirstLevelStatement = true, - array $currentlyAssignedExpressions = [], - array $currentlyAllowedUndefinedExpressions = [], - array $nativeExpressionTypes = [], - array $inFunctionCallsStack = [], - bool $afterExtractCall = false, - ?Scope $parentScope = null, - ): MutatingScope; + public function __construct(private InternalScopeFactory $internalScopeFactory) + { + } + + public function create(ScopeContext $context): MutatingScope + { + return $this->internalScopeFactory->create($context); + } } diff --git a/src/Analyser/StatementContext.php b/src/Analyser/StatementContext.php new file mode 100644 index 0000000000..222d768b52 --- /dev/null +++ b/src/Analyser/StatementContext.php @@ -0,0 +1,38 @@ +isTopLevel; + } + + public function enterDeep(): self + { + if ($this->isTopLevel) { + return self::createDeep(); + } + + return $this; + } + +} diff --git a/src/Analyser/StatementResult.php b/src/Analyser/StatementResult.php index be2e8aef4b..e731931225 100644 --- a/src/Analyser/StatementResult.php +++ b/src/Analyser/StatementResult.php @@ -12,6 +12,7 @@ class StatementResult /** * @param StatementExitPoint[] $exitPoints * @param ThrowPoint[] $throwPoints + * @param ImpurePoint[] $impurePoints */ public function __construct( private MutatingScope $scope, @@ -19,6 +20,7 @@ public function __construct( private bool $isAlwaysTerminating, private array $exitPoints, private array $throwPoints, + private array $impurePoints, ) { } @@ -52,14 +54,14 @@ public function filterOutLoopExitPoints(): self $num = $statement->num; if (!$num instanceof LNumber) { - return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints); + return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints); } if ($num->value !== 1) { continue; } - return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints); + return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints); } return $this; @@ -155,4 +157,12 @@ public function getThrowPoints(): array return $this->throwPoints; } + /** + * @return ImpurePoint[] + */ + public function getImpurePoints(): array + { + return $this->impurePoints; + } + } diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 5439383672..3c90d190b1 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -18,13 +18,18 @@ use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Expr\StaticPropertyFetch; use PhpParser\Node\Name; +use PHPStan\Node\Expr\AlwaysRememberedExpr; +use PHPStan\Node\IssetExpr; use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ResolvedFunctionVariant; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\HasOffsetType; @@ -33,16 +38,18 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\ConditionalTypeForParameter; -use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantScalarType; -use PHPStan\Type\ConstantType; -use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\FloatType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\Generic\TemplateType; +use PHPStan\Type\Generic\TemplateTypeHelper; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; @@ -61,18 +68,17 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; -use function array_filter; use function array_key_exists; +use function array_map; use function array_merge; -use function array_reduce; use function array_reverse; +use function array_shift; use function count; use function in_array; use function is_string; use function strtolower; +use function substr; class TypeSpecifier { @@ -149,7 +155,7 @@ public function specifyTypesInCondition( if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } - if ($type instanceof TypeWithClassName) { + if ($type->getObjectClassNames() !== []) { return $type; } if ($type instanceof GenericClassStringType) { @@ -179,106 +185,7 @@ public function specifyTypesInCondition( return $this->create($exprNode, new ObjectWithoutClassType(), $context, false, $scope, $rootExpr); } } elseif ($expr instanceof Node\Expr\BinaryOp\Identical) { - $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr); - if ($expressions !== null) { - /** @var Expr $exprNode */ - $exprNode = $expressions[0]; - /** @var ConstantScalarType $constantType */ - $constantType = $expressions[1]; - - $specifiedType = $this->specifyTypesForConstantBinaryExpression($exprNode, $constantType, $context, $scope, $rootExpr); - if ($specifiedType !== null) { - return $specifiedType; - } - } - - $rightType = $scope->getType($expr->right); - if ( - $expr->left instanceof ClassConstFetch && - $expr->left->class instanceof Expr && - $expr->left->name instanceof Node\Identifier && - $expr->right instanceof ClassConstFetch && - $rightType instanceof ConstantStringType && - strtolower($expr->left->name->toString()) === 'class' - ) { - return $this->specifyTypesInCondition( - $scope, - new Instanceof_( - $expr->left->class, - new Name($rightType->getValue()), - ), - $context, - $rootExpr, - ); - } - if ($context->false()) { - $identicalType = $scope->getType($expr); - if ($identicalType instanceof ConstantBooleanType) { - $never = new NeverType(); - $contextForTypes = $identicalType->getValue() ? $context->negate() : $context; - $leftTypes = $this->create($expr->left, $never, $contextForTypes, false, $scope, $rootExpr); - $rightTypes = $this->create($expr->right, $never, $contextForTypes, false, $scope, $rootExpr); - return $leftTypes->unionWith($rightTypes); - } - } - - $types = null; - $exprLeftType = $scope->getType($expr->left); - $exprRightType = $scope->getType($expr->right); - if ( - ($exprLeftType instanceof ConstantType && !$exprRightType->equals($exprLeftType) && $exprRightType->isSuperTypeOf($exprLeftType)->yes()) - || $exprLeftType instanceof ConstantScalarType - || $exprLeftType instanceof EnumCaseObjectType - ) { - $types = $this->create( - $expr->right, - $exprLeftType, - $context, - false, - $scope, - $rootExpr, - ); - } - if ( - ($exprRightType instanceof ConstantType && !$exprLeftType->equals($exprRightType) && $exprLeftType->isSuperTypeOf($exprRightType)->yes()) - || $exprRightType instanceof ConstantScalarType - || $exprRightType instanceof EnumCaseObjectType - ) { - $leftType = $this->create( - $expr->left, - $exprRightType, - $context, - false, - $scope, - $rootExpr, - ); - if ($types !== null) { - $types = $types->unionWith($leftType); - } else { - $types = $leftType; - } - } - - if ($types !== null) { - return $types; - } - - $leftExprString = $this->exprPrinter->printExpr($expr->left); - $rightExprString = $this->exprPrinter->printExpr($expr->right); - if ($leftExprString === $rightExprString) { - if (!$expr->left instanceof Expr\Variable || !$expr->right instanceof Expr\Variable) { - return new SpecifiedTypes([], [], false, [], $rootExpr); - } - } - - if ($context->true()) { - $leftTypes = $this->create($expr->left, $exprRightType, $context, false, $scope, $rootExpr); - $rightTypes = $this->create($expr->right, $exprLeftType, $context, false, $scope, $rootExpr); - return $leftTypes->unionWith($rightTypes); - } elseif ($context->false()) { - return $this->create($expr->left, $exprLeftType, $context, false, $scope, $rootExpr)->normalize($scope) - ->intersectWith($this->create($expr->right, $exprRightType, $context, false, $scope, $rootExpr)->normalize($scope)); - } + return $this->resolveIdentical($expr, $scope, $context, $rootExpr); } elseif ($expr instanceof Node\Expr\BinaryOp\NotIdentical) { return $this->specifyTypesInCondition( @@ -288,128 +195,7 @@ public function specifyTypesInCondition( $rootExpr, ); } elseif ($expr instanceof Node\Expr\BinaryOp\Equal) { - $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr); - if ($expressions !== null) { - /** @var Expr $exprNode */ - $exprNode = $expressions[0]; - /** @var ConstantScalarType $constantType */ - $constantType = $expressions[1]; - if (!$context->null() && ($constantType->getValue() === false || $constantType->getValue() === null)) { - return $this->specifyTypesInCondition( - $scope, - $exprNode, - $context->true() ? TypeSpecifierContext::createFalsey() : TypeSpecifierContext::createFalsey()->negate(), - $rootExpr, - ); - } - - if (!$context->null() && $constantType->getValue() === true) { - return $this->specifyTypesInCondition( - $scope, - $exprNode, - $context->true() ? TypeSpecifierContext::createTruthy() : TypeSpecifierContext::createTruthy()->negate(), - $rootExpr, - ); - } - - if ( - $exprNode instanceof FuncCall - && $exprNode->name instanceof Name - && strtolower($exprNode->name->toString()) === 'gettype' - && isset($exprNode->getArgs()[0]) - && $constantType instanceof ConstantStringType - ) { - return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context, $rootExpr); - } - - if ( - $exprNode instanceof FuncCall - && $exprNode->name instanceof Name - && strtolower($exprNode->name->toString()) === 'get_class' - && isset($exprNode->getArgs()[0]) - && $constantType instanceof ConstantStringType - ) { - return $this->specifyTypesInCondition( - $scope, - new Instanceof_( - $exprNode->getArgs()[0]->value, - new Name($constantType->getValue()), - ), - $context, - $rootExpr, - ); - } - } - - $leftType = $scope->getType($expr->left); - $rightType = $scope->getType($expr->right); - - $leftBooleanType = $leftType->toBoolean(); - if ($leftBooleanType instanceof ConstantBooleanType && $rightType instanceof BooleanType) { - return $this->specifyTypesInCondition( - $scope, - new Expr\BinaryOp\Identical( - new ConstFetch(new Name($leftBooleanType->getValue() ? 'true' : 'false')), - $expr->right, - ), - $context, - $rootExpr, - ); - } - - $rightBooleanType = $rightType->toBoolean(); - if ($rightBooleanType instanceof ConstantBooleanType && $leftType instanceof BooleanType) { - return $this->specifyTypesInCondition( - $scope, - new Expr\BinaryOp\Identical( - $expr->left, - new ConstFetch(new Name($rightBooleanType->getValue() ? 'true' : 'false')), - ), - $context, - $rootExpr, - ); - } - - if ( - !$context->null() - && $rightType->isArray()->yes() - && $leftType instanceof ConstantArrayType && $leftType->isEmpty() - ) { - return $this->create($expr->right, new NonEmptyArrayType(), $context->negate(), false, $scope, $rootExpr); - } - - if ( - !$context->null() - && $leftType->isArray()->yes() - && $rightType instanceof ConstantArrayType && $rightType->isEmpty() - ) { - return $this->create($expr->left, new NonEmptyArrayType(), $context->negate(), false, $scope, $rootExpr); - } - - $integerType = new IntegerType(); - $floatType = new FloatType(); - if ( - ($leftType->isString()->yes() && $rightType->isString()->yes()) - || ($integerType->isSuperTypeOf($leftType)->yes() && $integerType->isSuperTypeOf($rightType)->yes()) - || ($floatType->isSuperTypeOf($leftType)->yes() && $floatType->isSuperTypeOf($rightType)->yes()) - ) { - return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context, $rootExpr); - } - - $leftExprString = $this->exprPrinter->printExpr($expr->left); - $rightExprString = $this->exprPrinter->printExpr($expr->right); - if ($leftExprString === $rightExprString) { - if (!$expr->left instanceof Expr\Variable || !$expr->right instanceof Expr\Variable) { - return new SpecifiedTypes([], [], false, [], $rootExpr); - } - } - - $leftTypes = $this->create($expr->left, $leftType, $context, false, $scope, $rootExpr); - $rightTypes = $this->create($expr->right, $rightType, $context, false, $scope, $rootExpr); - - return $context->true() - ? $leftTypes->unionWith($rightTypes) - : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($scope)); + return $this->resolveEqual($expr, $scope, $context, $rootExpr); } elseif ($expr instanceof Node\Expr\BinaryOp\NotEqual) { return $this->specifyTypesInCondition( $scope, @@ -419,20 +205,16 @@ public function specifyTypesInCondition( ); } elseif ($expr instanceof Node\Expr\BinaryOp\Smaller || $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual) { - $orEqual = $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual; - $offset = $orEqual ? 0 : 1; - $leftType = $scope->getType($expr->left); - $rightType = $scope->getType($expr->right); if ( $expr->left instanceof FuncCall && count($expr->left->getArgs()) === 1 && $expr->left->name instanceof Name - && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen'], true) + && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen', 'mb_strlen'], true) && ( !$expr->right instanceof FuncCall || !$expr->right->name instanceof Name - || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen'], true) + || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen', 'mb_strlen'], true) ) ) { $inverseOperator = $expr instanceof Node\Expr\BinaryOp\Smaller @@ -447,6 +229,9 @@ public function specifyTypesInCondition( ); } + $orEqual = $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual; + $offset = $orEqual ? 0 : 1; + $leftType = $scope->getType($expr->left); $result = new SpecifiedTypes([], [], false, [], $rootExpr); if ( @@ -455,15 +240,22 @@ public function specifyTypesInCondition( && count($expr->right->getArgs()) === 1 && $expr->right->name instanceof Name && in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true) - && (new IntegerType())->isSuperTypeOf($leftType)->yes() + && $leftType->isInteger()->yes() ) { if ( - $context->truthy() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) - || ($context->falsey() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) + $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) + || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) ) { $argType = $scope->getType($expr->right->getArgs()[0]->value); if ($argType->isArray()->yes()) { - $result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, new NonEmptyArrayType(), $context, false, $scope, $rootExpr)); + $newType = new NonEmptyArrayType(); + if ($context->true() && $argType->isList()->yes()) { + $newType = AccessoryArrayListType::intersectWith($newType); + } + + $result = $result->unionWith( + $this->create($expr->right->getArgs()[0]->value, $newType, $context, false, $scope, $rootExpr), + ); } } } @@ -473,8 +265,8 @@ public function specifyTypesInCondition( && $expr->right instanceof FuncCall && count($expr->right->getArgs()) === 1 && $expr->right->name instanceof Name - && strtolower((string) $expr->right->name) === 'strlen' - && (new IntegerType())->isSuperTypeOf($leftType)->yes() + && in_array(strtolower((string) $expr->right->name), ['strlen', 'mb_strlen'], true) + && $leftType->isInteger()->yes() ) { if ( $context->truthy() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) @@ -517,6 +309,7 @@ public function specifyTypesInCondition( } } + $rightType = $scope->getType($expr->right); if ($rightType instanceof ConstantIntegerType) { if ($expr->left instanceof Expr\PostInc) { $result = $result->unionWith($this->createRangeTypes( @@ -613,26 +406,45 @@ public function specifyTypesInCondition( return $extension->specifyTypes($functionReflection, $expr, $scope, $context); } + // lazy create parametersAcceptor, as creation can be expensive + $parametersAcceptor = null; if (count($expr->getArgs()) > 0) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $functionReflection->getVariants()); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $functionReflection->getVariants(), $functionReflection->getNamedArgumentsVariants()); + $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes; } } + + $assertions = $functionReflection->getAsserts(); + if ($assertions->getAll() !== []) { + $parametersAcceptor ??= ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $functionReflection->getVariants(), $functionReflection->getNamedArgumentsVariants()); + + $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( + $type, + $parametersAcceptor->getResolvedTemplateTypeMap(), + $parametersAcceptor instanceof ParametersAcceptorWithPhpDocs ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createInvariant(), + )); + $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + } } return $this->handleDefaultTruthyOrFalseyContext($context, $rootExpr, $expr, $scope); } elseif ($expr instanceof MethodCall && $expr->name instanceof Node\Identifier) { $methodCalledOnType = $scope->getType($expr->var); - $referencedClasses = TypeUtils::getDirectClassNames($methodCalledOnType); - if ( - count($referencedClasses) === 1 - && $this->reflectionProvider->hasClass($referencedClasses[0]) - ) { - $methodClassReflection = $this->reflectionProvider->getClass($referencedClasses[0]); - if ($methodClassReflection->hasMethod($expr->name->name)) { - $methodReflection = $methodClassReflection->getMethod($expr->name->name, $scope); + $methodReflection = $scope->getMethodReflection($methodCalledOnType, $expr->name->name); + if ($methodReflection !== null) { + $referencedClasses = $methodCalledOnType->getObjectClassNames(); + if ( + count($referencedClasses) === 1 + && $this->reflectionProvider->hasClass($referencedClasses[0]) + ) { + $methodClassReflection = $this->reflectionProvider->getClass($referencedClasses[0]); foreach ($this->getMethodTypeSpecifyingExtensionsForClass($methodClassReflection->getName()) as $extension) { if (!$extension->isMethodSupported($methodReflection, $expr, $context)) { continue; @@ -640,13 +452,32 @@ public function specifyTypesInCondition( return $extension->specifyTypes($methodReflection, $expr, $scope, $context); } + } - if (count($expr->getArgs()) > 0) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants()); - $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); - if ($specifiedTypes !== null) { - return $specifiedTypes; - } + // lazy create parametersAcceptor, as creation can be expensive + $parametersAcceptor = null; + if (count($expr->getArgs()) > 0) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); + + $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + } + + $assertions = $methodReflection->getAsserts(); + if ($assertions->getAll() !== []) { + $parametersAcceptor ??= ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); + + $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( + $type, + $parametersAcceptor->getResolvedTemplateTypeMap(), + $parametersAcceptor instanceof ParametersAcceptorWithPhpDocs ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createInvariant(), + )); + $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); + if ($specifiedTypes !== null) { + return $specifiedTypes; } } } @@ -661,7 +492,7 @@ public function specifyTypesInCondition( $staticMethodReflection = $scope->getMethodReflection($calleeType, $expr->name->name); if ($staticMethodReflection !== null) { - $referencedClasses = TypeUtils::getDirectClassNames($calleeType); + $referencedClasses = $calleeType->getObjectClassNames(); if ( count($referencedClasses) === 1 && $this->reflectionProvider->hasClass($referencedClasses[0]) @@ -676,13 +507,32 @@ public function specifyTypesInCondition( } } + // lazy create parametersAcceptor, as creation can be expensive + $parametersAcceptor = null; if (count($expr->getArgs()) > 0) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $staticMethodReflection->getVariants()); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $staticMethodReflection->getVariants(), $staticMethodReflection->getNamedArgumentsVariants()); + $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes; } } + + $assertions = $staticMethodReflection->getAsserts(); + if ($assertions->getAll() !== []) { + $parametersAcceptor ??= ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $staticMethodReflection->getVariants(), $staticMethodReflection->getNamedArgumentsVariants()); + + $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( + $type, + $parametersAcceptor->getResolvedTemplateTypeMap(), + $parametersAcceptor instanceof ParametersAcceptorWithPhpDocs ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createInvariant(), + )); + $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + } } return $this->handleDefaultTruthyOrFalseyContext($context, $rootExpr, $expr, $scope); @@ -700,8 +550,10 @@ public function specifyTypesInCondition( $types->getSureNotTypes(), false, array_merge( - $this->processBooleanConditionalTypes($scope, $leftTypes, $rightTypes), - $this->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes), + $this->processBooleanNotSureConditionalTypes($scope, $leftTypes, $rightTypes), + $this->processBooleanNotSureConditionalTypes($scope, $rightTypes, $leftTypes), + $this->processBooleanSureConditionalTypes($scope, $leftTypes, $rightTypes), + $this->processBooleanSureConditionalTypes($scope, $rightTypes, $leftTypes), ), $rootExpr, ); @@ -722,8 +574,10 @@ public function specifyTypesInCondition( $types->getSureNotTypes(), false, array_merge( - $this->processBooleanConditionalTypes($scope, $leftTypes, $rightTypes), - $this->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes), + $this->processBooleanNotSureConditionalTypes($scope, $leftTypes, $rightTypes), + $this->processBooleanNotSureConditionalTypes($scope, $rightTypes, $leftTypes), + $this->processBooleanSureConditionalTypes($scope, $leftTypes, $rightTypes), + $this->processBooleanSureConditionalTypes($scope, $rightTypes, $leftTypes), ), $rootExpr, ); @@ -746,52 +600,130 @@ public function specifyTypesInCondition( && count($expr->vars) > 0 && !$context->null() ) { + // rewrite multi param isset() to and-chained single param isset() + if (count($expr->vars) > 1) { + $issets = []; + foreach ($expr->vars as $var) { + $issets[] = new Expr\Isset_([$var], $expr->getAttributes()); + } + + $first = array_shift($issets); + $andChain = null; + foreach ($issets as $isset) { + if ($andChain === null) { + $andChain = new BooleanAnd($first, $isset); + continue; + } + + $andChain = new BooleanAnd($andChain, $isset); + } + + if ($andChain === null) { + throw new ShouldNotHappenException(); + } + + return $this->specifyTypesInCondition($scope, $andChain, $context, $rootExpr); + } + + $issetExpr = $expr->vars[0]; + if (!$context->true()) { if (!$scope instanceof MutatingScope) { throw new ShouldNotHappenException(); } - return array_reduce( - array_filter( - $expr->vars, - static fn (Expr $var) => $scope->issetCheck($var, static fn () => true), - ), - fn (SpecifiedTypes $types, Expr $var) => $types->unionWith($this->specifyTypesInCondition($scope, $var, $context, $rootExpr)), - new SpecifiedTypes(), + $isset = $scope->issetCheck($issetExpr, static fn () => true); + + if ($isset === false) { + return new SpecifiedTypes(); + } + + $type = $scope->getType($issetExpr); + $isNullable = !$type->isNull()->no(); + $exprType = $this->create( + $issetExpr, + new NullType(), + $context->negate(), + false, + $scope, + $rootExpr, ); - } - $vars = []; - foreach ($expr->vars as $var) { - $tmpVars = [$var]; + if ($issetExpr instanceof Expr\Variable && is_string($issetExpr->name)) { + if ($isset === true) { + if ($isNullable) { + return $exprType; + } - while ( - $var instanceof ArrayDimFetch - || $var instanceof PropertyFetch - || ( - $var instanceof StaticPropertyFetch - && $var->class instanceof Expr - ) - ) { - if ($var instanceof StaticPropertyFetch) { - /** @var Expr $var */ - $var = $var->class; - } else { - $var = $var->var; + // variable cannot exist in !isset() + return $exprType->unionWith($this->create( + new IssetExpr($issetExpr), + new NullType(), + $context, + false, + $scope, + $rootExpr, + )); + } + + if ($isNullable) { + // reduces variable certainty to maybe + return $exprType->unionWith($this->create( + new IssetExpr($issetExpr), + new NullType(), + $context->negate(), + false, + $scope, + $rootExpr, + )); } - $tmpVars[] = $var; + + // variable cannot exist in !isset() + return $this->create( + new IssetExpr($issetExpr), + new NullType(), + $context, + false, + $scope, + $rootExpr, + ); + } + + if ($isNullable && $isset === true) { + return $exprType; } - $vars = array_merge($vars, array_reverse($tmpVars)); + return new SpecifiedTypes(); } - $types = null; + $tmpVars = [$issetExpr]; + while ( + $issetExpr instanceof ArrayDimFetch + || $issetExpr instanceof PropertyFetch + || ( + $issetExpr instanceof StaticPropertyFetch + && $issetExpr->class instanceof Expr + ) + ) { + if ($issetExpr instanceof StaticPropertyFetch) { + /** @var Expr $issetExpr */ + $issetExpr = $issetExpr->class; + } else { + $issetExpr = $issetExpr->var; + } + $tmpVars[] = $issetExpr; + } + $vars = array_reverse($tmpVars); + + $types = new SpecifiedTypes(); foreach ($vars as $var) { + if ($var instanceof Expr\Variable && is_string($var->name)) { if ($scope->hasVariableType($var->name)->no()) { return new SpecifiedTypes([], [], false, [], $rootExpr); } } + if ( $var instanceof ArrayDimFetch && $var->dim !== null @@ -800,86 +732,114 @@ public function specifyTypesInCondition( $dimType = $scope->getType($var->dim); if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) { - $type = $this->create( - $var->var, - new HasOffsetType($dimType), - $context, - false, - $scope, - $rootExpr, + $types = $types->unionWith( + $this->create( + $var->var, + new HasOffsetType($dimType), + $context, + false, + $scope, + $rootExpr, + ), ); - } else { - $type = new SpecifiedTypes(); } - - $type = $type->unionWith( - $this->create($var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope, $rootExpr), - ); - } else { - $type = $this->create($var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope, $rootExpr); } if ( $var instanceof PropertyFetch && $var->name instanceof Node\Identifier ) { - $type = $type->unionWith($this->create($var->var, new IntersectionType([ - new ObjectWithoutClassType(), - new HasPropertyType($var->name->toString()), - ]), TypeSpecifierContext::createTruthy(), false, $scope, $rootExpr)); + $types = $types->unionWith( + $this->create($var->var, new IntersectionType([ + new ObjectWithoutClassType(), + new HasPropertyType($var->name->toString()), + ]), TypeSpecifierContext::createTruthy(), false, $scope, $rootExpr), + ); } elseif ( $var instanceof StaticPropertyFetch && $var->class instanceof Expr && $var->name instanceof Node\VarLikeIdentifier ) { - $type = $type->unionWith($this->create($var->class, new IntersectionType([ - new ObjectWithoutClassType(), - new HasPropertyType($var->name->toString()), - ]), TypeSpecifierContext::createTruthy(), false, $scope, $rootExpr)); + $types = $types->unionWith( + $this->create($var->class, new IntersectionType([ + new ObjectWithoutClassType(), + new HasPropertyType($var->name->toString()), + ]), TypeSpecifierContext::createTruthy(), false, $scope, $rootExpr), + ); } - if ($types === null) { - $types = $type; - } else { - $types = $types->unionWith($type); - } + $types = $types->unionWith( + $this->create($var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope, $rootExpr), + ); } return $types; } elseif ( $expr instanceof Expr\BinaryOp\Coalesce - && $context->true() - && ((new ConstantBooleanType(false))->isSuperTypeOf($scope->getType($expr->right))->yes()) - ) { - return $this->create( - $expr->left, - new NullType(), - TypeSpecifierContext::createFalse(), - false, - $scope, - $rootExpr, - ); - } elseif ( - $expr instanceof Expr\Empty_ - ) { - return $this->specifyTypesInCondition($scope, new BooleanOr( - new Expr\BooleanNot(new Expr\Isset_([$expr->expr])), - new Expr\BooleanNot($expr->expr), - ), $context, $rootExpr); - } elseif ($expr instanceof Expr\ErrorSuppress) { - return $this->specifyTypesInCondition($scope, $expr->expr, $context, $rootExpr); - } elseif ( - $expr instanceof Expr\Ternary && !$context->null() - && ((new ConstantBooleanType(false))->isSuperTypeOf($scope->getType($expr->else))->yes()) ) { - $conditionExpr = $expr->cond; - if ($expr->if !== null) { - $conditionExpr = new BooleanAnd($conditionExpr, $expr->if); - } - - return $this->specifyTypesInCondition($scope, $conditionExpr, $context, $rootExpr); - + if (!$context->true()) { + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + + $isset = $scope->issetCheck($expr->left, static fn () => true); + + if ($isset !== true) { + return new SpecifiedTypes(); + } + + return $this->create( + $expr->left, + new NullType(), + $context->negate(), + false, + $scope, + $rootExpr, + ); + } + + if ((new ConstantBooleanType(false))->isSuperTypeOf($scope->getType($expr->right)->toBoolean())->yes()) { + return $this->create( + $expr->left, + new NullType(), + TypeSpecifierContext::createFalse(), + false, + $scope, + $rootExpr, + ); + } + + } elseif ( + $expr instanceof Expr\Empty_ + ) { + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + + $isset = $scope->issetCheck($expr->expr, static fn () => true); + if ($isset === false) { + return new SpecifiedTypes(); + } + + return $this->specifyTypesInCondition($scope, new BooleanOr( + new Expr\BooleanNot(new Expr\Isset_([$expr->expr])), + new Expr\BooleanNot($expr->expr), + ), $context, $rootExpr); + } elseif ($expr instanceof Expr\ErrorSuppress) { + return $this->specifyTypesInCondition($scope, $expr->expr, $context, $rootExpr); + } elseif ( + $expr instanceof Expr\Ternary + && !$context->null() + && $scope->getType($expr->else)->isFalse()->yes() + ) { + $conditionExpr = $expr->cond; + if ($expr->if !== null) { + $conditionExpr = new BooleanAnd($conditionExpr, $expr->if); + } + + return $this->specifyTypesInCondition($scope, $conditionExpr, $context, $rootExpr); + } elseif ($expr instanceof Expr\NullsafePropertyFetch && !$context->null()) { $types = $this->specifyTypesInCondition( $scope, @@ -906,6 +866,34 @@ public function specifyTypesInCondition( $nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $rootExpr, $expr, $scope); return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope)); + } elseif ( + $expr instanceof Expr\New_ + && $expr->class instanceof Name + && $this->reflectionProvider->hasClass($expr->class->toString()) + ) { + $classReflection = $this->reflectionProvider->getClass($expr->class->toString()); + + if ($classReflection->hasConstructor()) { + $methodReflection = $classReflection->getConstructor(); + $asserts = $methodReflection->getAsserts(); + + if ($asserts->getAll() !== []) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); + + $asserts = $asserts->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( + $type, + $parametersAcceptor->getResolvedTemplateTypeMap(), + $parametersAcceptor instanceof ParametersAcceptorWithPhpDocs ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createInvariant(), + )); + + $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); + + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + } + } } elseif (!$context->null()) { return $this->handleDefaultTruthyOrFalseyContext($context, $rootExpr, $expr, $scope); } @@ -923,6 +911,10 @@ private function specifyTypesForConstantBinaryExpression( { if (!$context->null() && $constantType->getValue() === false) { $types = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); + if ($exprNode instanceof Expr\NullsafeMethodCall || $exprNode instanceof Expr\NullsafePropertyFetch) { + return $types; + } + return $types->unionWith($this->specifyTypesInCondition( $scope, $exprNode, @@ -933,6 +925,10 @@ private function specifyTypesForConstantBinaryExpression( if (!$context->null() && $constantType->getValue() === true) { $types = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); + if ($exprNode instanceof Expr\NullsafeMethodCall || $exprNode instanceof Expr\NullsafePropertyFetch) { + return $types; + } + return $types->unionWith($this->specifyTypesInCondition( $scope, $exprNode, @@ -961,7 +957,16 @@ private function specifyTypesForConstantBinaryExpression( $argType = $scope->getType($exprNode->getArgs()[0]->value); if ($argType->isArray()->yes()) { $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); - $valueTypes = $this->create($exprNode->getArgs()[0]->value, new NonEmptyArrayType(), $newContext, false, $scope, $rootExpr); + if ($argType->isList()->yes() && $context->truthy() && $constantType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); + $itemType = $argType->getIterableValueType(); + for ($i = 0; $i < $constantType->getValue(); $i++) { + $valueTypesBuilder->setOffsetValueType(new ConstantIntegerType($i), $itemType); + } + $valueTypes = $this->create($exprNode->getArgs()[0]->value, $valueTypesBuilder->getArray(), $context, false, $scope, $rootExpr); + } else { + $valueTypes = $this->create($exprNode->getArgs()[0]->value, new NonEmptyArrayType(), $newContext, false, $scope, $rootExpr); + } return $funcTypes->unionWith($valueTypes); } } @@ -972,7 +977,7 @@ private function specifyTypesForConstantBinaryExpression( && $exprNode instanceof FuncCall && count($exprNode->getArgs()) === 1 && $exprNode->name instanceof Name - && strtolower((string) $exprNode->name) === 'strlen' + && in_array(strtolower((string) $exprNode->name), ['strlen', 'mb_strlen'], true) && $constantType instanceof ConstantIntegerType ) { if ($context->truthy() || $constantType->getValue() === 0) { @@ -996,10 +1001,6 @@ private function specifyTypesForConstantBinaryExpression( } - if ($constantType instanceof ConstantStringType) { - return $this->specifyTypesForConstantStringBinaryExpression($exprNode, $constantType, $context, $scope, $rootExpr); - } - return null; } @@ -1015,7 +1016,7 @@ private function specifyTypesForConstantStringBinaryExpression( $context->truthy() && $exprNode instanceof FuncCall && $exprNode->name instanceof Name - && in_array(strtolower($exprNode->name->toString()), ['substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper', 'mb_strtolower', 'mb_strtoupper', 'ucfirst', 'lcfirst', 'ucwords'], true) + && in_array(strtolower($exprNode->name->toString()), ['substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper', 'mb_strtolower', 'mb_strtoupper', 'ucfirst', 'lcfirst', 'ucwords', 'mb_convert_case', 'mb_convert_kana'], true) && isset($exprNode->getArgs()[0]) && $constantType->getValue() !== '' ) { @@ -1058,7 +1059,7 @@ private function specifyTypesForConstantStringBinaryExpression( if ($constantType->getValue() === 'boolean') { $type = new BooleanType(); } - if ($constantType->getValue() === 'resource' || $constantType->getValue() === 'resource (closed)') { + if (in_array($constantType->getValue(), ['resource', 'resource (closed)'], true)) { $type = new ResourceType(); } if ($constantType->getValue() === 'integer') { @@ -1075,7 +1076,9 @@ private function specifyTypesForConstantStringBinaryExpression( } if ($type !== null) { - return $this->create($exprNode->getArgs()[0]->value, $type, $context, false, $scope, $rootExpr); + $callType = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); + $argType = $this->create($exprNode->getArgs()[0]->value, $type, $context, false, $scope, $rootExpr); + return $callType->unionWith($argType); } } @@ -1100,7 +1103,7 @@ private function specifyTypesForConstantStringBinaryExpression( ); } - if ((new ObjectWithoutClassType())->isSuperTypeOf($argType)->yes()) { + if ($argType->isObject()->yes()) { return $this->create( $exprNode->getArgs()[0]->value, $objectType, @@ -1235,10 +1238,184 @@ public function getConditionalSpecifiedTypes( return $specifiedTypes; } + private function specifyTypesFromAsserts(TypeSpecifierContext $context, Expr\CallLike $call, Assertions $assertions, ParametersAcceptor $parametersAcceptor, Scope $scope): ?SpecifiedTypes + { + if ($context->null()) { + $asserts = $assertions->getAsserts(); + } elseif ($context->true()) { + $asserts = $assertions->getAssertsIfTrue(); + } elseif ($context->false()) { + $asserts = $assertions->getAssertsIfFalse(); + } else { + throw new ShouldNotHappenException(); + } + + if (count($asserts) === 0) { + return null; + } + + $argsMap = []; + $parameters = $parametersAcceptor->getParameters(); + foreach ($call->getArgs() as $i => $arg) { + if ($arg->unpack) { + continue; + } + + if ($arg->name !== null) { + $paramName = $arg->name->toString(); + } elseif (isset($parameters[$i])) { + $paramName = $parameters[$i]->getName(); + } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { + $lastParameter = $parameters[count($parameters) - 1]; + $paramName = $lastParameter->getName(); + } else { + continue; + } + + $argsMap[$paramName][] = $arg->value; + } + + if ($call instanceof MethodCall) { + $argsMap['this'] = [$call->var]; + } + + /** @var SpecifiedTypes|null $types */ + $types = null; + + foreach ($asserts as $assert) { + foreach ($argsMap[substr($assert->getParameter()->getParameterName(), 1)] ?? [] as $parameterExpr) { + $assertedType = TypeTraverser::map($assert->getType(), static function (Type $type, callable $traverse) use ($argsMap, $scope): Type { + if ($type instanceof ConditionalTypeForParameter) { + $parameterName = substr($type->getParameterName(), 1); + if (array_key_exists($parameterName, $argsMap)) { + $argType = TypeCombinator::union(...array_map(static fn (Expr $expr) => $scope->getType($expr), $argsMap[$parameterName])); + $type = $type->toConditional($argType); + } + } + + return $traverse($type); + }); + + $assertExpr = $assert->getParameter()->getExpr($parameterExpr); + + $templateTypeMap = $parametersAcceptor->getResolvedTemplateTypeMap(); + $containsUnresolvedTemplate = false; + TypeTraverser::map( + $assert->getOriginalType(), + static function (Type $type, callable $traverse) use ($templateTypeMap, &$containsUnresolvedTemplate) { + if ($type instanceof TemplateType && $type->getScope()->getClassName() !== null) { + $resolvedType = $templateTypeMap->getType($type->getName()); + if ($resolvedType === null || $type->getBound()->equals($resolvedType)) { + $containsUnresolvedTemplate = true; + return $type; + } + } + + return $traverse($type); + }, + ); + + $newTypes = $this->create( + $assertExpr, + $assertedType, + $assert->isNegated() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createTrue(), + false, + $scope, + $containsUnresolvedTemplate || $assert->isEquality() ? $call : null, + ); + $types = $types !== null ? $types->unionWith($newTypes) : $newTypes; + + if (!$context->null() || !$assertedType instanceof ConstantBooleanType) { + continue; + } + + $subContext = $assertedType->getValue() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createFalse(); + if ($assert->isNegated()) { + $subContext = $subContext->negate(); + } + + $types = $types->unionWith($this->specifyTypesInCondition( + $scope, + $assertExpr, + $subContext, + )); + } + } + + return $types; + } + + /** + * @return array + */ + private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): array + { + $conditionExpressionTypes = []; + foreach ($leftTypes->getSureTypes() as $exprString => [$expr, $type]) { + if (!$expr instanceof Expr\Variable) { + continue; + } + if (!is_string($expr->name)) { + continue; + } + + $conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes( + $expr, + TypeCombinator::remove($scope->getType($expr), $type), + ); + } + + if (count($conditionExpressionTypes) > 0) { + $holders = []; + foreach ($rightTypes->getSureTypes() as $exprString => [$expr, $type]) { + if (!$expr instanceof Expr\Variable) { + continue; + } + if (!is_string($expr->name)) { + continue; + } + + if (!isset($holders[$exprString])) { + $holders[$exprString] = []; + } + + $conditions = $conditionExpressionTypes; + foreach ($conditions as $conditionExprString => $conditionExprTypeHolder) { + $conditionExpr = $conditionExprTypeHolder->getExpr(); + if (!$conditionExpr instanceof Expr\Variable) { + continue; + } + if (!is_string($conditionExpr->name)) { + continue; + } + if ($conditionExpr->name !== $expr->name) { + continue; + } + + unset($conditions[$conditionExprString]); + } + + if (count($conditions) === 0) { + continue; + } + + $holder = new ConditionalExpressionHolder( + $conditions, + new ExpressionTypeHolder($expr, TypeCombinator::intersect($scope->getType($expr), $type), TrinaryLogic::createYes()), + ); + $holders[$exprString][$holder->getKey()] = $holder; + } + + return $holders; + } + + return []; + } + /** * @return array */ - private function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): array + private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): array { $conditionExpressionTypes = []; foreach ($leftTypes->getSureNotTypes() as $exprString => [$expr, $type]) { @@ -1249,7 +1426,10 @@ private function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $le continue; } - $conditionExpressionTypes[$exprString] = TypeCombinator::intersect($scope->getType($expr), $type); + $conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes( + $expr, + TypeCombinator::intersect($scope->getType($expr), $type), + ); } if (count($conditionExpressionTypes) > 0) { @@ -1266,9 +1446,29 @@ private function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $le $holders[$exprString] = []; } + $conditions = $conditionExpressionTypes; + foreach ($conditions as $conditionExprString => $conditionExprTypeHolder) { + $conditionExpr = $conditionExprTypeHolder->getExpr(); + if (!$conditionExpr instanceof Expr\Variable) { + continue; + } + if (!is_string($conditionExpr->name)) { + continue; + } + if ($conditionExpr->name !== $expr->name) { + continue; + } + + unset($conditions[$conditionExprString]); + } + + if (count($conditions) === 0) { + continue; + } + $holder = new ConditionalExpressionHolder( - $conditionExpressionTypes, - new VariableTypeHolder(TypeCombinator::remove($scope->getType($expr), $type), TrinaryLogic::createYes()), + $conditions, + new ExpressionTypeHolder($expr, TypeCombinator::remove($scope->getType($expr), $type), TrinaryLogic::createYes()), ); $holders[$exprString][$holder->getKey()] = $holder; } @@ -1280,22 +1480,33 @@ private function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $le } /** - * @return (Expr|ConstantScalarType)[]|null + * @return array{Expr, ConstantScalarType}|null */ private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\BinaryOp $binaryOperation): ?array { $leftType = $scope->getType($binaryOperation->left); $rightType = $scope->getType($binaryOperation->right); + + $rightExpr = $binaryOperation->right; + if ($rightExpr instanceof AlwaysRememberedExpr) { + $rightExpr = $rightExpr->getExpr(); + } + + $leftExpr = $binaryOperation->left; + if ($leftExpr instanceof AlwaysRememberedExpr) { + $leftExpr = $leftExpr->getExpr(); + } + if ( $leftType instanceof ConstantScalarType - && !$binaryOperation->right instanceof ConstFetch - && !$binaryOperation->right instanceof ClassConstFetch + && !$rightExpr instanceof ConstFetch + && !$rightExpr instanceof ClassConstFetch ) { return [$binaryOperation->right, $leftType]; } elseif ( $rightType instanceof ConstantScalarType - && !$binaryOperation->left instanceof ConstFetch - && !$binaryOperation->left instanceof ClassConstFetch + && !$leftExpr instanceof ConstFetch + && !$leftExpr instanceof ClassConstFetch ) { return [$binaryOperation->left, $rightType]; } @@ -1318,14 +1529,21 @@ public function create( } $specifiedExprs = []; + if ($expr instanceof AlwaysRememberedExpr) { + $specifiedExprs[] = $expr; + $expr = $expr->expr; + } if ($expr instanceof Expr\Assign) { $specifiedExprs[] = $expr->var; + $specifiedExprs[] = $expr->expr; while ($expr->expr instanceof Expr\Assign) { $specifiedExprs[] = $expr->expr->var; $expr = $expr->expr; } + } elseif ($expr instanceof Expr\AssignOp\Coalesce) { + $specifiedExprs[] = $expr->var; } else { $specifiedExprs[] = $expr; } @@ -1356,14 +1574,14 @@ private function createForExpr( { if ($scope !== null) { if ($context->true()) { - $resultType = TypeCombinator::intersect($scope->getType($expr), $type); + $containsNull = !$type->isNull()->no() && !$scope->getType($expr)->isNull()->no(); } elseif ($context->false()) { - $resultType = TypeCombinator::remove($scope->getType($expr), $type); + $containsNull = !TypeCombinator::containsNull($type) && !$scope->getType($expr)->isNull()->no(); } } $originalExpr = $expr; - if (isset($resultType) && !TypeCombinator::containsNull($resultType)) { + if (isset($containsNull) && !$containsNull) { $expr = NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($expr); } @@ -1412,7 +1630,7 @@ private function createForExpr( || $methodReflection->hasSideEffects()->yes() || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no()) ) { - if (isset($resultType) && !TypeCombinator::containsNull($resultType)) { + if (isset($containsNull) && !$containsNull) { return $this->createNullsafeTypes($rootExpr, $originalExpr, $scope, $context, $overwrite, $type); } @@ -1438,7 +1656,7 @@ private function createForExpr( || $methodReflection->hasSideEffects()->yes() || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no()) ) { - if (isset($resultType) && !TypeCombinator::containsNull($resultType)) { + if (isset($containsNull) && !$containsNull) { return $this->createNullsafeTypes($rootExpr, $originalExpr, $scope, $context, $overwrite, $type); } @@ -1463,7 +1681,7 @@ private function createForExpr( } $types = new SpecifiedTypes($sureTypes, $sureNotTypes, $overwrite, [], $rootExpr); - if ($scope !== null && isset($resultType) && !TypeCombinator::containsNull($resultType)) { + if ($scope !== null && isset($containsNull) && !$containsNull) { return $this->createNullsafeTypes($rootExpr, $originalExpr, $scope, $context, $overwrite, $type)->unionWith($types); } @@ -1595,4 +1813,342 @@ private function getTypeSpecifyingExtensionsForType(array $extensions, string $c return array_merge(...$extensionsForClass); } + public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecifierContext $context, ?Expr $rootExpr): SpecifiedTypes + { + $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr); + if ($expressions !== null) { + $exprNode = $expressions[0]; + $constantType = $expressions[1]; + if (!$context->null() && ($constantType->getValue() === false || $constantType->getValue() === null)) { + return $this->specifyTypesInCondition( + $scope, + $exprNode, + $context->true() ? TypeSpecifierContext::createFalsey() : TypeSpecifierContext::createFalsey()->negate(), + $rootExpr, + ); + } + + if (!$context->null() && $constantType->getValue() === true) { + return $this->specifyTypesInCondition( + $scope, + $exprNode, + $context->true() ? TypeSpecifierContext::createTruthy() : TypeSpecifierContext::createTruthy()->negate(), + $rootExpr, + ); + } + + if ( + $exprNode instanceof FuncCall + && $exprNode->name instanceof Name + && strtolower($exprNode->name->toString()) === 'gettype' + && isset($exprNode->getArgs()[0]) + && $constantType->isString()->yes() + ) { + return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context, $rootExpr); + } + + if ( + $exprNode instanceof FuncCall + && $exprNode->name instanceof Name + && strtolower($exprNode->name->toString()) === 'get_class' + && isset($exprNode->getArgs()[0]) + && $constantType->isString()->yes() + ) { + return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context, $rootExpr); + } + } + + $leftType = $scope->getType($expr->left); + $rightType = $scope->getType($expr->right); + + $leftBooleanType = $leftType->toBoolean(); + if ($leftBooleanType instanceof ConstantBooleanType && $rightType->isBoolean()->yes()) { + return $this->specifyTypesInCondition( + $scope, + new Expr\BinaryOp\Identical( + new ConstFetch(new Name($leftBooleanType->getValue() ? 'true' : 'false')), + $expr->right, + ), + $context, + $rootExpr, + ); + } + + $rightBooleanType = $rightType->toBoolean(); + if ($rightBooleanType instanceof ConstantBooleanType && $leftType->isBoolean()->yes()) { + return $this->specifyTypesInCondition( + $scope, + new Expr\BinaryOp\Identical( + $expr->left, + new ConstFetch(new Name($rightBooleanType->getValue() ? 'true' : 'false')), + ), + $context, + $rootExpr, + ); + } + + if ( + !$context->null() + && $rightType->isArray()->yes() + && $leftType->isConstantArray()->yes() && $leftType->isIterableAtLeastOnce()->no() + ) { + return $this->create($expr->right, new NonEmptyArrayType(), $context->negate(), false, $scope, $rootExpr); + } + + if ( + !$context->null() + && $leftType->isArray()->yes() + && $rightType->isConstantArray()->yes() && $rightType->isIterableAtLeastOnce()->no() + ) { + return $this->create($expr->left, new NonEmptyArrayType(), $context->negate(), false, $scope, $rootExpr); + } + + if ( + ($leftType->isString()->yes() && $rightType->isString()->yes()) + || ($leftType->isInteger()->yes() && $rightType->isInteger()->yes()) + || ($leftType->isFloat()->yes() && $rightType->isFloat()->yes()) + || ($leftType->isEnum()->yes() && $rightType->isEnum()->yes()) + ) { + return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context, $rootExpr); + } + + $leftExprString = $this->exprPrinter->printExpr($expr->left); + $rightExprString = $this->exprPrinter->printExpr($expr->right); + if ($leftExprString === $rightExprString) { + if (!$expr->left instanceof Expr\Variable || !$expr->right instanceof Expr\Variable) { + return new SpecifiedTypes([], [], false, [], $rootExpr); + } + } + + $leftTypes = $this->create($expr->left, $leftType, $context, false, $scope, $rootExpr); + $rightTypes = $this->create($expr->right, $rightType, $context, false, $scope, $rootExpr); + + return $context->true() + ? $leftTypes->unionWith($rightTypes) + : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($scope)); + } + + public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context, ?Expr $rootExpr): SpecifiedTypes + { + $leftExpr = $expr->left; + $rightExpr = $expr->right; + if ($rightExpr instanceof FuncCall && !$leftExpr instanceof FuncCall) { + [$leftExpr, $rightExpr] = [$rightExpr, $leftExpr]; + } + $unwrappedLeftExpr = $leftExpr; + if ($leftExpr instanceof AlwaysRememberedExpr) { + $unwrappedLeftExpr = $leftExpr->getExpr(); + } + $unwrappedRightExpr = $rightExpr; + if ($rightExpr instanceof AlwaysRememberedExpr) { + $unwrappedRightExpr = $rightExpr->getExpr(); + } + $rightType = $scope->getType($rightExpr); + if ( + $context->true() + && $unwrappedLeftExpr instanceof FuncCall + && $unwrappedLeftExpr->name instanceof Name + && strtolower($unwrappedLeftExpr->name->toString()) === 'get_class' + && isset($unwrappedLeftExpr->getArgs()[0]) + ) { + if ($rightType->getClassStringObjectType()->isObject()->yes()) { + return $this->create( + $unwrappedLeftExpr->getArgs()[0]->value, + $rightType->getClassStringObjectType(), + $context, + false, + $scope, + $rootExpr, + )->unionWith($this->create($leftExpr, $rightType, $context, false, $scope, $rootExpr)); + } + } + + if (count($rightType->getConstantStrings()) > 0) { + $types = null; + foreach ($rightType->getConstantStrings() as $constantString) { + $specifiedType = $this->specifyTypesForConstantStringBinaryExpression($unwrappedLeftExpr, $constantString, $context, $scope, $rootExpr); + if ($specifiedType === null) { + continue; + } + if ($types === null) { + $types = $specifiedType; + continue; + } + + $types = $types->intersectWith($specifiedType); + } + + if ($types !== null) { + if ($leftExpr !== $unwrappedLeftExpr) { + $types = $types->unionWith($this->create($leftExpr, $rightType, $context, false, $scope, $rootExpr)); + } + return $types; + } + } + + $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr); + if ($expressions !== null) { + $exprNode = $expressions[0]; + $constantType = $expressions[1]; + + $specifiedType = $this->specifyTypesForConstantBinaryExpression($exprNode, $constantType, $context, $scope, $rootExpr); + if ($specifiedType !== null) { + if ($exprNode instanceof AlwaysRememberedExpr) { + $specifiedType->unionWith( + $this->create($exprNode->getExpr(), $constantType, $context, false, $scope, $rootExpr), + ); + } + return $specifiedType; + } + } + + if ( + $context->true() && + $unwrappedLeftExpr instanceof ClassConstFetch && + $unwrappedLeftExpr->class instanceof Expr && + $unwrappedLeftExpr->name instanceof Node\Identifier && + $unwrappedRightExpr instanceof ClassConstFetch && + $rightType instanceof ConstantStringType && + strtolower($unwrappedLeftExpr->name->toString()) === 'class' + ) { + return $this->specifyTypesInCondition( + $scope, + new Instanceof_( + $unwrappedLeftExpr->class, + new Name($rightType->getValue()), + ), + $context, + $rootExpr, + )->unionWith($this->create($leftExpr, $rightType, $context, false, $scope, $rootExpr)); + } + + $leftType = $scope->getType($leftExpr); + if ( + $context->true() && + $unwrappedRightExpr instanceof ClassConstFetch && + $unwrappedRightExpr->class instanceof Expr && + $unwrappedRightExpr->name instanceof Node\Identifier && + $unwrappedLeftExpr instanceof ClassConstFetch && + $leftType instanceof ConstantStringType && + strtolower($unwrappedRightExpr->name->toString()) === 'class' + ) { + return $this->specifyTypesInCondition( + $scope, + new Instanceof_( + $unwrappedRightExpr->class, + new Name($leftType->getValue()), + ), + $context, + $rootExpr, + )->unionWith($this->create($rightExpr, $leftType, $context, false, $scope, $rootExpr)); + } + + if ($context->false()) { + $identicalType = $scope->getType($expr); + if ($identicalType instanceof ConstantBooleanType) { + $never = new NeverType(); + $contextForTypes = $identicalType->getValue() ? $context->negate() : $context; + $leftTypes = $this->create($leftExpr, $never, $contextForTypes, false, $scope, $rootExpr); + $rightTypes = $this->create($rightExpr, $never, $contextForTypes, false, $scope, $rootExpr); + if ($leftExpr instanceof AlwaysRememberedExpr) { + $leftTypes = $leftTypes->unionWith( + $this->create($unwrappedLeftExpr, $never, $contextForTypes, false, $scope, $rootExpr), + ); + } + if ($rightExpr instanceof AlwaysRememberedExpr) { + $rightTypes = $rightTypes->unionWith( + $this->create($unwrappedRightExpr, $never, $contextForTypes, false, $scope, $rootExpr), + ); + } + return $leftTypes->unionWith($rightTypes); + } + } + + $types = null; + if ( + count($leftType->getFiniteTypes()) === 1 + || ($context->true() && $leftType->isConstantValue()->yes() && !$rightType->equals($leftType) && $rightType->isSuperTypeOf($leftType)->yes()) + ) { + $types = $this->create( + $rightExpr, + $leftType, + $context, + false, + $scope, + $rootExpr, + ); + if ($rightExpr instanceof AlwaysRememberedExpr) { + $types = $types->unionWith($this->create( + $unwrappedRightExpr, + $leftType, + $context, + false, + $scope, + $rootExpr, + )); + } + } + if ( + count($rightType->getFiniteTypes()) === 1 + || ($context->true() && $rightType->isConstantValue()->yes() && !$leftType->equals($rightType) && $leftType->isSuperTypeOf($rightType)->yes()) + ) { + $leftTypes = $this->create( + $leftExpr, + $rightType, + $context, + false, + $scope, + $rootExpr, + ); + if ($leftExpr instanceof AlwaysRememberedExpr) { + $leftTypes = $leftTypes->unionWith($this->create( + $unwrappedLeftExpr, + $rightType, + $context, + false, + $scope, + $rootExpr, + )); + } + if ($types !== null) { + $types = $types->unionWith($leftTypes); + } else { + $types = $leftTypes; + } + } + + if ($types !== null) { + return $types; + } + + $leftExprString = $this->exprPrinter->printExpr($unwrappedLeftExpr); + $rightExprString = $this->exprPrinter->printExpr($unwrappedRightExpr); + if ($leftExprString === $rightExprString) { + if (!$unwrappedLeftExpr instanceof Expr\Variable || !$unwrappedRightExpr instanceof Expr\Variable) { + return new SpecifiedTypes([], [], false, [], $rootExpr); + } + } + + if ($context->true()) { + $leftTypes = $this->create($leftExpr, $rightType, $context, false, $scope, $rootExpr); + $rightTypes = $this->create($rightExpr, $leftType, $context, false, $scope, $rootExpr); + if ($leftExpr instanceof AlwaysRememberedExpr) { + $leftTypes = $leftTypes->unionWith( + $this->create($unwrappedLeftExpr, $rightType, $context, false, $scope, $rootExpr), + ); + } + if ($rightExpr instanceof AlwaysRememberedExpr) { + $rightTypes = $rightTypes->unionWith( + $this->create($unwrappedRightExpr, $leftType, $context, false, $scope, $rootExpr), + ); + } + return $leftTypes->unionWith($rightTypes); + } elseif ($context->false()) { + return $this->create($leftExpr, $leftType, $context, false, $scope, $rootExpr)->normalize($scope) + ->intersectWith($this->create($rightExpr, $rightType, $context, false, $scope, $rootExpr)->normalize($scope)); + } + + return new SpecifiedTypes([], [], false, [], $rootExpr); + } + } diff --git a/src/Broker/AnonymousClassNameHelper.php b/src/Broker/AnonymousClassNameHelper.php index b0e9bdfb40..20e3087b00 100644 --- a/src/Broker/AnonymousClassNameHelper.php +++ b/src/Broker/AnonymousClassNameHelper.php @@ -34,7 +34,7 @@ public function getAnonymousClassName( return sprintf( 'AnonymousClass%s', - md5(sprintf('%s:%s', $filename, $classNode->getLine())), + md5(sprintf('%s:%s', $filename, $classNode->getStartLine())), ); } diff --git a/src/Broker/BrokerFactory.php b/src/Broker/BrokerFactory.php index 182b0ca3ca..d35b68e30d 100644 --- a/src/Broker/BrokerFactory.php +++ b/src/Broker/BrokerFactory.php @@ -10,10 +10,12 @@ class BrokerFactory public const PROPERTIES_CLASS_REFLECTION_EXTENSION_TAG = 'phpstan.broker.propertiesClassReflectionExtension'; public const METHODS_CLASS_REFLECTION_EXTENSION_TAG = 'phpstan.broker.methodsClassReflectionExtension'; + public const ALLOWED_SUB_TYPES_CLASS_REFLECTION_EXTENSION_TAG = 'phpstan.broker.allowedSubTypesClassReflectionExtension'; public const DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicMethodReturnTypeExtension'; public const DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicStaticMethodReturnTypeExtension'; public const DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicFunctionReturnTypeExtension'; public const OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG = 'phpstan.broker.operatorTypeSpecifyingExtension'; + public const EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG = 'phpstan.broker.expressionTypeResolverExtension'; public function __construct(private Container $container) { diff --git a/src/Cache/FileCacheStorage.php b/src/Cache/FileCacheStorage.php index aa28c41498..38b36d25aa 100644 --- a/src/Cache/FileCacheStorage.php +++ b/src/Cache/FileCacheStorage.php @@ -5,12 +5,11 @@ use InvalidArgumentException; use Nette\Utils\Random; use PHPStan\File\FileWriter; +use PHPStan\Internal\DirectoryCreator; +use PHPStan\Internal\DirectoryCreatorException; use PHPStan\ShouldNotHappenException; -use function clearstatcache; use function error_get_last; -use function is_dir; use function is_file; -use function mkdir; use function rename; use function sha1; use function sprintf; @@ -26,24 +25,6 @@ public function __construct(private string $directory) { } - private function makeDir(string $directory): void - { - if (is_dir($directory)) { - return; - } - - $result = @mkdir($directory, 0777); - if ($result === false) { - clearstatcache(); - if (is_dir($directory)) { - return; - } - - $error = error_get_last(); - throw new InvalidArgumentException(sprintf('Failed to create directory "%s" (%s).', $this->directory, $error !== null ? $error['message'] : 'unknown cause')); - } - } - /** * @return mixed|null */ @@ -52,10 +33,6 @@ public function load(string $key, string $variableKey) [,, $filePath] = $this->getFilePaths($key); return (static function () use ($variableKey, $filePath) { - if (!is_file($filePath)) { - return null; - } - $cacheItem = @include $filePath; if (!$cacheItem instanceof CacheItem) { return null; @@ -70,13 +47,14 @@ public function load(string $key, string $variableKey) /** * @param mixed $data + * @throws DirectoryCreatorException */ public function save(string $key, string $variableKey, $data): void { [$firstDirectory, $secondDirectory, $path] = $this->getFilePaths($key); - $this->makeDir($this->directory); - $this->makeDir($firstDirectory); - $this->makeDir($secondDirectory); + DirectoryCreator::ensureDirectoryExists($this->directory, 0777); + DirectoryCreator::ensureDirectoryExists($firstDirectory, 0777); + DirectoryCreator::ensureDirectoryExists($secondDirectory, 0777); $tmpPath = sprintf('%s/%s.tmp', $this->directory, Random::generate()); $errorBefore = error_get_last(); @@ -88,7 +66,8 @@ public function save(string $key, string $variableKey, $data): void FileWriter::write( $tmpPath, sprintf( - " */ + public function getClassPrefixes(): array; + +} diff --git a/src/Collectors/Collector.php b/src/Collectors/Collector.php index 2049278668..e046f89750 100644 --- a/src/Collectors/Collector.php +++ b/src/Collectors/Collector.php @@ -6,6 +6,19 @@ use PHPStan\Analyser\Scope; /** + * This is the interface custom collectors implement. To register it in the configuration file + * use the `phpstan.collector` service tag: + * + * ``` + * services: + * - + * class: App\MyCollector + * tags: + * - phpstan.collector + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/collectors + * * @api * @phpstan-template-covariant TNodeType of Node * @phpstan-template-covariant TValue diff --git a/src/Command/AnalyseApplication.php b/src/Command/AnalyseApplication.php index 01a7ca4793..a71e9dd769 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -2,32 +2,21 @@ namespace PHPStan\Command; -use PHPStan\AnalysedCodeException; use PHPStan\Analyser\AnalyserResult; -use PHPStan\Analyser\Error; -use PHPStan\Analyser\IgnoredErrorHelper; +use PHPStan\Analyser\AnalyserResultFinalizer; +use PHPStan\Analyser\Ignore\IgnoredErrorHelper; use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; -use PHPStan\Analyser\RuleErrorTransformer; -use PHPStan\Analyser\ScopeContext; -use PHPStan\Analyser\ScopeFactory; -use PHPStan\BetterReflection\NodeCompiler\Exception\UnableToCompileNode; -use PHPStan\BetterReflection\Reflection\Exception\CircularReference; -use PHPStan\BetterReflection\Reflection\Exception\NotAClassReflection; -use PHPStan\BetterReflection\Reflection\Exception\NotAnInterfaceReflection; -use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; -use PHPStan\Collectors\CollectedData; use PHPStan\Internal\BytesHelper; -use PHPStan\Node\CollectedDataNode; use PHPStan\PhpDoc\StubFilesProvider; use PHPStan\PhpDoc\StubValidator; -use PHPStan\Rules\Registry as RuleRegistry; use PHPStan\ShouldNotHappenException; use Symfony\Component\Console\Input\InputInterface; use function array_merge; use function count; -use function is_string; +use function is_file; use function memory_get_peak_usage; use function microtime; +use function sha1_file; use function sprintf; class AnalyseApplication @@ -35,14 +24,12 @@ class AnalyseApplication public function __construct( private AnalyserRunner $analyserRunner, + private AnalyserResultFinalizer $analyserResultFinalizer, private StubValidator $stubValidator, private ResultCacheManagerFactory $resultCacheManagerFactory, private IgnoredErrorHelper $ignoredErrorHelper, private int $internalErrorsCountLimit, private StubFilesProvider $stubFilesProvider, - private RuleRegistry $ruleRegistry, - private ScopeFactory $scopeFactory, - private RuleErrorTransformer $ruleErrorTransformer, ) { } @@ -63,17 +50,22 @@ public function analyse( InputInterface $input, ): AnalysisResult { - $resultCacheManager = $this->resultCacheManagerFactory->create([]); + $isResultCacheUsed = false; + $resultCacheManager = $this->resultCacheManagerFactory->create(); $ignoredErrorHelperResult = $this->ignoredErrorHelper->initialize(); + $fileSpecificErrors = []; + $notFileSpecificErrors = []; if (count($ignoredErrorHelperResult->getErrors()) > 0) { - $errors = $ignoredErrorHelperResult->getErrors(); + $notFileSpecificErrors = $ignoredErrorHelperResult->getErrors(); $internalErrors = []; $collectedData = []; $savedResultCache = false; + $memoryUsageBytes = memory_get_peak_usage(true); if ($errorOutput->isDebug()) { $errorOutput->writeLineFormatted('Result cache was not saved because of ignoredErrorHelperResult errors.'); } + $changedProjectExtensionFilesOutsideOfAnalysedPaths = []; } else { $resultCache = $resultCacheManager->restore($files, $debug, $onlyFiles, $projectConfigArray, $errorOutput); $intermediateAnalyserResult = $this->runAnalyser( @@ -88,46 +80,75 @@ public function analyse( $projectStubFiles = $this->stubFilesProvider->getProjectStubFiles(); - if ($resultCache->isFullAnalysis() && count($projectStubFiles) !== 0) { + $forceValidateStubFiles = (bool) ($_SERVER['__PHPSTAN_FORCE_VALIDATE_STUB_FILES'] ?? false); + if ( + $resultCache->isFullAnalysis() + && count($projectStubFiles) !== 0 + && (!$onlyFiles || $forceValidateStubFiles) + ) { $stubErrors = $this->stubValidator->validate($projectStubFiles, $debug); $intermediateAnalyserResult = new AnalyserResult( - array_merge($intermediateAnalyserResult->getErrors(), $stubErrors), + array_merge($intermediateAnalyserResult->getUnorderedErrors(), $stubErrors), + $intermediateAnalyserResult->getFilteredPhpErrors(), + $intermediateAnalyserResult->getAllPhpErrors(), + $intermediateAnalyserResult->getLocallyIgnoredErrors(), + $intermediateAnalyserResult->getLinesToIgnore(), + $intermediateAnalyserResult->getUnmatchedLineIgnores(), $intermediateAnalyserResult->getInternalErrors(), $intermediateAnalyserResult->getCollectedData(), $intermediateAnalyserResult->getDependencies(), $intermediateAnalyserResult->getExportedNodes(), $intermediateAnalyserResult->hasReachedInternalErrorsCountLimit(), + $intermediateAnalyserResult->getPeakMemoryUsageBytes(), ); } $resultCacheResult = $resultCacheManager->process($intermediateAnalyserResult, $resultCache, $errorOutput, $onlyFiles, true); - $analyserResult = $resultCacheResult->getAnalyserResult(); + $analyserResult = $this->analyserResultFinalizer->finalize($resultCacheResult->getAnalyserResult(), $onlyFiles)->getAnalyserResult(); $internalErrors = $analyserResult->getInternalErrors(); - $errors = $analyserResult->getErrors(); + $errors = array_merge( + $analyserResult->getErrors(), + $analyserResult->getFilteredPhpErrors(), + ); $hasInternalErrors = count($internalErrors) > 0 || $analyserResult->hasReachedInternalErrorsCountLimit(); - if (!$hasInternalErrors) { - foreach ($this->getCollectedDataErrors($analyserResult->getCollectedData()) as $error) { - $errors[] = $error; + $memoryUsageBytes = $analyserResult->getPeakMemoryUsageBytes(); + $isResultCacheUsed = !$resultCache->isFullAnalysis(); + + $changedProjectExtensionFilesOutsideOfAnalysedPaths = []; + if ( + $isResultCacheUsed + && $resultCacheResult->isSaved() + && !$onlyFiles + && $projectConfigArray !== null + ) { + foreach ($resultCache->getProjectExtensionFiles() as $file => [$hash, $isAnalysed, $className]) { + if ($isAnalysed) { + continue; + } + + if (!is_file($file)) { + $changedProjectExtensionFilesOutsideOfAnalysedPaths[$file] = $className; + continue; + } + + $newHash = sha1_file($file); + if ($newHash === $hash) { + continue; + } + + $changedProjectExtensionFilesOutsideOfAnalysedPaths[$file] = $className; } } - $errors = $ignoredErrorHelperResult->process($errors, $onlyFiles, $files, $hasInternalErrors); + + $ignoredErrorHelperProcessedResult = $ignoredErrorHelperResult->process($errors, $onlyFiles, $files, $hasInternalErrors); + $fileSpecificErrors = $ignoredErrorHelperProcessedResult->getNotIgnoredErrors(); + $notFileSpecificErrors = $ignoredErrorHelperProcessedResult->getOtherIgnoreMessages(); $collectedData = $analyserResult->getCollectedData(); $savedResultCache = $resultCacheResult->isSaved(); if ($analyserResult->hasReachedInternalErrorsCountLimit()) { - $errors[] = sprintf('Reached internal errors count limit of %d, exiting...', $this->internalErrorsCountLimit); - } - $errors = array_merge($errors, $internalErrors); - } - - $fileSpecificErrors = []; - $notFileSpecificErrors = []; - foreach ($errors as $error) { - if (is_string($error)) { - $notFileSpecificErrors[] = $error; - continue; + $notFileSpecificErrors[] = sprintf('Reached internal errors count limit of %d, exiting...', $this->internalErrorsCountLimit); } - - $fileSpecificErrors[] = $error; + $notFileSpecificErrors = array_merge($notFileSpecificErrors, $internalErrors); } return new AnalysisResult( @@ -139,42 +160,12 @@ public function analyse( $defaultLevelUsed, $projectConfigFile, $savedResultCache, + $memoryUsageBytes, + $isResultCacheUsed, + $changedProjectExtensionFilesOutsideOfAnalysedPaths, ); } - /** - * @param CollectedData[] $collectedData - * @return Error[] - */ - private function getCollectedDataErrors(array $collectedData): array - { - $nodeType = CollectedDataNode::class; - $node = new CollectedDataNode($collectedData); - $file = 'N/A'; - $scope = $this->scopeFactory->create(ScopeContext::create($file)); - $errors = []; - foreach ($this->ruleRegistry->getRules($nodeType) as $rule) { - try { - $ruleErrors = $rule->processNode($node, $scope); - } catch (AnalysedCodeException $e) { - $errors[] = new Error($e->getMessage(), $file, $node->getLine(), $e, null, null, $e->getTip()); - continue; - } catch (IdentifierNotFound $e) { - $errors[] = new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, $node->getLine(), $e, null, null, 'Learn more at https://phpstan.org/user-guide/discovering-symbols'); - continue; - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection | CircularReference $e) { - $errors[] = new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getLine(), $e); - continue; - } - - foreach ($ruleErrors as $ruleError) { - $errors[] = $this->ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getLine()); - } - } - - return $errors; - } - /** * @param string[] $files * @param string[] $allAnalysedFiles @@ -195,7 +186,7 @@ private function runAnalyser( $errorOutput->getStyle()->progressStart($allAnalysedFilesCount); $errorOutput->getStyle()->progressAdvance($allAnalysedFilesCount); $errorOutput->getStyle()->progressFinish(); - return new AnalyserResult([], [], [], [], [], false); + return new AnalyserResult([], [], [], [], [], [], [], [], [], [], false, memory_get_peak_usage(true)); } if (!$debug) { @@ -227,7 +218,7 @@ private function runAnalyser( } } - $analyserResult = $this->analyserRunner->runAnalyser($files, $allAnalysedFiles, $preFileCallback, $postFileCallback, $debug, true, $projectConfigFile, null, null, $input); + $analyserResult = $this->analyserRunner->runAnalyser($files, $allAnalysedFiles, $preFileCallback, $postFileCallback, $debug, true, $projectConfigFile, $input); if (!$debug) { $errorOutput->getStyle()->progressFinish(); diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index ce142af9e0..45d3393880 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -3,17 +3,21 @@ namespace PHPStan\Command; use OndraM\CiDetector\CiDetector; -use PHPStan\Analyser\ResultCache\ResultCacheClearer; use PHPStan\Command\ErrorFormatter\BaselineNeonErrorFormatter; +use PHPStan\Command\ErrorFormatter\BaselinePhpErrorFormatter; use PHPStan\Command\ErrorFormatter\ErrorFormatter; -use PHPStan\Command\ErrorFormatter\TableErrorFormatter; use PHPStan\Command\Symfony\SymfonyOutput; use PHPStan\Command\Symfony\SymfonyStyle; +use PHPStan\DependencyInjection\Container; use PHPStan\File\CouldNotWriteFileException; use PHPStan\File\FileReader; use PHPStan\File\FileWriter; use PHPStan\File\ParentDirectoryRelativePathHelper; use PHPStan\File\PathNotFoundException; +use PHPStan\File\RelativePathHelper; +use PHPStan\Internal\BytesHelper; +use PHPStan\Internal\DirectoryCreator; +use PHPStan\Internal\DirectoryCreatorException; use PHPStan\ShouldNotHappenException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -23,18 +27,22 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\StreamOutput; use Throwable; +use function array_intersect; +use function array_keys; use function array_map; +use function array_unique; +use function array_values; use function count; use function dirname; +use function filesize; use function fopen; use function get_class; use function implode; +use function in_array; use function is_array; use function is_bool; -use function is_dir; use function is_file; use function is_string; -use function mkdir; use function pathinfo; use function rewind; use function sprintf; @@ -78,10 +86,11 @@ protected function configure(): void new InputOption('generate-baseline', 'b', InputOption::VALUE_OPTIONAL, 'Path to a file where the baseline should be saved', false), new InputOption('allow-empty-baseline', null, InputOption::VALUE_NONE, 'Do not error out when the generated baseline is empty'), new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for analysis'), - new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with XDebug for debugging purposes'), + new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with Xdebug for debugging purposes'), new InputOption('fix', null, InputOption::VALUE_NONE, 'Launch PHPStan Pro'), new InputOption('watch', null, InputOption::VALUE_NONE, 'Launch PHPStan Pro'), new InputOption('pro', null, InputOption::VALUE_NONE, 'Launch PHPStan Pro'), + new InputOption('fail-without-result-cache', null, InputOption::VALUE_NONE, 'Return non-zero exit code when result cache is not used'), ]); } @@ -98,7 +107,7 @@ protected function initialize(InputInterface $input, OutputInterface $output): v if ((bool) $input->getOption('debug')) { $application = $this->getApplication(); if ($application === null) { - throw new ShouldNotHappenException(); + return; } $application->setCatchExceptions(false); return; @@ -115,6 +124,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $allowXdebug = $input->getOption('xdebug'); $debugEnabled = (bool) $input->getOption('debug'); $fix = (bool) $input->getOption('fix') || (bool) $input->getOption('watch') || (bool) $input->getOption('pro'); + $failWithoutResultCache = (bool) $input->getOption('fail-without-result-cache'); /** @var string|false|null $generateBaselineFile */ $generateBaselineFile = $input->getOption('generate-baseline'); @@ -157,7 +167,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($generateBaselineFile === null && $allowEmptyBaseline) { $inceptionResult->getStdOutput()->getStyle()->error('You must pass the --generate-baseline option alongside --allow-empty-baseline.'); - return $inceptionResult->handleReturn(1); + return $inceptionResult->handleReturn(1, null); } $errorOutput = $inceptionResult->getErrorOutput(); @@ -200,13 +210,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int $baselineExtension = pathinfo($generateBaselineFile, PATHINFO_EXTENSION); if ($baselineExtension === '') { $inceptionResult->getStdOutput()->getStyle()->error(sprintf('Baseline filename must have an extension, %s provided instead.', pathinfo($generateBaselineFile, PATHINFO_BASENAME))); - return $inceptionResult->handleReturn(1); + return $inceptionResult->handleReturn(1, null); } - if ($baselineExtension !== 'neon') { - $inceptionResult->getStdOutput()->getStyle()->error(sprintf('Baseline filename extension must be .neon, .%s was used instead.', $baselineExtension)); + if (!in_array($baselineExtension, ['neon', 'php'], true)) { + $inceptionResult->getStdOutput()->getStyle()->error(sprintf('Baseline filename extension must be .neon or .php, .%s was used instead.', $baselineExtension)); - return $inceptionResult->handleReturn(1); + return $inceptionResult->handleReturn(1, null); } } @@ -215,9 +225,49 @@ protected function execute(InputInterface $input, OutputInterface $output): int } catch (PathNotFoundException $e) { $inceptionResult->getErrorOutput()->writeLineFormatted(sprintf('%s', $e->getMessage())); return 1; + } catch (InceptionNotSuccessfulException) { + return 1; + } + + if (count($files) === 0) { + $bleedingEdge = (bool) $container->getParameter('featureToggles')['zeroFiles']; + if (!$bleedingEdge) { + $inceptionResult->getErrorOutput()->getStyle()->note('No files found to analyse.'); + $inceptionResult->getErrorOutput()->getStyle()->warning('This will cause a non-zero exit code in PHPStan 2.0.'); + + return $inceptionResult->handleReturn(0, null); + } + + $inceptionResult->getErrorOutput()->getStyle()->error('No files found to analyse.'); + + return $inceptionResult->handleReturn(1, null); } - /** @var AnalyseApplication $application */ + $analysedConfigFiles = array_intersect($files, $container->getParameter('allConfigFiles')); + /** @var RelativePathHelper $relativePathHelper */ + $relativePathHelper = $container->getService('relativePathHelper'); + foreach ($analysedConfigFiles as $analysedConfigFile) { + $fileSize = @filesize($analysedConfigFile); + if ($fileSize === false) { + continue; + } + + if ($fileSize <= 512 * 1024) { + continue; + } + + $inceptionResult->getErrorOutput()->getStyle()->warning(sprintf( + 'Configuration file %s (%s) is too big and might slow down PHPStan. Consider adding it to excludePaths.', + $relativePathHelper->getRelativePath($analysedConfigFile), + BytesHelper::bytes($fileSize), + )); + } + + if ($fix) { + return $this->runFixer($inceptionResult, $container, $onlyFiles, $input, $output, $files); + } + + /** @var AnalyseApplication $application */ $application = $container->getByType(AnalyseApplication::class); $debug = $input->getOption('debug'); @@ -267,192 +317,223 @@ protected function execute(InputInterface $input, OutputInterface $output): int $previous = $previous->getPrevious(); } - return $inceptionResult->handleReturn(1); + return $inceptionResult->handleReturn(1, null); } throw $t; } if ($generateBaselineFile !== null) { - if (!$allowEmptyBaseline && !$analysisResult->hasErrors()) { - $inceptionResult->getStdOutput()->getStyle()->error('No errors were found during the analysis. Baseline could not be generated.'); - $inceptionResult->getStdOutput()->writeLineFormatted('To allow generating empty baselines, pass --allow-empty-baseline option.'); - - return $inceptionResult->handleReturn(1); - } - if ($analysisResult->hasInternalErrors()) { - $inceptionResult->getStdOutput()->getStyle()->error('An internal error occurred. Baseline could not be generated. Re-run PHPStan without --generate-baseline to see what\'s going on.'); - - return $inceptionResult->handleReturn(1); - } + return $this->generateBaseline($generateBaselineFile, $inceptionResult, $analysisResult, $output, $allowEmptyBaseline, $baselineExtension, $failWithoutResultCache); + } - $baselineFileDirectory = dirname($generateBaselineFile); - $baselineErrorFormatter = new BaselineNeonErrorFormatter(new ParentDirectoryRelativePathHelper($baselineFileDirectory)); + /** @var ErrorFormatter $errorFormatter */ + $errorFormatter = $container->getService($errorFormatterServiceName); - $existingBaselineContent = is_file($generateBaselineFile) ? FileReader::read($generateBaselineFile) : ''; + $exitCode = $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput()); + if ($failWithoutResultCache && !$analysisResult->isResultCacheUsed()) { + $exitCode = 2; + } - $streamOutput = $this->createStreamOutput(); - $errorConsoleStyle = new ErrorsConsoleStyle(new StringInput(''), $streamOutput); - $baselineOutput = new SymfonyOutput($streamOutput, new SymfonyStyle($errorConsoleStyle)); - $baselineErrorFormatter->formatErrors($analysisResult, $baselineOutput, $existingBaselineContent); + if ( + $analysisResult->isResultCacheUsed() + && $analysisResult->isResultCacheSaved() + && !$onlyFiles + && $inceptionResult->getProjectConfigArray() !== null + ) { + $projectServicesNotInAnalysedPaths = array_values(array_unique($analysisResult->getChangedProjectExtensionFilesOutsideOfAnalysedPaths())); + $projectServiceFileNamesNotInAnalysedPaths = array_keys($analysisResult->getChangedProjectExtensionFilesOutsideOfAnalysedPaths()); + + if (count($projectServicesNotInAnalysedPaths) > 0) { + $one = count($projectServicesNotInAnalysedPaths) === 1; + $errorOutput->writeLineFormatted('Result cache might not behave correctly.'); + $errorOutput->writeLineFormatted(sprintf('You\'re using custom %s in your project config', $one ? 'extension' : 'extensions')); + $errorOutput->writeLineFormatted(sprintf('but %s not part of analysed paths:', $one ? 'this extension is' : 'these extensions are')); + $errorOutput->writeLineFormatted(''); + foreach ($projectServicesNotInAnalysedPaths as $service) { + $errorOutput->writeLineFormatted(sprintf('- %s', $service)); + } - $stream = $streamOutput->getStream(); - rewind($stream); - $baselineContents = stream_get_contents($stream); - if ($baselineContents === false) { - throw new ShouldNotHappenException(); - } + $errorOutput->writeLineFormatted(''); - if (!is_dir($baselineFileDirectory)) { - $mkdirResult = @mkdir($baselineFileDirectory, 0644, true); - if ($mkdirResult === false) { - $inceptionResult->getStdOutput()->writeLineFormatted(sprintf('Failed to create directory "%s".', $baselineFileDirectory)); + $errorOutput->writeLineFormatted('When you edit them and re-run PHPStan, the result cache will get stale.'); - return $inceptionResult->handleReturn(1); + $directoriesToAdd = []; + foreach ($projectServiceFileNamesNotInAnalysedPaths as $path) { + $directoriesToAdd[] = dirname($relativePathHelper->getRelativePath($path)); } - } - try { - FileWriter::write($generateBaselineFile, $baselineContents); - } catch (CouldNotWriteFileException $e) { - $inceptionResult->getStdOutput()->writeLineFormatted($e->getMessage()); + $directoriesToAdd = array_unique($directoriesToAdd); + $oneDirectory = count($directoriesToAdd) === 1; - return $inceptionResult->handleReturn(1); - } + $errorOutput->writeLineFormatted(sprintf('Add %s to your analysed paths to get rid of this problem:', $oneDirectory ? 'this directory' : 'these directories')); + + $errorOutput->writeLineFormatted(''); - $errorsCount = 0; - $unignorableCount = 0; - foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { - if (!$fileSpecificError->canBeIgnored()) { - $unignorableCount++; - if ($output->isVeryVerbose()) { - $inceptionResult->getStdOutput()->writeLineFormatted('Unignorable could not be added to the baseline:'); - $inceptionResult->getStdOutput()->writeLineFormatted($fileSpecificError->getMessage()); - $inceptionResult->getStdOutput()->writeLineFormatted($fileSpecificError->getFile()); - $inceptionResult->getStdOutput()->writeLineFormatted(''); - } - continue; + foreach ($directoriesToAdd as $directory) { + $errorOutput->writeLineFormatted(sprintf('- %s', $directory)); } - $errorsCount++; - } + $errorOutput->writeLineFormatted(''); - $message = sprintf('Baseline generated with %d %s.', $errorsCount, $errorsCount === 1 ? 'error' : 'errors'); + $bleedingEdge = (bool) $container->getParameter('featureToggles')['projectServicesNotInAnalysedPaths']; + if ($bleedingEdge) { + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes()); + } - if ( - $unignorableCount === 0 - && count($analysisResult->getNotFileSpecificErrors()) === 0 - ) { - $inceptionResult->getStdOutput()->getStyle()->success($message); - } else { - $inceptionResult->getStdOutput()->getStyle()->warning($message . "\nSome errors could not be put into baseline. Re-run PHPStan and fix them."); + $errorOutput->getStyle()->warning('This will cause a non-zero exit code in PHPStan 2.0.'); } + } - return $inceptionResult->handleReturn(0); + return $inceptionResult->handleReturn( + $exitCode, + $analysisResult->getPeakMemoryUsageBytes(), + ); + } + + private function createStreamOutput(): StreamOutput + { + $resource = fopen('php://memory', 'w', false); + if ($resource === false) { + throw new ShouldNotHappenException(); } + return new StreamOutput($resource); + } - if ($fix) { - $ciDetector = new CiDetector(); - if ($ciDetector->isCiDetected()) { - $inceptionResult->getStdOutput()->writeLineFormatted('PHPStan Pro can\'t run in CI environment yet. Stay tuned!'); + private function generateBaseline(string $generateBaselineFile, InceptionResult $inceptionResult, AnalysisResult $analysisResult, OutputInterface $output, bool $allowEmptyBaseline, string $baselineExtension, bool $failWithoutResultCache): int + { + if (!$allowEmptyBaseline && !$analysisResult->hasErrors()) { + $inceptionResult->getStdOutput()->getStyle()->error('No errors were found during the analysis. Baseline could not be generated.'); + $inceptionResult->getStdOutput()->writeLineFormatted('To allow generating empty baselines, pass --allow-empty-baseline option.'); - return $inceptionResult->handleReturn(1); - } - $container->getByType(ResultCacheClearer::class)->clearTemporaryCaches(); - $hasInternalErrors = $analysisResult->hasInternalErrors(); - $nonIgnorableErrorsByException = []; - foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { - if (!$fileSpecificError->hasNonIgnorableException()) { - continue; - } + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes()); + } + if ($analysisResult->hasInternalErrors()) { + $internalErrors = array_values(array_unique($analysisResult->getInternalErrors())); - $nonIgnorableErrorsByException[] = $fileSpecificError; + foreach ($internalErrors as $internalError) { + $inceptionResult->getStdOutput()->writeLineFormatted($internalError); + $inceptionResult->getStdOutput()->writeLineFormatted(''); } - if ($hasInternalErrors || count($nonIgnorableErrorsByException) > 0) { - $fixerAnalysisResult = new AnalysisResult( - $nonIgnorableErrorsByException, - $analysisResult->getInternalErrors(), - $analysisResult->getInternalErrors(), - [], - $analysisResult->getCollectedData(), - $analysisResult->isDefaultLevelUsed(), - $analysisResult->getProjectConfigFile(), - $analysisResult->isResultCacheSaved(), - ); + $inceptionResult->getStdOutput()->getStyle()->error(sprintf( + '%s occurred. Baseline could not be generated.', + count($internalErrors) === 1 ? 'An internal error' : 'Internal errors', + )); - $stdOutput = $inceptionResult->getStdOutput(); - $stdOutput->getStyle()->error('PHPStan Pro can\'t be launched because of these errors:'); + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes()); + } - /** @var TableErrorFormatter $tableErrorFormatter */ - $tableErrorFormatter = $container->getService('errorFormatter.table'); - $tableErrorFormatter->formatErrors($fixerAnalysisResult, $stdOutput); + foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { + if (!$fileSpecificError->hasNonIgnorableException()) { + continue; + } - $stdOutput->writeLineFormatted('Please fix them first and then re-run PHPStan.'); + $inceptionResult->getStdOutput()->getStyle()->error('An internal error occurred. Baseline could not be generated.'); - if ($stdOutput->isDebug()) { - $stdOutput->writeLineFormatted(sprintf('hasInternalErrors: %s', $hasInternalErrors ? 'true' : 'false')); - $stdOutput->writeLineFormatted(sprintf('nonIgnorableErrorsByExceptionCount: %d', count($nonIgnorableErrorsByException))); - } + $inceptionResult->getStdOutput()->writeLineFormatted($fileSpecificError->getMessage()); + $inceptionResult->getStdOutput()->writeLineFormatted($fileSpecificError->getFile()); + $inceptionResult->getStdOutput()->writeLineFormatted(''); - return $inceptionResult->handleReturn(1); - } + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes()); + } - if (!$analysisResult->isResultCacheSaved() && !$onlyFiles) { - // this can happen only if there are some regex-related errors in ignoreErrors configuration - $stdOutput = $inceptionResult->getStdOutput(); - if (count($analysisResult->getFileSpecificErrors()) > 0) { - $stdOutput->getStyle()->error('Unknown error. Please report this as a bug.'); - return $inceptionResult->handleReturn(1); - } + $streamOutput = $this->createStreamOutput(); + $errorConsoleStyle = new ErrorsConsoleStyle(new StringInput(''), $streamOutput); + $baselineOutput = new SymfonyOutput($streamOutput, new SymfonyStyle($errorConsoleStyle)); + $baselineFileDirectory = dirname($generateBaselineFile); + $baselinePathHelper = new ParentDirectoryRelativePathHelper($baselineFileDirectory); + + if ($baselineExtension === 'php') { + $baselineErrorFormatter = new BaselinePhpErrorFormatter($baselinePathHelper); + $baselineErrorFormatter->formatErrors($analysisResult, $baselineOutput); + } else { + $baselineErrorFormatter = new BaselineNeonErrorFormatter($baselinePathHelper); + $existingBaselineContent = is_file($generateBaselineFile) ? FileReader::read($generateBaselineFile) : ''; + $baselineErrorFormatter->formatErrors($analysisResult, $baselineOutput, $existingBaselineContent); + } + + $stream = $streamOutput->getStream(); + rewind($stream); + $baselineContents = stream_get_contents($stream); + if ($baselineContents === false) { + throw new ShouldNotHappenException(); + } - $stdOutput->getStyle()->error('PHPStan Pro can\'t be launched because of these errors:'); + try { + DirectoryCreator::ensureDirectoryExists($baselineFileDirectory, 0644); + } catch (DirectoryCreatorException $e) { + $inceptionResult->getStdOutput()->writeLineFormatted($e->getMessage()); - /** @var TableErrorFormatter $tableErrorFormatter */ - $tableErrorFormatter = $container->getService('errorFormatter.table'); - $tableErrorFormatter->formatErrors($analysisResult, $stdOutput); + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes()); + } - $stdOutput->writeLineFormatted('Please fix them first and then re-run PHPStan.'); + try { + FileWriter::write($generateBaselineFile, $baselineContents); + } catch (CouldNotWriteFileException $e) { + $inceptionResult->getStdOutput()->writeLineFormatted($e->getMessage()); - if ($stdOutput->isDebug()) { - $stdOutput->writeLineFormatted('Result cache was not saved.'); - } + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes()); + } - return $inceptionResult->handleReturn(1); + $errorsCount = 0; + $unignorableCount = 0; + foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { + if (!$fileSpecificError->canBeIgnored()) { + $unignorableCount++; + if ($output->isVeryVerbose()) { + $inceptionResult->getStdOutput()->writeLineFormatted('Unignorable could not be added to the baseline:'); + $inceptionResult->getStdOutput()->writeLineFormatted($fileSpecificError->getMessage()); + $inceptionResult->getStdOutput()->writeLineFormatted($fileSpecificError->getFile()); + $inceptionResult->getStdOutput()->writeLineFormatted(''); + } + continue; } - $inceptionResult->handleReturn(0); + $errorsCount++; + } - /** @var FixerApplication $fixerApplication */ - $fixerApplication = $container->getByType(FixerApplication::class); + $message = sprintf('Baseline generated with %d %s.', $errorsCount, $errorsCount === 1 ? 'error' : 'errors'); - return $fixerApplication->run( - $inceptionResult->getProjectConfigFile(), - $inceptionResult, - $input, - $output, - $analysisResult->getFileSpecificErrors(), - $analysisResult->getNotFileSpecificErrors(), - count($files), - $_SERVER['argv'][0], - ); + if ( + $unignorableCount === 0 + && count($analysisResult->getNotFileSpecificErrors()) === 0 + ) { + $inceptionResult->getStdOutput()->getStyle()->success($message); + } else { + $inceptionResult->getStdOutput()->getStyle()->warning($message . "\nSome errors could not be put into baseline. Re-run PHPStan and fix them."); } - /** @var ErrorFormatter $errorFormatter */ - $errorFormatter = $container->getService($errorFormatterServiceName); + $exitCode = 0; + if ($failWithoutResultCache && !$analysisResult->isResultCacheUsed()) { + $exitCode = 2; + } - return $inceptionResult->handleReturn( - $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput()), - ); + return $inceptionResult->handleReturn($exitCode, $analysisResult->getPeakMemoryUsageBytes()); } - private function createStreamOutput(): StreamOutput + /** + * @param string[] $files + */ + private function runFixer(InceptionResult $inceptionResult, Container $container, bool $onlyFiles, InputInterface $input, OutputInterface $output, array $files): int { - $resource = fopen('php://memory', 'w', false); - if ($resource === false) { - throw new ShouldNotHappenException(); + $ciDetector = new CiDetector(); + if ($ciDetector->isCiDetected()) { + $inceptionResult->getStdOutput()->writeLineFormatted('PHPStan Pro can\'t run in CI environment yet. Stay tuned!'); + + return $inceptionResult->handleReturn(1, null); } - return new StreamOutput($resource); + + /** @var FixerApplication $fixerApplication */ + $fixerApplication = $container->getByType(FixerApplication::class); + + return $fixerApplication->run( + $inceptionResult->getProjectConfigFile(), + $input, + $output, + count($files), + $_SERVER['argv'][0], + ); } } diff --git a/src/Command/AnalyserRunner.php b/src/Command/AnalyserRunner.php index 627c4f4ac5..da6eb42962 100644 --- a/src/Command/AnalyserRunner.php +++ b/src/Command/AnalyserRunner.php @@ -8,12 +8,13 @@ use PHPStan\Parallel\ParallelAnalyser; use PHPStan\Parallel\Scheduler; use PHPStan\Process\CpuCoreCounter; +use PHPStan\ShouldNotHappenException; +use React\EventLoop\StreamSelectLoop; use Symfony\Component\Console\Input\InputInterface; -use function array_filter; -use function array_values; use function count; use function function_exists; use function is_file; +use function memory_get_peak_usage; class AnalyserRunner { @@ -41,14 +42,12 @@ public function runAnalyser( bool $debug, bool $allowParallel, ?string $projectConfigFile, - ?string $tmpFile, - ?string $insteadOfFile, InputInterface $input, ): AnalyserResult { $filesCount = count($files); if ($filesCount === 0) { - return new AnalyserResult([], [], [], [], [], false); + return new AnalyserResult([], [], [], [], [], [], [], [], [], [], false, memory_get_peak_usage(true)); } $schedule = $this->scheduler->scheduleWork($this->cpuCoreCounter->getNumberOfCpuCores(), $files); @@ -64,39 +63,26 @@ public function runAnalyser( && $mainScript !== null && $schedule->getNumberOfProcesses() > 0 ) { - return $this->parallelAnalyser->analyse($schedule, $mainScript, $postFileCallback, $projectConfigFile, $tmpFile, $insteadOfFile, $input); + $loop = new StreamSelectLoop(); + $result = null; + $promise = $this->parallelAnalyser->analyse($loop, $schedule, $mainScript, $postFileCallback, $projectConfigFile, $input, null); + $promise->then(static function (AnalyserResult $tmp) use (&$result): void { + $result = $tmp; + }); + $loop->run(); + if ($result === null) { + throw new ShouldNotHappenException(); + } + return $result; } return $this->analyser->analyse( - $this->switchTmpFile($files, $insteadOfFile, $tmpFile), + $files, $preFileCallback, $postFileCallback, $debug, - $this->switchTmpFile($allAnalysedFiles, $insteadOfFile, $tmpFile), + $allAnalysedFiles, ); } - /** - * @param string[] $analysedFiles - * @return string[] - */ - private function switchTmpFile( - array $analysedFiles, - ?string $insteadOfFile, - ?string $tmpFile, - ): array - { - $analysedFiles = array_values(array_filter($analysedFiles, static function (string $file) use ($insteadOfFile): bool { - if ($insteadOfFile === null) { - return true; - } - return $file !== $insteadOfFile; - })); - if ($tmpFile !== null) { - $analysedFiles[] = $tmpFile; - } - - return $analysedFiles; - } - } diff --git a/src/Command/AnalysisResult.php b/src/Command/AnalysisResult.php index 7cad69c01f..02c7de64b1 100644 --- a/src/Command/AnalysisResult.php +++ b/src/Command/AnalysisResult.php @@ -11,15 +11,16 @@ class AnalysisResult { - /** @var Error[] sorted by their file name, line number and message */ + /** @var list sorted by their file name, line number and message */ private array $fileSpecificErrors; /** - * @param Error[] $fileSpecificErrors - * @param string[] $notFileSpecificErrors - * @param string[] $internalErrors - * @param string[] $warnings - * @param CollectedData[] $collectedData + * @param list $fileSpecificErrors + * @param list $notFileSpecificErrors + * @param list $internalErrors + * @param list $warnings + * @param list $collectedData + * @param array $changedProjectExtensionFilesOutsideOfAnalysedPaths */ public function __construct( array $fileSpecificErrors, @@ -30,6 +31,9 @@ public function __construct( private bool $defaultLevelUsed, private ?string $projectConfigFile, private bool $savedResultCache, + private int $peakMemoryUsageBytes, + private bool $isResultCacheUsed, + private array $changedProjectExtensionFilesOutsideOfAnalysedPaths, ) { usort( @@ -59,7 +63,7 @@ public function getTotalErrorsCount(): int } /** - * @return Error[] sorted by their file name, line number and message + * @return list sorted by their file name, line number and message */ public function getFileSpecificErrors(): array { @@ -67,7 +71,7 @@ public function getFileSpecificErrors(): array } /** - * @return string[] + * @return list */ public function getNotFileSpecificErrors(): array { @@ -75,7 +79,7 @@ public function getNotFileSpecificErrors(): array } /** - * @return string[] + * @return list */ public function getInternalErrors(): array { @@ -83,7 +87,7 @@ public function getInternalErrors(): array } /** - * @return string[] + * @return list */ public function getWarnings(): array { @@ -96,7 +100,7 @@ public function hasWarnings(): bool } /** - * @return CollectedData[] + * @return list */ public function getCollectedData(): array { @@ -123,4 +127,22 @@ public function isResultCacheSaved(): bool return $this->savedResultCache; } + public function getPeakMemoryUsageBytes(): int + { + return $this->peakMemoryUsageBytes; + } + + public function isResultCacheUsed(): bool + { + return $this->isResultCacheUsed; + } + + /** + * @return array + */ + public function getChangedProjectExtensionFilesOutsideOfAnalysedPaths(): array + { + return $this->changedProjectExtensionFilesOutsideOfAnalysedPaths; + } + } diff --git a/src/Command/ClearResultCacheCommand.php b/src/Command/ClearResultCacheCommand.php index 5e9bc7b87c..30c59100ec 100644 --- a/src/Command/ClearResultCacheCommand.php +++ b/src/Command/ClearResultCacheCommand.php @@ -8,6 +8,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use function is_bool; use function is_string; class ClearResultCacheCommand extends Command @@ -34,6 +35,7 @@ protected function configure(): void new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), new InputOption('debug', null, InputOption::VALUE_NONE, 'Show debug information - which file is analysed, do not catch internal errors'), new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for clearing result cache'), + new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with Xdebug for debugging purposes'), ]); } @@ -55,11 +57,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int $configuration = $input->getOption('configuration'); $memoryLimit = $input->getOption('memory-limit'); $debugEnabled = (bool) $input->getOption('debug'); + $allowXdebug = $input->getOption('xdebug'); if ( (!is_string($autoloadFile) && $autoloadFile !== null) || (!is_string($configuration) && $configuration !== null) || (!is_string($memoryLimit) && $memoryLimit !== null) + || (!is_bool($allowXdebug)) ) { throw new ShouldNotHappenException(); } @@ -68,14 +72,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $inceptionResult = CommandHelper::begin( $input, $output, - ['.'], + [], $memoryLimit, $autoloadFile, $this->composerAutoloaderProjectPaths, $configuration, null, '0', - false, + $allowXdebug, $debugEnabled, ); } catch (InceptionNotSuccessfulException) { @@ -84,7 +88,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $container = $inceptionResult->getContainer(); - /** @var ResultCacheClearer $resultCacheClearer */ $resultCacheClearer = $container->getByType(ResultCacheClearer::class); $path = $resultCacheClearer->clear(); diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 8c277003eb..32fd7bae98 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -2,7 +2,6 @@ namespace PHPStan\Command; -use Closure; use Composer\XdebugHandler\XdebugHandler; use Nette\DI\Helpers; use Nette\DI\InvalidConfigurationException; @@ -12,6 +11,7 @@ use Nette\Schema\ValidationException; use Nette\Utils\AssertionException; use Nette\Utils\Strings; +use PHPStan\Analyser\MutatingScope; use PHPStan\Command\Symfony\SymfonyOutput; use PHPStan\Command\Symfony\SymfonyStyle; use PHPStan\DependencyInjection\Container; @@ -20,22 +20,29 @@ use PHPStan\DependencyInjection\InvalidIgnoredErrorPatternsException; use PHPStan\DependencyInjection\LoaderFactory; use PHPStan\ExtensionInstaller\GeneratedConfig; +use PHPStan\File\FileExcluder; use PHPStan\File\FileFinder; use PHPStan\File\FileHelper; +use PHPStan\Internal\DirectoryCreator; +use PHPStan\Internal\DirectoryCreatorException; +use PHPStan\PhpDoc\StubFilesProvider; use PHPStan\ShouldNotHappenException; use ReflectionClass; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Throwable; +use function array_filter; use function array_key_exists; use function array_map; +use function array_values; use function class_exists; use function count; use function dirname; use function error_get_last; use function get_class; use function getcwd; +use function getenv; use function gettype; use function implode; use function ini_get; @@ -44,12 +51,11 @@ use function is_file; use function is_readable; use function is_string; -use function mkdir; use function register_shutdown_function; use function spl_autoload_functions; use function sprintf; +use function str_contains; use function str_repeat; -use function strpos; use function sys_get_temp_dir; use const DIRECTORY_SEPARATOR; use const E_ERROR; @@ -80,11 +86,16 @@ public static function begin( ?string $level, bool $allowXdebug, bool $debugEnabled = false, - ?string $singleReflectionFile = null, - ?string $singleReflectionInsteadOfFile = null, bool $cleanupContainerCache = true, ): InceptionResult { + $stdOutput = new SymfonyOutput($output, new SymfonyStyle(new ErrorsConsoleStyle($input, $output))); + + $errorOutput = (static function () use ($input, $output): Output { + $symfonyErrorOutput = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output; + return new SymfonyOutput($symfonyErrorOutput, new SymfonyStyle(new ErrorsConsoleStyle($input, $symfonyErrorOutput))); + })(); + if (!$allowXdebug) { $xdebug = new XdebugHandler('phpstan'); $xdebug->setPersistent(); @@ -92,13 +103,25 @@ public static function begin( unset($xdebug); } - $stdOutput = new SymfonyOutput($output, new SymfonyStyle(new ErrorsConsoleStyle($input, $output))); + if ($allowXdebug) { + if (!XdebugHandler::isXdebugActive()) { + $errorOutput->getStyle()->note('You are running with "--xdebug" enabled, but the Xdebug PHP extension is not active. The process will not halt at breakpoints.'); + } else { + $errorOutput->getStyle()->note("You are running with \"--xdebug\" enabled, and the Xdebug PHP extension is active.\nThe process will halt at breakpoints, but PHPStan will run much slower.\nUse this only if you are debugging PHPStan itself or your custom extensions."); + } + } elseif (XdebugHandler::isXdebugActive()) { + $errorOutput->getStyle()->note('The Xdebug PHP extension is active, but "--xdebug" is not used. This may slow down performance and the process will not halt at breakpoints.'); + } elseif ($debugEnabled) { + $v = XdebugHandler::getSkippedVersion(); + if ($v !== '') { + $errorOutput->getStyle()->note( + "The Xdebug PHP extension is active, but \"--xdebug\" is not used.\n" . + "The process was restarted and it will not halt at breakpoints.\n" . + 'Use "--xdebug" if you want to halt at breakpoints.', + ); + } + } - /** @var Output $errorOutput */ - $errorOutput = (static function () use ($input, $output): Output { - $symfonyErrorOutput = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output; - return new SymfonyOutput($symfonyErrorOutput, new SymfonyStyle(new ErrorsConsoleStyle($input, $symfonyErrorOutput))); - })(); if ($memoryLimit !== null) { if (Strings::match($memoryLimit, '#^-?\d+[kMG]?$#i') === null) { $errorOutput->writeLineFormatted(sprintf('Invalid memory limit format "%s".', $memoryLimit)); @@ -121,7 +144,7 @@ public static function begin( return; } - if (strpos($error['message'], 'Allowed memory size') === false) { + if (!str_contains($error['message'], 'Allowed memory size')) { return; } @@ -201,6 +224,7 @@ public static function begin( $defaultParameters = [ 'rootDir' => $containerFactory->getRootDirectory(), 'currentWorkingDirectory' => $containerFactory->getCurrentWorkingDirectory(), + 'env' => getenv(), ]; if (isset($projectConfig['parameters']['tmpDir'])) { @@ -265,8 +289,10 @@ public static function begin( } $createDir = static function (string $path) use ($errorOutput): void { - if (!is_dir($path) && !@mkdir($path, 0777) && !is_dir($path)) { - $errorOutput->writeLineFormatted(sprintf('Cannot create a temp directory %s', $path)); + try { + DirectoryCreator::ensureDirectoryExists($path, 0777); + } catch (DirectoryCreatorException $e) { + $errorOutput->writeLineFormatted($e->getMessage()); throw new InceptionNotSuccessfulException(); } }; @@ -277,7 +303,7 @@ public static function begin( } try { - $container = $containerFactory->create($tmpDir, $additionalConfigFiles, $paths, $composerAutoloaderProjectPaths, $analysedPathsFromConfig, $level ?? self::DEFAULT_LEVEL, $generateBaselineFile, $autoloadFile, $singleReflectionFile, $singleReflectionInsteadOfFile); + $container = $containerFactory->create($tmpDir, $additionalConfigFiles, $paths, $composerAutoloaderProjectPaths, $analysedPathsFromConfig, $level ?? self::DEFAULT_LEVEL, $generateBaselineFile, $autoloadFile); } catch (InvalidConfigurationException | AssertionException $e) { $errorOutput->writeLineFormatted('Invalid configuration:'); $errorOutput->writeLineFormatted($e->getMessage()); @@ -345,11 +371,6 @@ public static function begin( $containerFactory->clearOldContainers($tmpDir); } - if (count($paths) === 0) { - $errorOutput->writeLineFormatted('At least one path must be specified to analyse.'); - throw new InceptionNotSuccessfulException(); - } - /** @var bool|null $customRulesetUsed */ $customRulesetUsed = $container->getParameter('customRulesetUsed'); if ($customRulesetUsed === null) { @@ -452,6 +473,56 @@ public static function begin( $errorOutput->writeLineFormatted(sprintf('Parameter excludes_analyse has been deprecated so use excludePaths only from now on.')); } + if ($container->hasParameter('scopeClass') && $container->getParameter('scopeClass') !== MutatingScope::class) { + $errorOutput->writeLineFormatted('⚠️ You\'re using a deprecated config option scopeClass. ⚠️️'); + $errorOutput->writeLineFormatted(''); + $errorOutput->writeLineFormatted(sprintf('Please implement PHPStan\Type\ExpressionTypeResolverExtension interface instead and register it as a service.')); + } + + if ($projectConfig !== null) { + $parameters = $projectConfig['parameters'] ?? []; + /** @var bool $checkMissingIterableValueType */ + $checkMissingIterableValueType = $parameters['checkMissingIterableValueType'] ?? true; + if (!$checkMissingIterableValueType) { + $errorOutput->writeLineFormatted('⚠️ You\'re using a deprecated config option checkMissingIterableValueType ⚠️️'); + $errorOutput->writeLineFormatted(''); + + $featureToggles = $container->getParameter('featureToggles'); + if (!((bool) $featureToggles['bleedingEdge'])) { + $errorOutput->writeLineFormatted('It\'s strongly recommended to remove it from your configuration file'); + $errorOutput->writeLineFormatted('and add the missing array typehints.'); + $errorOutput->writeLineFormatted(''); + } + + $errorOutput->writeLineFormatted('If you want to continue ignoring missing typehints from arrays,'); + $errorOutput->writeLineFormatted('add missingType.iterableValue error identifier to your ignoreErrors:'); + $errorOutput->writeLineFormatted(''); + $errorOutput->writeLineFormatted('parameters:'); + $errorOutput->writeLineFormatted("\tignoreErrors:"); + $errorOutput->writeLineFormatted("\t\t-"); + $errorOutput->writeLineFormatted("\t\t\tidentifier: missingType.iterableValue"); + $errorOutput->writeLineFormatted(''); + } + + /** @var bool $checkGenericClassInNonGenericObjectType */ + $checkGenericClassInNonGenericObjectType = $parameters['checkGenericClassInNonGenericObjectType'] ?? true; + if (!$checkGenericClassInNonGenericObjectType) { + $errorOutput->writeLineFormatted('⚠️ You\'re using a deprecated config option checkGenericClassInNonGenericObjectType ⚠️️'); + $errorOutput->writeLineFormatted(''); + $errorOutput->writeLineFormatted('It\'s strongly recommended to remove it from your configuration file'); + $errorOutput->writeLineFormatted('and add the missing generic typehints.'); + $errorOutput->writeLineFormatted(''); + $errorOutput->writeLineFormatted('If you want to continue ignoring missing typehints from generics,'); + $errorOutput->writeLineFormatted('add missingType.generics error identifier to your ignoreErrors:'); + $errorOutput->writeLineFormatted(''); + $errorOutput->writeLineFormatted('parameters:'); + $errorOutput->writeLineFormatted("\tignoreErrors:"); + $errorOutput->writeLineFormatted("\t\t-"); + $errorOutput->writeLineFormatted("\t\t\tidentifier: missingType.generics"); + $errorOutput->writeLineFormatted(''); + } + } + $tempResultCachePath = $container->getParameter('tempResultCachePath'); $createDir($tempResultCachePath); @@ -460,13 +531,22 @@ public static function begin( $pathRoutingParser = $container->getService('pathRoutingParser'); - /** @var Closure(): array{string[], bool} $filesCallback */ - $filesCallback = static function () use ($fileFinder, $pathRoutingParser, $paths): array { + $stubFilesProvider = $container->getByType(StubFilesProvider::class); + + $filesCallback = static function () use ($currentWorkingDirectoryFileHelper, $stubFilesProvider, $fileFinder, $pathRoutingParser, $paths, $errorOutput): array { + if (count($paths) === 0) { + $errorOutput->writeLineFormatted('At least one path must be specified to analyse.'); + throw new InceptionNotSuccessfulException(); + } $fileFinderResult = $fileFinder->findFiles($paths); $files = $fileFinderResult->getFiles(); $pathRoutingParser->setAnalysedFiles($files); + $stubFilesExcluder = new FileExcluder($currentWorkingDirectoryFileHelper, $stubFilesProvider->getProjectStubFiles()); + + $files = array_values(array_filter($files, static fn (string $file) => !$stubFilesExcluder->isExcludedFromAnalysing($file))); + return [$files, $fileFinderResult->isOnlyFiles()]; }; diff --git a/src/Command/DumpParametersCommand.php b/src/Command/DumpParametersCommand.php index 33e12a7664..50e6e57b4b 100644 --- a/src/Command/DumpParametersCommand.php +++ b/src/Command/DumpParametersCommand.php @@ -3,6 +3,7 @@ namespace PHPStan\Command; use Nette\Neon\Neon; +use Nette\Utils\Json; use PHPStan\ShouldNotHappenException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -35,6 +36,7 @@ protected function configure(): void new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), new InputOption('debug', null, InputOption::VALUE_NONE, 'Show debug information - which file is analysed, do not catch internal errors'), new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for clearing result cache'), + new InputOption('json', null, InputOption::VALUE_NONE, 'Dump parameters as JSON instead of NEON'), ]); } @@ -56,6 +58,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $autoloadFile = $input->getOption('autoload-file'); $configuration = $input->getOption('configuration'); $level = $input->getOption(AnalyseCommand::OPTION_LEVEL); + $json = (bool) $input->getOption('json'); if ( (!is_string($memoryLimit) && $memoryLimit !== null) @@ -70,7 +73,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $inceptionResult = CommandHelper::begin( $input, $output, - ['.'], + [], $memoryLimit, $autoloadFile, $this->composerAutoloaderProjectPaths, @@ -92,11 +95,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int unset($parameters['productionMode']); unset($parameters['tempDir']); unset($parameters['__validate']); - // internal - static reflection - unset($parameters['singleReflectionFile']); - unset($parameters['singleReflectionInsteadOfFile']); - $output->writeln(Neon::encode($parameters, true)); + if ($json) { + $encoded = Json::encode($parameters, Json::PRETTY); + } else { + $encoded = Neon::encode($parameters, true); + } + + $output->writeln($encoded); return 0; } diff --git a/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php b/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php new file mode 100644 index 0000000000..0af50dba47 --- /dev/null +++ b/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php @@ -0,0 +1,105 @@ +hasErrors()) { + $php = 'writeRaw($php); + return 0; + } + + $fileErrors = []; + foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { + if (!$fileSpecificError->canBeIgnored()) { + continue; + } + $fileErrors['/' . $this->relativePathHelper->getRelativePath($fileSpecificError->getFilePath())][] = $fileSpecificError; + } + ksort($fileErrors, SORT_STRING); + + $php = ' $errors) { + $fileErrorsByMessage = []; + foreach ($errors as $error) { + $errorMessage = $error->getMessage(); + if (!isset($fileErrorsByMessage[$errorMessage])) { + $fileErrorsByMessage[$errorMessage] = [ + 1, + $error->getIdentifier() !== null ? [$error->getIdentifier() => true] : [], + ]; + continue; + } + + $fileErrorsByMessage[$errorMessage][0]++; + + if ($error->getIdentifier() === null) { + continue; + } + $fileErrorsByMessage[$errorMessage][1][$error->getIdentifier()] = true; + } + ksort($fileErrorsByMessage, SORT_STRING); + + foreach ($fileErrorsByMessage as $message => [$count, $identifiersInKeys]) { + $identifiers = array_keys($identifiersInKeys); + sort($identifiers); + $identifiersComment = ''; + if (count($identifiers) > 0) { + if (count($identifiers) === 1) { + $identifiersComment = "\n\t// identifier: " . $identifiers[0]; + } else { + $identifiersComment = "\n\t// identifiers: " . implode(', ', $identifiers); + } + } + + $php .= sprintf( + "\$ignoreErrors[] = [%s\n\t'message' => %s,\n\t'count' => %d,\n\t'path' => __DIR__ . %s,\n];\n", + $identifiersComment, + var_export(Helpers::escape('#^' . preg_quote($message, '#') . '$#'), true), + var_export($count, true), + var_export(Helpers::escape($file), true), + ); + } + } + + $php .= "\n"; + $php .= 'return [\'parameters\' => [\'ignoreErrors\' => $ignoreErrors]];'; + $php .= "\n"; + + $output->writeRaw($php); + + return 1; + } + +} diff --git a/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php b/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php index 33eb65bb37..cfb2ddc559 100644 --- a/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php +++ b/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php @@ -38,9 +38,10 @@ public function formatErrors( foreach ($errors as $error) { $output->writeRaw(sprintf( - ' ', + ' ', $this->escape((string) $error->getLine()), $this->escape($error->getMessage()), + $error->getIdentifier() !== null ? sprintf(' source="%s"', $this->escape($error->getIdentifier())) : '', )); $output->writeLineFormatted(''); } diff --git a/src/Command/ErrorFormatter/ErrorFormatter.php b/src/Command/ErrorFormatter/ErrorFormatter.php index cc4b48df3a..2d4e17ee31 100644 --- a/src/Command/ErrorFormatter/ErrorFormatter.php +++ b/src/Command/ErrorFormatter/ErrorFormatter.php @@ -5,7 +5,20 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; -/** @api */ +/** + * This is the interface custom error formatters implement. Register it in the configuration file + * like this: + * + * ``` + * services: + * errorFormatter.myFormat: + * class: App\PHPStan\AwesomeErrorFormatter + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/error-formatters + * + * @api + */ interface ErrorFormatter { diff --git a/src/Command/ErrorFormatter/JsonErrorFormatter.php b/src/Command/ErrorFormatter/JsonErrorFormatter.php index 5139a87554..9f9f1edcfb 100644 --- a/src/Command/ErrorFormatter/JsonErrorFormatter.php +++ b/src/Command/ErrorFormatter/JsonErrorFormatter.php @@ -5,6 +5,7 @@ use Nette\Utils\Json; use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; +use Symfony\Component\Console\Formatter\OutputFormatter; use function array_key_exists; use function count; @@ -26,6 +27,8 @@ public function formatErrors(AnalysisResult $analysisResult, Output $output): in 'errors' => [], ]; + $tipFormatter = new OutputFormatter(false); + foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { $file = $fileSpecificError->getFile(); if (!array_key_exists($file, $errorsArray['files'])) { @@ -43,7 +46,11 @@ public function formatErrors(AnalysisResult $analysisResult, Output $output): in ]; if ($fileSpecificError->getTip() !== null) { - $message['tip'] = $fileSpecificError->getTip(); + $message['tip'] = $tipFormatter->format($fileSpecificError->getTip()); + } + + if ($fileSpecificError->getIdentifier() !== null) { + $message['identifier'] = $fileSpecificError->getIdentifier(); } $errorsArray['files'][$file]['messages'][] = $message; diff --git a/src/Command/ErrorFormatter/TableErrorFormatter.php b/src/Command/ErrorFormatter/TableErrorFormatter.php index c4a489b23a..16d10956d3 100644 --- a/src/Command/ErrorFormatter/TableErrorFormatter.php +++ b/src/Command/ErrorFormatter/TableErrorFormatter.php @@ -9,10 +9,16 @@ use PHPStan\File\RelativePathHelper; use PHPStan\File\SimpleRelativePathHelper; use Symfony\Component\Console\Formatter\OutputFormatter; +use function array_key_exists; use function array_map; use function count; +use function explode; +use function getenv; +use function in_array; use function is_string; +use function ltrim; use function sprintf; +use function str_contains; use function str_replace; class TableErrorFormatter implements ErrorFormatter @@ -24,6 +30,7 @@ public function __construct( private CiDetectedErrorFormatter $ciDetectedErrorFormatter, private bool $showTipsOfTheDay, private ?string $editorUrl, + private ?string $editorUrlTitle, ) { } @@ -44,6 +51,7 @@ public function formatErrors( if (!$analysisResult->hasErrors() && !$analysisResult->hasWarnings()) { $style->success('No errors'); + if ($this->showTipsOfTheDay) { if ($analysisResult->isDefaultLevelUsed()) { $output->writeLineFormatted('💡 Tip of the Day:'); @@ -61,34 +69,82 @@ public function formatErrors( /** @var array $fileErrors */ $fileErrors = []; + $outputIdentifiers = $output->isVerbose(); + $outputIdentifiersInFile = []; foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { if (!isset($fileErrors[$fileSpecificError->getFile()])) { $fileErrors[$fileSpecificError->getFile()] = []; } $fileErrors[$fileSpecificError->getFile()][] = $fileSpecificError; + if ($outputIdentifiers) { + continue; + } + + $filePath = $fileSpecificError->getTraitFilePath() ?? $fileSpecificError->getFilePath(); + if (array_key_exists($filePath, $outputIdentifiersInFile)) { + continue; + } + + if ($fileSpecificError->getIdentifier() === null) { + continue; + } + + if (!in_array($fileSpecificError->getIdentifier(), [ + 'ignore.unmatchedIdentifier', + 'ignore.parseError', + 'ignore.unmatched', + ], true)) { + continue; + } + + $outputIdentifiersInFile[$filePath] = true; } foreach ($fileErrors as $file => $errors) { $rows = []; foreach ($errors as $error) { $message = $error->getMessage(); + $filePath = $error->getTraitFilePath() ?? $error->getFilePath(); + if (($outputIdentifiers || array_key_exists($filePath, $outputIdentifiersInFile)) && $error->getIdentifier() !== null && $error->canBeIgnored()) { + $message .= "\n"; + $message .= '🪪 ' . $error->getIdentifier(); + } if ($error->getTip() !== null) { $tip = $error->getTip(); $tip = str_replace('%configurationFile%', $projectConfigFile, $tip); - $message .= "\n💡 " . $tip; + + $message .= "\n"; + if (str_contains($tip, "\n")) { + $lines = explode("\n", $tip); + foreach ($lines as $line) { + $message .= '💡 ' . ltrim($line, ' •') . "\n"; + } + } else { + $message .= '💡 ' . $tip; + } } if (is_string($this->editorUrl)) { - $editorFile = $error->getTraitFilePath() ?? $error->getFilePath(); $url = str_replace( ['%file%', '%relFile%', '%line%'], - [$editorFile, $this->simpleRelativePathHelper->getRelativePath($editorFile), (string) $error->getLine()], + [$filePath, $this->simpleRelativePathHelper->getRelativePath($filePath), (string) $error->getLine()], $this->editorUrl, ); - $message .= "\n✏️ ' . $this->relativePathHelper->getRelativePath($editorFile) . ''; + + if (is_string($this->editorUrlTitle)) { + $title = str_replace( + ['%file%', '%relFile%', '%line%'], + [$filePath, $this->simpleRelativePathHelper->getRelativePath($filePath), (string) $error->getLine()], + $this->editorUrlTitle, + ); + } else { + $title = $this->relativePathHelper->getRelativePath($filePath); + } + + $message .= "\n✏️ ' . $title . ''; } $rows[] = [ - (string) $error->getLine(), + $this->formatLineNumber($error->getLine()), $message, ]; } @@ -119,4 +175,18 @@ public function formatErrors( return $analysisResult->getTotalErrorsCount() > 0 ? 1 : 0; } + private function formatLineNumber(?int $lineNumber): string + { + if ($lineNumber === null) { + return ''; + } + + $isRunningInVSCodeTerminal = getenv('TERM_PROGRAM') === 'vscode'; + if ($isRunningInVSCodeTerminal) { + return ':' . $lineNumber; + } + + return (string) $lineNumber; + } + } diff --git a/src/Command/ErrorsConsoleStyle.php b/src/Command/ErrorsConsoleStyle.php index 76954e735f..520ce7add3 100644 --- a/src/Command/ErrorsConsoleStyle.php +++ b/src/Command/ErrorsConsoleStyle.php @@ -101,12 +101,26 @@ private function wrap(array $rows, int $terminalWidth, int $maxHeaderWidth): arr if (str_starts_with($columnRow, '✏️')) { continue; } - $columnRows[$k] = wordwrap( + $wrapped = wordwrap( $columnRow, $terminalWidth - $maxHeaderWidth - 5, - "\n", - true, ); + if (str_starts_with($columnRow, '💡 ')) { + $wrappedLines = explode("\n", $wrapped); + $newWrappedLines = []; + foreach ($wrappedLines as $l => $line) { + if ($l === 0) { + $newWrappedLines[] = $line; + continue; + } + + $newWrappedLines[] = ' ' . $line; + } + $columnRows[$k] = implode("\n", $newWrappedLines); + } else { + $columnRows[$k] = $wrapped; + } + } $rows[$i] = implode("\n", $columnRows); @@ -119,6 +133,11 @@ public function createProgressBar(int $max = 0): ProgressBar { $this->progressBar = parent::createProgressBar($max); + $format = $this->getProgressBarFormat(); + if ($format !== null) { + $this->progressBar->setFormat($format); + } + $ci = $this->isCiDetected(); $this->progressBar->setOverwrite(!$ci); @@ -136,6 +155,31 @@ public function createProgressBar(int $max = 0): ProgressBar return $this->progressBar; } + private function getProgressBarFormat(): ?string + { + switch ($this->getVerbosity()) { + case OutputInterface::VERBOSITY_NORMAL: + $formatName = ProgressBar::FORMAT_NORMAL; + break; + case OutputInterface::VERBOSITY_VERBOSE: + $formatName = ProgressBar::FORMAT_VERBOSE; + break; + case OutputInterface::VERBOSITY_VERY_VERBOSE: + case OutputInterface::VERBOSITY_DEBUG: + $formatName = ProgressBar::FORMAT_VERY_VERBOSE; + break; + default: + $formatName = null; + break; + } + + if ($formatName === null) { + return null; + } + + return ProgressBar::getFormatDefinition($formatName); + } + public function progressStart(int $max = 0): void { if (!$this->showProgress) { diff --git a/src/Command/FixerApplication.php b/src/Command/FixerApplication.php index bd9d5e3b6a..7cc4d1dea6 100644 --- a/src/Command/FixerApplication.php +++ b/src/Command/FixerApplication.php @@ -10,35 +10,27 @@ use DateTimeZone; use Nette\Utils\Json; use Phar; -use PHPStan\Analyser\AnalyserResult; -use PHPStan\Analyser\Error; -use PHPStan\Analyser\IgnoredErrorHelper; -use PHPStan\Analyser\ResultCache\ResultCacheClearer; -use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; -use PHPStan\File\CouldNotReadFileException; +use PHPStan\Analyser\Ignore\IgnoredErrorHelper; use PHPStan\File\FileMonitor; use PHPStan\File\FileMonitorResult; use PHPStan\File\FileReader; use PHPStan\File\FileWriter; use PHPStan\Internal\ComposerHelper; -use PHPStan\Parallel\Scheduler; -use PHPStan\Process\CpuCoreCounter; -use PHPStan\Process\ProcessCanceledException; -use PHPStan\Process\ProcessCrashedException; +use PHPStan\Internal\DirectoryCreator; +use PHPStan\Internal\DirectoryCreatorException; +use PHPStan\PhpDoc\StubFilesProvider; use PHPStan\Process\ProcessHelper; use PHPStan\Process\ProcessPromise; -use PHPStan\Process\Runnable\RunnableQueue; -use PHPStan\Process\Runnable\RunnableQueueLogger; use PHPStan\ShouldNotHappenException; use Psr\Http\Message\ResponseInterface; use React\ChildProcess\Process; +use React\Dns\Config\Config; use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use React\EventLoop\StreamSelectLoop; use React\Http\Browser; use React\Promise\CancellablePromiseInterface; use React\Promise\ExtendedPromiseInterface; -use React\Promise\PromiseInterface; use React\Socket\ConnectionInterface; use React\Socket\Connector; use React\Socket\TcpServer; @@ -48,6 +40,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Throwable; +use function array_merge; use function count; use function defined; use function escapeshellarg; @@ -57,18 +50,13 @@ use function getenv; use function http_build_query; use function ini_get; -use function is_dir; use function is_file; -use function is_string; -use function min; -use function mkdir; use function parse_url; use function React\Async\await; -use function React\Promise\resolve; use function sprintf; use function strlen; -use function strpos; use function unlink; +use const JSON_INVALID_UTF8_IGNORE; use const PHP_BINARY; use const PHP_URL_PORT; use const PHP_VERSION_ID; @@ -79,37 +67,36 @@ class FixerApplication /** @var (ExtendedPromiseInterface&CancellablePromiseInterface)|null */ private $processInProgress; - private ?string $fixerSuggestionId = null; + private bool $fileMonitorActive = true; /** * @param string[] $analysedPaths + * @param list $dnsServers + * @param string[] $composerAutoloaderProjectPaths + * @param string[] $allConfigFiles + * @param string[] $bootstrapFiles */ public function __construct( private FileMonitor $fileMonitor, - private ResultCacheManagerFactory $resultCacheManagerFactory, - private ResultCacheClearer $resultCacheClearer, private IgnoredErrorHelper $ignoredErrorHelper, - private CpuCoreCounter $cpuCoreCounter, - private Scheduler $scheduler, + private StubFilesProvider $stubFilesProvider, private array $analysedPaths, private string $currentWorkingDirectory, - private string $fixerTmpDir, - private int $maximumNumberOfProcesses, + private string $proTmpDir, + private array $dnsServers, + private array $composerAutoloaderProjectPaths, + private array $allConfigFiles, + private ?string $cliAutoloadFile, + private array $bootstrapFiles, + private ?string $editorUrl, ) { } - /** - * @param Error[] $fileSpecificErrors - * @param string[] $notFileSpecificErrors - */ public function run( ?string $projectConfigFile, - InceptionResult $inceptionResult, InputInterface $input, OutputInterface $output, - array $fileSpecificErrors, - array $notFileSpecificErrors, int $filesCount, string $mainScript, ): int @@ -119,121 +106,79 @@ public function run( /** @var string $serverAddress */ $serverAddress = $server->getAddress(); - /** @var int $serverPort */ + /** @var int<0, 65535> $serverPort */ $serverPort = parse_url($serverAddress, PHP_URL_PORT); - $reanalyseProcessQueue = new RunnableQueue( - new class () implements RunnableQueueLogger { - - public function log(string $message): void - { - } - - }, - min($this->cpuCoreCounter->getNumberOfCpuCores(), $this->maximumNumberOfProcesses), - ); - - $server->on('connection', function (ConnectionInterface $connection) use ($loop, $projectConfigFile, $input, $output, $fileSpecificErrors, $notFileSpecificErrors, $mainScript, $filesCount, $reanalyseProcessQueue, $inceptionResult): void { + $server->on('connection', function (ConnectionInterface $connection) use ($loop, $projectConfigFile, $input, $output, $mainScript, $filesCount): void { // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; // phpcs:enable $decoder = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore, 128 * 1024 * 1024); $encoder = new Encoder($connection, $jsonInvalidUtf8Ignore); $encoder->write(['action' => 'initialData', 'data' => [ - 'fileSpecificErrors' => $fileSpecificErrors, - 'notFileSpecificErrors' => $notFileSpecificErrors, 'currentWorkingDirectory' => $this->currentWorkingDirectory, 'analysedPaths' => $this->analysedPaths, 'projectConfigFile' => $projectConfigFile, 'filesCount' => $filesCount, 'phpstanVersion' => ComposerHelper::getPhpStanVersion(), + 'editorUrl' => $this->editorUrl, ]]); $decoder->on('data', function (array $data) use ( - $loop, - $encoder, - $projectConfigFile, - $input, $output, - $mainScript, - $reanalyseProcessQueue, - $inceptionResult, ): void { if ($data['action'] === 'webPort') { $output->writeln(sprintf('Open your web browser at: http://127.0.0.1:%d', $data['data']['port'])); $output->writeln('Press [Ctrl-C] to quit.'); return; } - if ($data['action'] === 'restoreResultCache') { - $this->fixerSuggestionId = $data['data']['fixerSuggestionId']; + if ($data['action'] === 'resumeFileMonitor') { + $this->fileMonitorActive = true; + return; } - if ($data['action'] !== 'reanalyse') { + if ($data['action'] === 'pauseFileMonitor') { + $this->fileMonitorActive = false; return; } + }); - $id = $data['id']; + $this->fileMonitor->initialize(array_merge( + $this->analysedPaths, + $this->getComposerLocks(), + $this->getComposerInstalled(), + $this->getExecutedFiles(), + $this->getStubFiles(), + $this->allConfigFiles, + )); - $this->reanalyseWithTmpFile( - $loop, - $inceptionResult, - $mainScript, - $reanalyseProcessQueue, - $projectConfigFile, - $data['data']['tmpFile'], - $data['data']['insteadOfFile'], - $data['data']['fixerSuggestionId'], - $input, - )->done(static function (string $output) use ($encoder, $id): void { - $encoder->write(['id' => $id, 'response' => Json::decode($output, Json::FORCE_ARRAY)]); - }, static function (Throwable $e) use ($encoder, $id, $output): void { - if ($e instanceof ProcessCrashedException) { - $output->writeln('Worker process exited: ' . $e->getMessage() . ''); - $encoder->write(['id' => $id, 'error' => $e->getMessage()]); - return; - } - if ($e instanceof ProcessCanceledException) { - $encoder->write(['id' => $id, 'error' => $e->getMessage()]); - return; - } - - $output->writeln('Unexpected error: ' . $e->getMessage() . ''); - $encoder->write(['id' => $id, 'error' => $e->getMessage()]); - }); - }); + $this->analyse( + $loop, + $mainScript, + $projectConfigFile, + $input, + $output, + $encoder, + ); - $this->fileMonitor->initialize($this->analysedPaths); - $this->monitorFileChanges($loop, function (FileMonitorResult $changes) use ($loop, $mainScript, $projectConfigFile, $input, $encoder, $output, $reanalyseProcessQueue, $inceptionResult): void { - $reanalyseProcessQueue->cancelAll(); + $this->monitorFileChanges($loop, function (FileMonitorResult $changes) use ($loop, $mainScript, $projectConfigFile, $input, $encoder, $output): void { if ($this->processInProgress !== null) { $this->processInProgress->cancel(); $this->processInProgress = null; - } else { - $encoder->write(['action' => 'analysisStart']); } - $this->reanalyseAfterFileChanges( + if (count($changes->getChangedFiles()) > 0) { + $encoder->write(['action' => 'changedFiles', 'data' => [ + 'paths' => $changes->getChangedFiles(), + ]]); + } + + $this->analyse( $loop, - $inceptionResult, $mainScript, $projectConfigFile, - $this->fixerSuggestionId, $input, - )->done(function (array $json) use ($encoder, $changes): void { - $this->processInProgress = null; - $this->fixerSuggestionId = null; - $encoder->write(['action' => 'analysisEnd', 'data' => [ - 'fileSpecificErrors' => $json['fileSpecificErrors'], - 'notFileSpecificErrors' => $json['notFileSpecificErrors'], - 'filesCount' => $changes->getTotalFilesCount(), - ]]); - $this->resultCacheClearer->clearTemporaryCaches(); - }, function (Throwable $e) use ($encoder, $output): void { - $this->processInProgress = null; - $this->fixerSuggestionId = null; - $output->writeln('Worker process exited: ' . $e->getMessage() . ''); - $encoder->write(['action' => 'analysisCrash', 'data' => [ - 'error' => $e->getMessage(), - ]]); - }); + $output, + $encoder, + ); }); }); @@ -253,7 +198,7 @@ public function log(string $message): void return; } $output->writeln(sprintf('PHPStan Pro process exited with code %d.', $exitCode)); - @unlink($this->fixerTmpDir . '/phar-info.json'); + @unlink($this->proTmpDir . '/phar-info.json'); }); $loop->run(); @@ -266,20 +211,21 @@ public function log(string $message): void */ private function getFixerProcess(OutputInterface $output, int $serverPort): Process { - if (!@mkdir($this->fixerTmpDir, 0777) && !is_dir($this->fixerTmpDir)) { - $output->writeln(sprintf('Cannot create a temp directory %s', $this->fixerTmpDir)); + try { + DirectoryCreator::ensureDirectoryExists($this->proTmpDir, 0777); + } catch (DirectoryCreatorException $e) { + $output->writeln($e->getMessage()); throw new FixerProcessException(); } - $pharPath = $this->fixerTmpDir . '/phpstan-fixer.phar'; - $infoPath = $this->fixerTmpDir . '/phar-info.json'; + $pharPath = $this->proTmpDir . '/phpstan-fixer.phar'; + $infoPath = $this->proTmpDir . '/phar-info.json'; try { $this->downloadPhar($output, $pharPath, $infoPath); } catch (RuntimeException $e) { if (!is_file($pharPath)) { - $output->writeln('Could not download the PHPStan Pro executable.'); - $output->writeln($e->getMessage()); + $this->printDownloadError($output, $e); throw new FixerProcessException(); } @@ -307,6 +253,7 @@ private function getFixerProcess(OutputInterface $output, int $serverPort): Proc } $env = getenv(); + $env['PHPSTAN_PRO_TMP_DIR'] = $this->proTmpDir; $forcedPort = $_SERVER['PHPSTAN_PRO_WEB_PORT'] ?? null; if ($forcedPort !== null) { $env['PHPSTAN_PRO_WEB_PORT'] = $_SERVER['PHPSTAN_PRO_WEB_PORT']; @@ -318,7 +265,7 @@ private function getFixerProcess(OutputInterface $output, int $serverPort): Proc $output->writeln(sprintf(' -p 127.0.0.1:%d:%d', $_SERVER['PHPSTAN_PRO_WEB_PORT'], $_SERVER['PHPSTAN_PRO_WEB_PORT'])); $output->writeln('2) Map the temp directory to a persistent volume'); $output->writeln(' so that you don\'t have to log in every time:'); - $output->writeln(sprintf(' -v ~/.phpstan-pro:%s', $this->fixerTmpDir)); + $output->writeln(sprintf(' -v ~/.phpstan-pro:%s', $this->proTmpDir)); $output->writeln(''); } } else { @@ -334,12 +281,12 @@ private function getFixerProcess(OutputInterface $output, int $serverPort): Proc $output->writeln(' -p 127.0.0.1:11111:11111'); $output->writeln('4) Map the temp directory to a persistent volume'); $output->writeln(' so that you don\'t have to log in every time:'); - $output->writeln(sprintf(' -v ~/phpstan-pro:%s', $this->fixerTmpDir)); + $output->writeln(sprintf(' -v ~/phpstan-pro:%s', $this->proTmpDir)); $output->writeln(''); } } - return new Process(sprintf('%s -d memory_limit=%s %s --port %d', PHP_BINARY, escapeshellarg(ini_get('memory_limit')), escapeshellarg($pharPath), $serverPort), null, $env, []); + return new Process(sprintf('%s -d memory_limit=%s %s --port %d', escapeshellarg(PHP_BINARY), escapeshellarg(ini_get('memory_limit')), escapeshellarg($pharPath), $serverPort), null, $env, []); } private function downloadPhar( @@ -349,21 +296,29 @@ private function downloadPhar( ): void { $currentVersion = null; + $branch = 'main'; if (is_file($pharPath) && is_file($infoPath)) { - /** @var array{version: string, date: string} $currentInfo */ + /** @var array{version: string, date: string, branch?: string} $currentInfo */ $currentInfo = Json::decode(FileReader::read($infoPath), Json::FORCE_ARRAY); $currentVersion = $currentInfo['version']; + $currentBranch = $currentInfo['branch'] ?? 'master'; $currentDate = DateTime::createFromFormat(DateTime::ATOM, $currentInfo['date']); if ($currentDate === false) { throw new ShouldNotHappenException(); } - if ((new DateTimeImmutable('', new DateTimeZone('UTC'))) <= $currentDate->modify('+24 hours')) { + if ( + $currentBranch === $branch + && (new DateTimeImmutable('', new DateTimeZone('UTC'))) <= $currentDate->modify('+24 hours') + ) { return; } $output->writeln('Checking if there\'s a new PHPStan Pro release...'); } + $dnsConfig = new Config(); + $dnsConfig->nameservers = $this->dnsServers; + $client = new Browser( new Connector( [ @@ -371,7 +326,7 @@ private function downloadPhar( 'tls' => [ 'cafile' => CaBundle::getBundledCaBundlePath(), ], - 'dns' => '1.1.1.1', + 'dns' => $dnsConfig, ], ), ); @@ -379,9 +334,9 @@ private function downloadPhar( /** * @var array{url: string, version: string} $latestInfo */ - $latestInfo = Json::decode((string) await($client->get(sprintf('https://fixer-download-api.phpstan.com/latest?%s', http_build_query(['phpVersion' => PHP_VERSION_ID]))))->getBody(), Json::FORCE_ARRAY); + $latestInfo = Json::decode((string) await($client->get(sprintf('https://fixer-download-api.phpstan.com/latest?%s', http_build_query(['phpVersion' => PHP_VERSION_ID, 'branch' => $branch]))))->getBody(), Json::FORCE_ARRAY); if ($currentVersion !== null && $latestInfo['version'] === $currentVersion) { - $this->writeInfoFile($infoPath, $latestInfo['version']); + $this->writeInfoFile($infoPath, $latestInfo['version'], $branch); $output->writeln('You\'re running the latest PHPStan Pro!'); return; } @@ -410,8 +365,8 @@ private function downloadPhar( fwrite($pharPathResource, $chunk); $progressBar->setProgress($bytes); }); - }, static function (Throwable $e) use ($output): void { - $output->writeln(sprintf('Could not download the PHPStan Pro executable: %s', $e->getMessage())); + }, function (Throwable $e) use ($output): void { + $this->printDownloadError($output, $e); }); Loop::run(); @@ -422,13 +377,27 @@ private function downloadPhar( $output->writeln(''); $output->writeln(''); - $this->writeInfoFile($infoPath, $latestInfo['version']); + $this->writeInfoFile($infoPath, $latestInfo['version'], $branch); + } + + private function printDownloadError(OutputInterface $output, Throwable $e): void + { + $output->writeln(sprintf('Could not download the PHPStan Pro executable: %s', $e->getMessage())); + $output->writeln(''); + $output->writeln('Try different DNS servers in your configuration file:'); + $output->writeln(''); + $output->writeln('parameters:'); + $output->writeln("\tpro:"); + $output->writeln("\t\tdnsServers!:"); + $output->writeln("\t\t\t- '8.8.8.8'"); + $output->writeln(''); } - private function writeInfoFile(string $infoPath, string $version): void + private function writeInfoFile(string $infoPath, string $version, string $branch): void { FileWriter::write($infoPath, Json::encode([ 'version' => $version, + 'branch' => $branch, 'date' => (new DateTimeImmutable('', new DateTimeZone('UTC')))->format(DateTime::ATOM), ])); } @@ -439,6 +408,10 @@ private function writeInfoFile(string $infoPath, string $version): void private function monitorFileChanges(LoopInterface $loop, callable $hasChangesCallback): void { $callback = function () use (&$callback, $loop, $hasChangesCallback): void { + if (!$this->fileMonitorActive) { + $loop->addTimer(1.0, $callback); + return; + } $changes = $this->fileMonitor->getChanges(); if ($changes->hasAnyChanges()) { @@ -450,122 +423,137 @@ private function monitorFileChanges(LoopInterface $loop, callable $hasChangesCal $loop->addTimer(1.0, $callback); } - private function reanalyseWithTmpFile( + private function analyse( LoopInterface $loop, - InceptionResult $inceptionResult, string $mainScript, - RunnableQueue $runnableQueue, ?string $projectConfigFile, - string $tmpFile, - string $insteadOfFile, - string $fixerSuggestionId, InputInterface $input, - ): PromiseInterface + OutputInterface $output, + Encoder $phpstanFixerEncoder, + ): void { - $resultCacheManager = $this->resultCacheManagerFactory->create([$insteadOfFile => $tmpFile]); - [$inceptionFiles] = $inceptionResult->getFiles(); - $resultCache = $resultCacheManager->restore($inceptionFiles, false, false, $inceptionResult->getProjectConfigArray(), $inceptionResult->getErrorOutput()); - $schedule = $this->scheduler->scheduleWork($this->cpuCoreCounter->getNumberOfCpuCores(), $resultCache->getFilesToAnalyse()); + $ignoredErrorHelperResult = $this->ignoredErrorHelper->initialize(); + if (count($ignoredErrorHelperResult->getErrors()) > 0) { + throw new ShouldNotHappenException(); + } + + // TCP server for fixer:worker (TCP client) + $server = new TcpServer('127.0.0.1:0', $loop); + /** @var string $serverAddress */ + $serverAddress = $server->getAddress(); + /** @var int<0, 65535> $serverPort */ + $serverPort = parse_url($serverAddress, PHP_URL_PORT); + + $server->on('connection', static function (ConnectionInterface $connection) use ($phpstanFixerEncoder): void { + // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly + $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; + // phpcs:enable + $decoder = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore, 128 * 1024 * 1024); + $decoder->on('data', static function (array $data) use ($phpstanFixerEncoder): void { + $phpstanFixerEncoder->write($data); + }); + }); - $process = new ProcessPromise($loop, $fixerSuggestionId, ProcessHelper::getWorkerCommand( + $process = new ProcessPromise($loop, 'changedFileAnalysis', ProcessHelper::getWorkerCommand( $mainScript, 'fixer:worker', $projectConfigFile, [ - '--tmp-file', - escapeshellarg($tmpFile), - '--instead-of', - escapeshellarg($insteadOfFile), - '--save-result-cache', - escapeshellarg($fixerSuggestionId), - '--allow-parallel', + '--server-port', + (string) $serverPort, ], $input, )); + $this->processInProgress = $process->run(); - return $runnableQueue->queue($process, $schedule->getNumberOfProcesses()); + $this->processInProgress->done(function () use ($server): void { + $this->processInProgress = null; + $server->close(); + }, function (Throwable $e) use ($server, $output, $phpstanFixerEncoder): void { + $this->processInProgress = null; + $server->close(); + $output->writeln('Worker process exited: ' . $e->getMessage() . ''); + $phpstanFixerEncoder->write(['action' => 'analysisCrash', 'data' => [ + 'errors' => [$e->getMessage()], + ]]); + throw $e; + }); } - private function reanalyseAfterFileChanges( - LoopInterface $loop, - InceptionResult $inceptionResult, - string $mainScript, - ?string $projectConfigFile, - ?string $fixerSuggestionId, - InputInterface $input, - ): PromiseInterface + private function isDockerRunning(): bool { - $ignoredErrorHelperResult = $this->ignoredErrorHelper->initialize(); - if (count($ignoredErrorHelperResult->getErrors()) > 0) { - throw new ShouldNotHappenException(); + return is_file('/.dockerenv'); + } + + /** + * @return list + */ + private function getComposerLocks(): array + { + $locks = []; + foreach ($this->composerAutoloaderProjectPaths as $autoloadPath) { + $lockPath = $autoloadPath . '/composer.lock'; + if (!is_file($lockPath)) { + continue; + } + + $locks[] = $lockPath; } - $projectConfigArray = $inceptionResult->getProjectConfigArray(); - - $resultCacheManager = $this->resultCacheManagerFactory->create([]); - [$inceptionFiles, $isOnlyFiles] = $inceptionResult->getFiles(); - $resultCache = $resultCacheManager->restore($inceptionFiles, false, false, $projectConfigArray, $inceptionResult->getErrorOutput(), $fixerSuggestionId); - if (count($resultCache->getFilesToAnalyse()) === 0) { - $result = $resultCacheManager->process( - new AnalyserResult([], [], [], [], [], false), - $resultCache, - $inceptionResult->getErrorOutput(), - false, - true, - )->getAnalyserResult(); - $intermediateErrors = $ignoredErrorHelperResult->process( - $result->getErrors(), - $isOnlyFiles, - $inceptionFiles, - count($result->getInternalErrors()) > 0 || $result->hasReachedInternalErrorsCountLimit(), - ); - $finalFileSpecificErrors = []; - $finalNotFileSpecificErrors = []; - foreach ($intermediateErrors as $intermediateError) { - if (is_string($intermediateError)) { - $finalNotFileSpecificErrors[] = $intermediateError; - continue; - } + return $locks; + } - $finalFileSpecificErrors[] = $intermediateError; + /** + * @return list + */ + private function getComposerInstalled(): array + { + $files = []; + foreach ($this->composerAutoloaderProjectPaths as $autoloadPath) { + $composer = ComposerHelper::getComposerConfig($autoloadPath); + if ($composer === null) { + continue; } - return resolve([ - 'fileSpecificErrors' => $finalFileSpecificErrors, - 'notFileSpecificErrors' => $finalNotFileSpecificErrors, - ]); - } + $filePath = ComposerHelper::getVendorDirFromComposerConfig($autoloadPath, $composer) . '/composer/installed.php'; + if (!is_file($filePath)) { + continue; + } - $options = ['--save-result-cache', '--allow-parallel']; - if ($fixerSuggestionId !== null) { - $options[] = '--restore-result-cache'; - $options[] = $fixerSuggestionId; + $files[] = $filePath; } - $process = new ProcessPromise($loop, 'changedFileAnalysis', ProcessHelper::getWorkerCommand( - $mainScript, - 'fixer:worker', - $projectConfigFile, - $options, - $input, - )); - $this->processInProgress = $process->run(); - return $this->processInProgress->then(static fn (string $output): array => Json::decode($output, Json::FORCE_ARRAY)); + return $files; } - private function isDockerRunning(): bool + /** + * @return list + */ + private function getExecutedFiles(): array { - if (!is_file('/proc/1/cgroup')) { - return false; + $files = []; + if ($this->cliAutoloadFile !== null) { + $files[] = $this->cliAutoloadFile; } - try { - $contents = FileReader::read('/proc/1/cgroup'); + foreach ($this->bootstrapFiles as $bootstrapFile) { + $files[] = $bootstrapFile; + } - return strpos($contents, 'docker') !== false; - } catch (CouldNotReadFileException) { - return false; + return $files; + } + + /** + * @return list + */ + private function getStubFiles(): array + { + $stubFiles = []; + foreach ($this->stubFilesProvider->getProjectStubFiles() as $stubFile) { + $stubFiles[] = $stubFile; } + + return $stubFiles; } } diff --git a/src/Command/FixerWorkerCommand.php b/src/Command/FixerWorkerCommand.php index 5d6e583924..942c66e486 100644 --- a/src/Command/FixerWorkerCommand.php +++ b/src/Command/FixerWorkerCommand.php @@ -2,21 +2,43 @@ namespace PHPStan\Command; -use Nette\Utils\Json; +use Clue\React\NDJson\Encoder; use PHPStan\Analyser\AnalyserResult; -use PHPStan\Analyser\IgnoredErrorHelper; +use PHPStan\Analyser\AnalyserResultFinalizer; +use PHPStan\Analyser\Error; +use PHPStan\Analyser\Ignore\IgnoredErrorHelper; +use PHPStan\Analyser\Ignore\IgnoredErrorHelperResult; use PHPStan\Analyser\ResultCache\ResultCacheManager; use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; +use PHPStan\DependencyInjection\Container; +use PHPStan\File\PathNotFoundException; +use PHPStan\Parallel\ParallelAnalyser; +use PHPStan\Parallel\Scheduler; +use PHPStan\Process\CpuCoreCounter; use PHPStan\ShouldNotHappenException; +use React\EventLoop\LoopInterface; +use React\EventLoop\StreamSelectLoop; +use React\Promise\PromiseInterface; +use React\Socket\ConnectionInterface; +use React\Socket\TcpConnector; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use function array_diff; use function count; +use function filemtime; +use function in_array; use function is_array; use function is_bool; +use function is_file; use function is_string; +use function memory_get_peak_usage; +use function React\Promise\resolve; +use function sprintf; +use function usort; +use const JSON_INVALID_UTF8_IGNORE; class FixerWorkerCommand extends Command { @@ -43,13 +65,10 @@ protected function configure(): void new InputOption(AnalyseCommand::OPTION_LEVEL, 'l', InputOption::VALUE_REQUIRED, 'Level of rule options - the higher the stricter'), new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for analysis'), - new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with XDebug for debugging purposes'), - new InputOption('tmp-file', null, InputOption::VALUE_REQUIRED), - new InputOption('instead-of', null, InputOption::VALUE_REQUIRED), - new InputOption('save-result-cache', null, InputOption::VALUE_OPTIONAL, '', false), - new InputOption('restore-result-cache', null, InputOption::VALUE_REQUIRED), - new InputOption('allow-parallel', null, InputOption::VALUE_NONE, 'Allow parallel analysis'), - ]); + new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with Xdebug for debugging purposes'), + new InputOption('server-port', null, InputOption::VALUE_REQUIRED, 'Server port for FixerApplication'), + ]) + ->setHidden(true); } protected function execute(InputInterface $input, OutputInterface $output): int @@ -60,7 +79,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $configuration = $input->getOption('configuration'); $level = $input->getOption(AnalyseCommand::OPTION_LEVEL); $allowXdebug = $input->getOption('xdebug'); - $allowParallel = $input->getOption('allow-parallel'); + $serverPort = $input->getOption('server-port'); if ( !is_array($paths) @@ -69,37 +88,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int || (!is_string($configuration) && $configuration !== null) || (!is_string($level) && $level !== null) || (!is_bool($allowXdebug)) - || (!is_bool($allowParallel)) + || (!is_string($serverPort)) ) { throw new ShouldNotHappenException(); } - /** @var string|null $tmpFile */ - $tmpFile = $input->getOption('tmp-file'); - - /** @var string|null $insteadOfFile */ - $insteadOfFile = $input->getOption('instead-of'); - - /** @var false|string|null $saveResultCache */ - $saveResultCache = $input->getOption('save-result-cache'); - - /** @var string|null $restoreResultCache */ - $restoreResultCache = $input->getOption('restore-result-cache'); - if (is_string($tmpFile)) { - if (!is_string($insteadOfFile)) { - throw new ShouldNotHappenException(); - } - } elseif (is_string($insteadOfFile)) { - throw new ShouldNotHappenException(); - } elseif ($saveResultCache === false) { - throw new ShouldNotHappenException(); - } - - $singleReflectionFile = null; - if ($tmpFile !== null) { - $singleReflectionFile = $tmpFile; - } - try { $inceptionResult = CommandHelper::begin( $input, @@ -113,8 +106,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $level, $allowXdebug, false, - $singleReflectionFile, - $insteadOfFile, false, ); } catch (InceptionNotSuccessfulException) { @@ -130,144 +121,229 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw new ShouldNotHappenException(); } - /** @var AnalyserRunner $analyserRunner */ - $analyserRunner = $container->getByType(AnalyserRunner::class); - - $fileReplacements = []; - if ($insteadOfFile !== null && $tmpFile !== null) { - $fileReplacements = [$insteadOfFile => $tmpFile]; - } - /** @var ResultCacheManager $resultCacheManager */ - $resultCacheManager = $container->getByType(ResultCacheManagerFactory::class)->create($fileReplacements); - $projectConfigArray = $inceptionResult->getProjectConfigArray(); - [$inceptionFiles, $isOnlyFiles] = $inceptionResult->getFiles(); - $resultCache = $resultCacheManager->restore($inceptionFiles, false, false, $projectConfigArray, $inceptionResult->getErrorOutput(), $restoreResultCache); - - $intermediateAnalyserResult = $analyserRunner->runAnalyser( - $resultCache->getFilesToAnalyse(), - $inceptionFiles, - null, - null, - false, - $allowParallel, - $configuration, - $tmpFile, - $insteadOfFile, - $input, - ); - $result = $resultCacheManager->process( - $this->switchTmpFileInAnalyserResult($intermediateAnalyserResult, $tmpFile, $insteadOfFile), - $resultCache, - $inceptionResult->getErrorOutput(), - false, - is_string($saveResultCache) ? $saveResultCache : $saveResultCache === null, - )->getAnalyserResult(); - - $intermediateErrors = $ignoredErrorHelperResult->process( - $result->getErrors(), - $isOnlyFiles, - $inceptionFiles, - count($result->getInternalErrors()) > 0 || $result->hasReachedInternalErrorsCountLimit(), - ); - $finalFileSpecificErrors = []; - $finalNotFileSpecificErrors = []; - foreach ($intermediateErrors as $intermediateError) { - if (is_string($intermediateError)) { - $finalNotFileSpecificErrors[] = $intermediateError; - continue; + $loop = new StreamSelectLoop(); + $tcpConnector = new TcpConnector($loop); + $tcpConnector->connect(sprintf('127.0.0.1:%d', $serverPort))->done(function (ConnectionInterface $connection) use ($container, $inceptionResult, $configuration, $input, $ignoredErrorHelperResult, $loop): void { + // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly + $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; + // phpcs:enable + $out = new Encoder($connection, $jsonInvalidUtf8Ignore); + //$in = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore, 128 * 1024 * 1024); + + /** @var ResultCacheManager $resultCacheManager */ + $resultCacheManager = $container->getByType(ResultCacheManagerFactory::class)->create(); + $projectConfigArray = $inceptionResult->getProjectConfigArray(); + + /** @var AnalyserResultFinalizer $analyserResultFinalizer */ + $analyserResultFinalizer = $container->getByType(AnalyserResultFinalizer::class); + + try { + [$inceptionFiles, $isOnlyFiles] = $inceptionResult->getFiles(); + } catch (PathNotFoundException | InceptionNotSuccessfulException) { + throw new ShouldNotHappenException(); } - $finalFileSpecificErrors[] = $intermediateError; - } + $out->write([ + 'action' => 'analysisStart', + 'result' => [ + 'analysedFiles' => $inceptionFiles, + ], + ]); - $output->writeln(Json::encode([ - 'fileSpecificErrors' => $finalFileSpecificErrors, - 'notFileSpecificErrors' => $finalNotFileSpecificErrors, - ]), OutputInterface::OUTPUT_RAW); + $resultCache = $resultCacheManager->restore($inceptionFiles, false, false, $projectConfigArray, $inceptionResult->getErrorOutput()); - return 0; - } + $errorsFromResultCacheTmp = $resultCache->getErrors(); + $locallyIgnoredErrorsFromResultCacheTmp = $resultCache->getLocallyIgnoredErrors(); + foreach ($resultCache->getFilesToAnalyse() as $fileToAnalyse) { + unset($errorsFromResultCacheTmp[$fileToAnalyse]); + unset($locallyIgnoredErrorsFromResultCacheTmp[$fileToAnalyse]); + } - private function switchTmpFileInAnalyserResult( - AnalyserResult $analyserResult, - ?string $insteadOfFile, - ?string $tmpFile, - ): AnalyserResult - { - $fileSpecificErrors = []; - foreach ($analyserResult->getErrors() as $error) { - if ( - $tmpFile !== null - && $insteadOfFile !== null - ) { - if ($error->getFilePath() === $insteadOfFile) { - $error = $error->changeFilePath($tmpFile); - } - if ($error->getTraitFilePath() === $insteadOfFile) { - $error = $error->changeTraitFilePath($tmpFile); + $errorsFromResultCache = []; + foreach ($errorsFromResultCacheTmp as $errorsByFile) { + foreach ($errorsByFile as $error) { + $errorsFromResultCache[] = $error; } } - $fileSpecificErrors[] = $error; - } + [$errorsFromResultCache, $ignoredErrorsFromResultCache] = $this->filterErrors($errorsFromResultCache, $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles, false); - $collectedData = []; - foreach ($analyserResult->getCollectedData() as $data) { - if ( - $tmpFile !== null - && $insteadOfFile !== null - ) { - if ($data->getFilePath() === $insteadOfFile) { - $data = $data->changeFilePath($tmpFile); + foreach ($locallyIgnoredErrorsFromResultCacheTmp as $locallyIgnoredErrors) { + foreach ($locallyIgnoredErrors as $locallyIgnoredError) { + $ignoredErrorsFromResultCache[] = [$locallyIgnoredError, null]; } } - $collectedData[] = $data; - } + $out->write([ + 'action' => 'analysisStream', + 'result' => [ + 'errors' => $errorsFromResultCache, + 'ignoredErrors' => $ignoredErrorsFromResultCache, + 'analysedFiles' => array_diff($inceptionFiles, $resultCache->getFilesToAnalyse()), + ], + ]); - $dependencies = null; - if ($analyserResult->getDependencies() !== null) { - $dependencies = []; - foreach ($analyserResult->getDependencies() as $dependencyFile => $dependentFiles) { - $new = []; - foreach ($dependentFiles as $file) { - if ($file === $insteadOfFile && $tmpFile !== null) { - $new[] = $tmpFile; - continue; - } + $filesToAnalyse = $resultCache->getFilesToAnalyse(); + usort($filesToAnalyse, static function (string $a, string $b): int { + $aTime = @filemtime($a); + if ($aTime === false) { + return 1; + } - $new[] = $file; + $bTime = @filemtime($b); + if ($bTime === false) { + return -1; } - $key = $dependencyFile; - if ($key === $insteadOfFile && $tmpFile !== null) { - $key = $tmpFile; + // files are sorted from the oldest + // because ParallelAnalyser reverses the scheduler jobs to do the smallest + // jobs first + return $aTime <=> $bTime; + }); + + $this->runAnalyser( + $loop, + $container, + $filesToAnalyse, + $configuration, + $input, + function (array $errors, array $locallyIgnoredErrors, array $analysedFiles) use ($out, $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles): void { + [$errors, $ignoredErrors] = $this->filterErrors($errors, $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles, false); + foreach ($locallyIgnoredErrors as $locallyIgnoredError) { + $ignoredErrors[] = [$locallyIgnoredError, null]; + } + $out->write([ + 'action' => 'analysisStream', + 'result' => [ + 'errors' => $errors, + 'ignoredErrors' => $ignoredErrors, + 'analysedFiles' => $analysedFiles, + ], + ]); + }, + )->then(function (AnalyserResult $intermediateAnalyserResult) use ($analyserResultFinalizer, $resultCacheManager, $resultCache, $inceptionResult, $isOnlyFiles, $ignoredErrorHelperResult, $inceptionFiles, $out): void { + $analyserResult = $resultCacheManager->process( + $intermediateAnalyserResult, + $resultCache, + $inceptionResult->getErrorOutput(), + false, + true, + )->getAnalyserResult(); + $finalizerResult = $analyserResultFinalizer->finalize($analyserResult, $isOnlyFiles); + + $hasInternalErrors = count($finalizerResult->getAnalyserResult()->getInternalErrors()) > 0 || $finalizerResult->getAnalyserResult()->hasReachedInternalErrorsCountLimit(); + + if ($hasInternalErrors) { + $out->write(['action' => 'analysisCrash', 'data' => [ + 'errors' => count($finalizerResult->getAnalyserResult()->getInternalErrors()) > 0 ? $finalizerResult->getAnalyserResult()->getInternalErrors() : [ + 'Internal error occurred', + ], + ]]); } - $dependencies[$key] = $new; + [$collectorErrors, $ignoredCollectorErrors] = $this->filterErrors($finalizerResult->getCollectorErrors(), $ignoredErrorHelperResult, $isOnlyFiles, $inceptionFiles, $hasInternalErrors); + foreach ($finalizerResult->getLocallyIgnoredCollectorErrors() as $locallyIgnoredCollectorError) { + $ignoredCollectorErrors[] = [$locallyIgnoredCollectorError, null]; + } + $out->write([ + 'action' => 'analysisStream', + 'result' => [ + 'errors' => $collectorErrors, + 'ignoredErrors' => $ignoredCollectorErrors, + 'analysedFiles' => [], + ], + ]); + + $ignoredErrorHelperProcessedResult = $ignoredErrorHelperResult->process( + $finalizerResult->getErrors(), + $isOnlyFiles, + $inceptionFiles, + $hasInternalErrors, + ); + $ignoreFileErrors = []; + foreach ($ignoredErrorHelperProcessedResult->getNotIgnoredErrors() as $error) { + if ($error->getIdentifier() === null) { + continue; + } + if (!in_array($error->getIdentifier(), ['ignore.count', 'ignore.unmatched', 'ignore.unmatchedLine', 'ignore.unmatchedIdentifier'], true)) { + continue; + } + $ignoreFileErrors[] = $error; + } + + $out->end([ + 'action' => 'analysisEnd', + 'result' => [ + 'ignoreFileErrors' => $ignoreFileErrors, + 'ignoreNotFileErrors' => $ignoredErrorHelperProcessedResult->getOtherIgnoreMessages(), + ], + ]); + }); + }); + $loop->run(); + + return 0; + } + + /** + * @param string[] $inceptionFiles + * @param array $errors + * @return array{list, list} + */ + private function filterErrors(array $errors, IgnoredErrorHelperResult $ignoredErrorHelperResult, bool $onlyFiles, array $inceptionFiles, bool $hasInternalErrors): array + { + $ignoredErrorHelperProcessedResult = $ignoredErrorHelperResult->process($errors, $onlyFiles, $inceptionFiles, $hasInternalErrors); + $finalErrors = []; + foreach ($ignoredErrorHelperProcessedResult->getNotIgnoredErrors() as $error) { + if ($error->getIdentifier() === null) { + $finalErrors[] = $error; + continue; + } + if (in_array($error->getIdentifier(), ['ignore.count', 'ignore.unmatched'], true)) { + continue; } + $finalErrors[] = $error; } - $exportedNodes = []; - foreach ($analyserResult->getExportedNodes() as $file => $fileExportedNodes) { - if ( - $tmpFile !== null - && $insteadOfFile !== null - && $file === $insteadOfFile - ) { - $file = $tmpFile; - } + return [ + $finalErrors, + $ignoredErrorHelperProcessedResult->getIgnoredErrors(), + ]; + } - $exportedNodes[$file] = $fileExportedNodes; + /** + * @param string[] $files + * @param callable(list, list, string[]): void $onFileAnalysisHandler + */ + private function runAnalyser(LoopInterface $loop, Container $container, array $files, ?string $configuration, InputInterface $input, callable $onFileAnalysisHandler): PromiseInterface + { + /** @var ParallelAnalyser $parallelAnalyser */ + $parallelAnalyser = $container->getByType(ParallelAnalyser::class); + $filesCount = count($files); + if ($filesCount === 0) { + return resolve(new AnalyserResult([], [], [], [], [], [], [], [], [], [], false, memory_get_peak_usage(true))); + } + + /** @var Scheduler $scheduler */ + $scheduler = $container->getByType(Scheduler::class); + + /** @var CpuCoreCounter $cpuCoreCounter */ + $cpuCoreCounter = $container->getByType(CpuCoreCounter::class); + + $schedule = $scheduler->scheduleWork($cpuCoreCounter->getNumberOfCpuCores(), $files); + $mainScript = null; + if (isset($_SERVER['argv'][0]) && is_file($_SERVER['argv'][0])) { + $mainScript = $_SERVER['argv'][0]; } - return new AnalyserResult( - $fileSpecificErrors, - $analyserResult->getInternalErrors(), - $collectedData, - $dependencies, - $exportedNodes, - $analyserResult->hasReachedInternalErrorsCountLimit(), + return $parallelAnalyser->analyse( + $loop, + $schedule, + $mainScript, + null, + $configuration, + $input, + $onFileAnalysisHandler, ); } diff --git a/src/Command/IgnoredRegexValidator.php b/src/Command/IgnoredRegexValidator.php index 59efbf03b5..e7a90c5bd2 100644 --- a/src/Command/IgnoredRegexValidator.php +++ b/src/Command/IgnoredRegexValidator.php @@ -12,7 +12,8 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; use function count; -use function strpos; +use function str_contains; +use function str_starts_with; use function strrpos; use function substr; @@ -34,12 +35,12 @@ public function validate(string $regex): IgnoredRegexValidatorResult /** @var TreeNode $ast */ $ast = $this->parser->parse($regex); } catch (Exception $e) { - if (strpos($e->getMessage(), 'Unexpected token "|" (alternation) at line 1') === 0) { + if (str_starts_with($e->getMessage(), 'Unexpected token "|" (alternation) at line 1')) { return new IgnoredRegexValidatorResult([], false, true, '||', '\|\|'); } if ( - strpos($regex, '()') !== false - && strpos($e->getMessage(), 'Unexpected token ")" (_capturing) at line 1') === 0 + str_contains($regex, '()') + && str_starts_with($e->getMessage(), 'Unexpected token ")" (_capturing) at line 1') ) { return new IgnoredRegexValidatorResult([], false, true, '()', '\(\)'); } @@ -86,15 +87,17 @@ private function getIgnoredTypes(TreeNode $ast): array continue; } - if ($type->describe(VerbosityLevel::typeOnly()) !== $matches[1]) { + if ($type instanceof ObjectType) { continue; } - if ($type instanceof ObjectType) { + $typeDescription = $type->describe(VerbosityLevel::typeOnly()); + + if ($typeDescription !== $matches[1]) { continue; } - $types[$type->describe(VerbosityLevel::typeOnly())] = $text; + $types[$typeDescription] = $text; } return $types; diff --git a/src/Command/InceptionResult.php b/src/Command/InceptionResult.php index 8a0010c8c2..308935fb9d 100644 --- a/src/Command/InceptionResult.php +++ b/src/Command/InceptionResult.php @@ -3,7 +3,9 @@ namespace PHPStan\Command; use PHPStan\DependencyInjection\Container; +use PHPStan\File\PathNotFoundException; use PHPStan\Internal\BytesHelper; +use function max; use function memory_get_peak_usage; use function sprintf; @@ -32,12 +34,15 @@ public function __construct( } /** + * @throws InceptionNotSuccessfulException + * @throws PathNotFoundException * @return array{string[], bool} */ public function getFiles(): array { $callback = $this->filesCallback; + /** @throws InceptionNotSuccessfulException|PathNotFoundException */ return $callback(); } @@ -79,10 +84,13 @@ public function getGenerateBaselineFile(): ?string return $this->generateBaselineFile; } - public function handleReturn(int $exitCode): int + public function handleReturn(int $exitCode, ?int $peakMemoryUsageBytes): int { - if ($this->getErrorOutput()->isVerbose()) { - $this->getErrorOutput()->writeLineFormatted(sprintf('Used memory: %s', BytesHelper::bytes(memory_get_peak_usage(true)))); + if ($peakMemoryUsageBytes !== null && $this->getErrorOutput()->isVerbose()) { + $this->getErrorOutput()->writeLineFormatted(sprintf( + 'Used memory: %s', + BytesHelper::bytes(max(memory_get_peak_usage(true), $peakMemoryUsageBytes)), + )); } return $exitCode; diff --git a/src/Command/WorkerCommand.php b/src/Command/WorkerCommand.php index 232d299fad..01be926b0f 100644 --- a/src/Command/WorkerCommand.php +++ b/src/Command/WorkerCommand.php @@ -23,13 +23,12 @@ use Symfony\Component\Console\Output\OutputInterface; use Throwable; use function array_fill_keys; -use function array_filter; -use function array_values; -use function count; +use function array_merge; use function defined; use function is_array; use function is_bool; use function is_string; +use function memory_get_peak_usage; use function sprintf; class WorkerCommand extends Command @@ -59,12 +58,11 @@ protected function configure(): void new InputOption(AnalyseCommand::OPTION_LEVEL, 'l', InputOption::VALUE_REQUIRED, 'Level of rule options - the higher the stricter'), new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for analysis'), - new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with XDebug for debugging purposes'), + new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with Xdebug for debugging purposes'), new InputOption('port', null, InputOption::VALUE_REQUIRED), new InputOption('identifier', null, InputOption::VALUE_REQUIRED), - new InputOption('tmp-file', null, InputOption::VALUE_REQUIRED), - new InputOption('instead-of', null, InputOption::VALUE_REQUIRED), - ]); + ]) + ->setHidden(true); } protected function execute(InputInterface $input, OutputInterface $output): int @@ -91,17 +89,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw new ShouldNotHappenException(); } - /** @var string|null $tmpFile */ - $tmpFile = $input->getOption('tmp-file'); - - /** @var string|null $insteadOfFile */ - $insteadOfFile = $input->getOption('instead-of'); - - $singleReflectionFile = null; - if ($tmpFile !== null) { - $singleReflectionFile = $tmpFile; - } - try { $inceptionResult = CommandHelper::begin( $input, @@ -115,8 +102,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $level, $allowXdebug, false, - $singleReflectionFile, - null, false, ); } catch (InceptionNotSuccessfulException $e) { @@ -128,27 +113,27 @@ protected function execute(InputInterface $input, OutputInterface $output): int try { [$analysedFiles] = $inceptionResult->getFiles(); - $analysedFiles = $this->switchTmpFile($analysedFiles, $insteadOfFile, $tmpFile); } catch (PathNotFoundException $e) { $inceptionResult->getErrorOutput()->writeLineFormatted(sprintf('%s', $e->getMessage())); return 1; + } catch (InceptionNotSuccessfulException) { + return 1; } - /** @var NodeScopeResolver $nodeScopeResolver */ $nodeScopeResolver = $container->getByType(NodeScopeResolver::class); $nodeScopeResolver->setAnalysedFiles($analysedFiles); $analysedFiles = array_fill_keys($analysedFiles, true); - $tcpConector = new TcpConnector($loop); - $tcpConector->connect(sprintf('127.0.0.1:%d', $port))->done(function (ConnectionInterface $connection) use ($container, $identifier, $output, $analysedFiles, $tmpFile, $insteadOfFile): void { + $tcpConnector = new TcpConnector($loop); + $tcpConnector->connect(sprintf('127.0.0.1:%d', $port))->done(function (ConnectionInterface $connection) use ($container, $identifier, $output, $analysedFiles): void { // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; // phpcs:enable $out = new Encoder($connection, $jsonInvalidUtf8Ignore); $in = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore, $container->getParameter('parallel')['buffer']); $out->write(['action' => 'hello', 'identifier' => $identifier]); - $this->runWorker($container, $out, $in, $output, $analysedFiles, $tmpFile, $insteadOfFile); + $this->runWorker($container, $out, $in, $output, $analysedFiles); }); $loop->run(); @@ -169,8 +154,6 @@ private function runWorker( ReadableStreamInterface $in, OutputInterface $output, array $analysedFiles, - ?string $tmpFile, - ?string $insteadOfFile, ): void { $handleError = function (Throwable $error) use ($out, $output): void { @@ -181,20 +164,17 @@ private function runWorker( 'result' => [ 'errors' => [$error->getMessage()], 'dependencies' => [], - 'filesCount' => 0, + 'files' => [], 'internalErrorsCount' => 1, ], ]); $out->end(); }; $out->on('error', $handleError); - /** @var FileAnalyser $fileAnalyser */ $fileAnalyser = $container->getByType(FileAnalyser::class); - /** @var RuleRegistry $ruleRegistry */ $ruleRegistry = $container->getByType(RuleRegistry::class); - /** @var CollectorRegistry $collectorRegistry */ $collectorRegistry = $container->getByType(CollectorRegistry::class); - $in->on('data', function (array $json) use ($fileAnalyser, $ruleRegistry, $collectorRegistry, $out, $analysedFiles, $tmpFile, $insteadOfFile, $output): void { + $in->on('data', function (array $json) use ($fileAnalyser, $ruleRegistry, $collectorRegistry, $out, $analysedFiles, $output): void { $action = $json['action']; if ($action !== 'analyse') { return; @@ -203,32 +183,43 @@ private function runWorker( $internalErrorsCount = 0; $files = $json['files']; $errors = []; + $filteredPhpErrors = []; + $allPhpErrors = []; + $locallyIgnoredErrors = []; + $linesToIgnore = []; + $unmatchedLineIgnores = []; $collectedData = []; $dependencies = []; $exportedNodes = []; foreach ($files as $file) { try { - if ($file === $insteadOfFile) { - $file = $tmpFile; - } $fileAnalyserResult = $fileAnalyser->analyseFile($file, $analysedFiles, $ruleRegistry, $collectorRegistry, null); $fileErrors = $fileAnalyserResult->getErrors(); + $filteredPhpErrors = array_merge($filteredPhpErrors, $fileAnalyserResult->getFilteredPhpErrors()); + $allPhpErrors = array_merge($allPhpErrors, $fileAnalyserResult->getAllPhpErrors()); + $linesToIgnore[$file] = $fileAnalyserResult->getLinesToIgnore(); + $unmatchedLineIgnores[$file] = $fileAnalyserResult->getUnmatchedLineIgnores(); $dependencies[$file] = $fileAnalyserResult->getDependencies(); $exportedNodes[$file] = $fileAnalyserResult->getExportedNodes(); foreach ($fileErrors as $fileError) { $errors[] = $fileError; } + foreach ($fileAnalyserResult->getLocallyIgnoredErrors() as $locallyIgnoredError) { + $locallyIgnoredErrors[] = $locallyIgnoredError; + } foreach ($fileAnalyserResult->getCollectedData() as $data) { $collectedData[] = $data; } } catch (Throwable $t) { $this->errorCount++; $internalErrorsCount++; - $internalErrorMessage = sprintf('Internal error: %s in file %s', $t->getMessage(), $file); + $internalErrorMessage = sprintf('Internal error: %s while analysing file %s', $t->getMessage(), $file); - $bugReportUrl = 'https://github.com/phpstan/phpstan/issues/new?template=Bug_report.md'; + $bugReportUrl = 'https://github.com/phpstan/phpstan/issues/new?template=Bug_report.yaml'; if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { - $internalErrorMessage .= sprintf('%sPost the following stack trace to %s: %s%s', "\n\n", $bugReportUrl, "\n", $t->getTraceAsString()); + $trace = sprintf('## %s(%d)%s', $t->getFile(), $t->getLine(), "\n"); + $trace .= $t->getTraceAsString(); + $internalErrorMessage .= sprintf('%sPost the following stack trace to %s: %s%s', "\n\n", $bugReportUrl, "\n", $trace); } else { $internalErrorMessage .= sprintf('%sRun PHPStan with -v option and post the stack trace to:%s%s', "\n", "\n", $bugReportUrl); } @@ -241,37 +232,20 @@ private function runWorker( 'action' => 'result', 'result' => [ 'errors' => $errors, + 'filteredPhpErrors' => $filteredPhpErrors, + 'allPhpErrors' => $allPhpErrors, + 'locallyIgnoredErrors' => $locallyIgnoredErrors, + 'linesToIgnore' => $linesToIgnore, + 'unmatchedLineIgnores' => $unmatchedLineIgnores, 'collectedData' => $collectedData, + 'memoryUsage' => memory_get_peak_usage(true), 'dependencies' => $dependencies, 'exportedNodes' => $exportedNodes, - 'filesCount' => count($files), + 'files' => $files, 'internalErrorsCount' => $internalErrorsCount, ]]); }); $in->on('error', $handleError); } - /** - * @param string[] $analysedFiles - * @return string[] - */ - private function switchTmpFile( - array $analysedFiles, - ?string $insteadOfFile, - ?string $tmpFile, - ): array - { - $analysedFiles = array_values(array_filter($analysedFiles, static function (string $file) use ($insteadOfFile): bool { - if ($insteadOfFile === null) { - return true; - } - return $file !== $insteadOfFile; - })); - if ($tmpFile !== null) { - $analysedFiles[] = $tmpFile; - } - - return $analysedFiles; - } - } diff --git a/src/Dependency/DependencyResolver.php b/src/Dependency/DependencyResolver.php index 498ac6f4e9..0688b5d19e 100644 --- a/src/Dependency/DependencyResolver.php +++ b/src/Dependency/DependencyResolver.php @@ -17,11 +17,11 @@ use PHPStan\Node\InFunctionNode; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\ClosureType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\ParserNodeTypeToPHPStanType; use PHPStan\Type\Type; @@ -69,18 +69,27 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies $this->addClassToDependencies($className->toString(), $dependenciesReflections); } } elseif ($node instanceof InClassMethodNode) { - $nativeMethod = $scope->getFunction(); - if ($nativeMethod !== null) { - $parametersAcceptor = ParametersAcceptorSelector::selectSingle($nativeMethod->getVariants()); - $this->extractThrowType($nativeMethod->getThrowType(), $dependenciesReflections); - if ($parametersAcceptor instanceof ParametersAcceptorWithPhpDocs) { - $this->extractFromParametersAcceptor($parametersAcceptor, $dependenciesReflections); + $nativeMethod = $node->getMethodReflection(); + $parametersAcceptor = ParametersAcceptorSelector::selectSingle($nativeMethod->getVariants()); + $this->extractThrowType($nativeMethod->getThrowType(), $dependenciesReflections); + $this->extractFromParametersAcceptor($parametersAcceptor, $dependenciesReflections); + foreach ($nativeMethod->getAsserts()->getAll() as $assertTag) { + foreach ($assertTag->getType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + foreach ($assertTag->getOriginalType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($nativeMethod->getSelfOutType() !== null) { + foreach ($nativeMethod->getSelfOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } } elseif ($node instanceof ClassPropertyNode) { $nativeTypeNode = $node->getNativeType(); - if ($nativeTypeNode !== null && $scope->isInClass()) { - $nativeType = ParserNodeTypeToPHPStanType::resolve($nativeTypeNode, $scope->getClassReflection()); + if ($nativeTypeNode !== null) { + $nativeType = ParserNodeTypeToPHPStanType::resolve($nativeTypeNode, $node->getClassReflection()); foreach ($nativeType->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } @@ -92,34 +101,65 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies } } } elseif ($node instanceof InFunctionNode) { - $functionReflection = $scope->getFunction(); - if ($functionReflection !== null) { - $this->extractThrowType($functionReflection->getThrowType(), $dependenciesReflections); - $parametersAcceptor = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()); + $functionReflection = $node->getFunctionReflection(); + $this->extractThrowType($functionReflection->getThrowType(), $dependenciesReflections); + $parametersAcceptor = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()); - if ($parametersAcceptor instanceof ParametersAcceptorWithPhpDocs) { - $this->extractFromParametersAcceptor($parametersAcceptor, $dependenciesReflections); + $this->extractFromParametersAcceptor($parametersAcceptor, $dependenciesReflections); + foreach ($functionReflection->getAsserts()->getAll() as $assertTag) { + foreach ($assertTag->getType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); } - } - } elseif ($node instanceof Closure) { - /** @var ClosureType $closureType */ - $closureType = $scope->getType($node); - foreach ($closureType->getParameters() as $parameter) { - $referencedClasses = $parameter->getType()->getReferencedClasses(); - foreach ($referencedClasses as $referencedClass) { + foreach ($assertTag->getOriginalType()->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } + } elseif ($node instanceof Closure || $node instanceof Node\Expr\ArrowFunction) { + $closureType = $scope->getType($node); + if ($closureType instanceof ClosureType) { + foreach ($closureType->getParameters() as $parameter) { + $referencedClasses = $parameter->getType()->getReferencedClasses(); + foreach ($referencedClasses as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } - $returnTypeReferencedClasses = $closureType->getReturnType()->getReferencedClasses(); - foreach ($returnTypeReferencedClasses as $referencedClass) { - $this->addClassToDependencies($referencedClass, $dependenciesReflections); + $returnTypeReferencedClasses = $closureType->getReturnType()->getReferencedClasses(); + foreach ($returnTypeReferencedClasses as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } } } elseif ($node instanceof Node\Expr\FuncCall) { $functionName = $node->name; if ($functionName instanceof Node\Name) { try { - $dependenciesReflections[] = $this->getFunctionReflection($functionName, $scope); + $functionReflection = $this->getFunctionReflection($functionName, $scope); + $dependenciesReflections[] = $functionReflection; + + foreach ($functionReflection->getVariants() as $functionVariant) { + foreach ($functionVariant->getParameters() as $parameter) { + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { + continue; + } + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + } + + foreach ($functionReflection->getAsserts()->getAll() as $assertTag) { + foreach ($assertTag->getType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + foreach ($assertTag->getOriginalType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } } catch (FunctionNotFoundException) { // pass } @@ -132,6 +172,23 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies foreach ($referencedClasses as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } + + foreach ($variant->getParameters() as $parameter) { + if (!$parameter instanceof ParameterReflectionWithPhpDocs) { + continue; + } + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { + continue; + } + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } } } } @@ -156,6 +213,36 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies $methodReflection = $scope->getMethodReflection($calledOnType, $node->name->toString()); if ($methodReflection !== null) { $this->addClassToDependencies($methodReflection->getDeclaringClass()->getName(), $dependenciesReflections); + foreach ($methodReflection->getVariants() as $methodVariant) { + foreach ($methodVariant->getParameters() as $parameter) { + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { + continue; + } + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + } + + foreach ($methodReflection->getAsserts()->getAll() as $assertTag) { + foreach ($assertTag->getType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + foreach ($assertTag->getOriginalType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + + if ($methodReflection->getSelfOutType() !== null) { + foreach ($methodReflection->getSelfOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } } } } elseif ($node instanceof Node\Expr\PropertyFetch) { @@ -198,12 +285,42 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies if ($methodClassReflection->hasMethod($node->name->toString())) { $methodReflection = $methodClassReflection->getMethod($node->name->toString(), $scope); $this->addClassToDependencies($methodReflection->getDeclaringClass()->getName(), $dependenciesReflections); + foreach ($methodReflection->getVariants() as $methodVariant) { + foreach ($methodVariant->getParameters() as $parameter) { + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { + continue; + } + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + } } } } else { $methodReflection = $scope->getMethodReflection($scope->getType($node->class), $node->name->toString()); if ($methodReflection !== null) { $this->addClassToDependencies($methodReflection->getDeclaringClass()->getName(), $dependenciesReflections); + foreach ($methodReflection->getVariants() as $methodVariant) { + foreach ($methodVariant->getParameters() as $parameter) { + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { + continue; + } + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + } } } } @@ -274,6 +391,22 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies && $node->class instanceof Node\Name ) { $this->addClassToDependencies($scope->resolveName($node->class), $dependenciesReflections); + } elseif ($node instanceof Node\Stmt\Trait_ && $node->namespacedName !== null) { + try { + $classReflection = $this->reflectionProvider->getClass($node->namespacedName->toString()); + + foreach ($classReflection->getRequireImplementsTags() as $implementsTag) { + foreach ($implementsTag->getType()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + } catch (ClassNotFoundException) { + // pass + } } elseif ($node instanceof Node\Stmt\TraitUse) { foreach ($node->traits as $traitName) { $this->addClassToDependencies($traitName->toString(), $dependenciesReflections); @@ -312,12 +445,13 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies } elseif ($node instanceof Foreach_) { $exprType = $scope->getType($node->expr); if ($node->keyVar !== null) { - foreach ($exprType->getIterableKeyType()->getReferencedClasses() as $referencedClass) { + + foreach ($scope->getIterableKeyType($exprType)->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } - foreach ($exprType->getIterableValueType()->getReferencedClasses() as $referencedClass) { + foreach ($scope->getIterableValueType($exprType)->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } elseif ( @@ -350,11 +484,7 @@ private function considerArrayForCallableTest(Scope $scope, Array_ $arrayNode): } $itemType = $scope->getType($items[0]->value); - if (!$itemType instanceof ConstantStringType) { - return false; - } - - return $itemType->isClassString(); + return $itemType->isClassStringType()->yes(); } /** @@ -388,6 +518,15 @@ private function addClassToDependencies(string $className, array &$dependenciesR } } + foreach ($classReflection->getRequireExtendsTags() as $extendsTag) { + foreach ($extendsTag->getType()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + } + foreach ($classReflection->getTemplateTags() as $templateTag) { foreach ($templateTag->getBound()->getReferencedClasses() as $referencedClass) { if (!$this->reflectionProvider->hasClass($referencedClass)) { @@ -398,7 +537,20 @@ private function addClassToDependencies(string $className, array &$dependenciesR } foreach ($classReflection->getPropertyTags() as $propertyTag) { - foreach ($propertyTag->getType()->getReferencedClasses() as $referencedClass) { + if ($propertyTag->isReadable()) { + foreach ($propertyTag->getReadableType()->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } + } + + if (!$propertyTag->isWritable()) { + continue; + } + + foreach ($propertyTag->getWritableType()->getReferencedClasses() as $referencedClass) { if (!$this->reflectionProvider->hasClass($referencedClass)) { continue; } @@ -450,6 +602,13 @@ private function addClassToDependencies(string $className, array &$dependenciesR } } + $phpDoc = $classReflection->getResolvedPhpDoc(); + if ($phpDoc !== null) { + foreach ($phpDoc->getTypeAliasImportTags() as $importTag) { + $dependenciesReflections[] = $this->reflectionProvider->getClass($importTag->getImportedFrom()); + } + } + $classReflection = $classReflection->getParentClass(); } while ($classReflection !== null); } @@ -476,6 +635,18 @@ private function extractFromParametersAcceptor( foreach ($referencedClasses as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } + + if ($parameter->getOutType() !== null) { + foreach ($parameter->getOutType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } + } + if ($parameter->getClosureThisType() === null) { + continue; + } + foreach ($parameter->getClosureThisType()->getReferencedClasses() as $referencedClass) { + $this->addClassToDependencies($referencedClass, $dependenciesReflections); + } } $returnTypeReferencedClasses = array_merge( diff --git a/src/Dependency/ExportedNodeFetcher.php b/src/Dependency/ExportedNodeFetcher.php index 4c531409de..4d562f1275 100644 --- a/src/Dependency/ExportedNodeFetcher.php +++ b/src/Dependency/ExportedNodeFetcher.php @@ -2,7 +2,6 @@ namespace PHPStan\Dependency; -use PhpParser\Node; use PhpParser\NodeTraverser; use PHPStan\Parser\Parser; use PHPStan\Parser\ParserErrorsException; @@ -26,7 +25,6 @@ public function fetchNodes(string $fileName): array $nodeTraverser->addVisitor($this->visitor); try { - /** @var Node[] $ast */ $ast = $this->parser->parseFile($fileName); } catch (ParserErrorsException) { return []; diff --git a/src/DependencyInjection/ConditionalTagsExtension.php b/src/DependencyInjection/ConditionalTagsExtension.php index 0b9248a1d1..358e673e10 100644 --- a/src/DependencyInjection/ConditionalTagsExtension.php +++ b/src/DependencyInjection/ConditionalTagsExtension.php @@ -8,9 +8,13 @@ use PHPStan\Analyser\TypeSpecifierFactory; use PHPStan\Broker\BrokerFactory; use PHPStan\Collectors\RegistryFactory as CollectorRegistryFactory; +use PHPStan\DependencyInjection\Type\LazyDynamicThrowTypeExtensionProvider; use PHPStan\Parser\RichParser; +use PHPStan\PhpDoc\StubFilesExtension; use PHPStan\PhpDoc\TypeNodeResolverExtension; +use PHPStan\Rules\Constants\AlwaysUsedClassConstantsExtensionProvider; use PHPStan\Rules\LazyRegistry; +use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; use PHPStan\ShouldNotHappenException; use function array_reduce; use function count; @@ -29,14 +33,22 @@ public function getConfigSchema(): Nette\Schema\Schema BrokerFactory::DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG => $bool, BrokerFactory::DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG => $bool, BrokerFactory::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG => $bool, + BrokerFactory::EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG => $bool, BrokerFactory::OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG => $bool, + BrokerFactory::ALLOWED_SUB_TYPES_CLASS_REFLECTION_EXTENSION_TAG => $bool, LazyRegistry::RULE_TAG => $bool, TypeNodeResolverExtension::EXTENSION_TAG => $bool, + StubFilesExtension::EXTENSION_TAG => $bool, + AlwaysUsedClassConstantsExtensionProvider::EXTENSION_TAG => $bool, + ReadWritePropertiesExtensionProvider::EXTENSION_TAG => $bool, TypeSpecifierFactory::FUNCTION_TYPE_SPECIFYING_EXTENSION_TAG => $bool, TypeSpecifierFactory::METHOD_TYPE_SPECIFYING_EXTENSION_TAG => $bool, TypeSpecifierFactory::STATIC_METHOD_TYPE_SPECIFYING_EXTENSION_TAG => $bool, RichParser::VISITOR_SERVICE_TAG => $bool, CollectorRegistryFactory::COLLECTOR_TAG => $bool, + LazyDynamicThrowTypeExtensionProvider::FUNCTION_TAG => $bool, + LazyDynamicThrowTypeExtensionProvider::METHOD_TAG => $bool, + LazyDynamicThrowTypeExtensionProvider::STATIC_METHOD_TAG => $bool, ])->min(1)); } diff --git a/src/DependencyInjection/Configurator.php b/src/DependencyInjection/Configurator.php index 1055ec5b09..1c8b1e7bc6 100644 --- a/src/DependencyInjection/Configurator.php +++ b/src/DependencyInjection/Configurator.php @@ -3,10 +3,15 @@ namespace PHPStan\DependencyInjection; use Nette\DI\Config\Loader; +use Nette\DI\Container as OriginalNetteContainer; use Nette\DI\ContainerLoader; -use PHPStan\File\FileReader; +use PHPStan\File\CouldNotReadFileException; use function array_keys; -use function sha1; +use function error_reporting; +use function restore_error_handler; +use function set_error_handler; +use function sha1_file; +use const E_USER_DEPRECATED; use const PHP_RELEASE_VERSION; use const PHP_VERSION_ID; @@ -60,6 +65,26 @@ public function loadContainer(): string ); } + public function createContainer(bool $initialize = true): OriginalNetteContainer + { + set_error_handler(static function (int $errno): bool { + if ((error_reporting() & $errno) === 0) { + // silence @ operator + return true; + } + + return $errno === E_USER_DEPRECATED; + }); + + try { + $container = parent::createContainer($initialize); + } finally { + restore_error_handler(); + } + + return $container; + } + /** * @return string[] */ @@ -67,7 +92,13 @@ private function getAllConfigFilesHashes(): array { $hashes = []; foreach ($this->allConfigFiles as $file) { - $hashes[$file] = sha1(FileReader::read($file)); + $hash = sha1_file($file); + + if ($hash === false) { + throw new CouldNotReadFileException($file); + } + + $hashes[$file] = $hash; } return $hashes; diff --git a/src/DependencyInjection/ContainerFactory.php b/src/DependencyInjection/ContainerFactory.php index 6b22914ef0..cb3bf3006a 100644 --- a/src/DependencyInjection/ContainerFactory.php +++ b/src/DependencyInjection/ContainerFactory.php @@ -2,10 +2,18 @@ namespace PHPStan\DependencyInjection; +use Nette\Bootstrap\Extensions\PhpExtension; use Nette\DI\Config\Adapters\PhpAdapter; +use Nette\DI\Definitions\Statement; use Nette\DI\Extensions\ExtensionsExtension; -use Nette\DI\Extensions\PhpExtension; use Nette\DI\Helpers; +use Nette\Schema\Context as SchemaContext; +use Nette\Schema\Elements\AnyOf; +use Nette\Schema\Elements\Structure; +use Nette\Schema\Elements\Type; +use Nette\Schema\Expect; +use Nette\Schema\Processor; +use Nette\Schema\Schema; use Nette\Utils\Strings; use Nette\Utils\Validators; use Phar; @@ -17,9 +25,15 @@ use PHPStan\Broker\Broker; use PHPStan\Command\CommandHelper; use PHPStan\File\FileHelper; +use PHPStan\Node\Printer\Printer; use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\PhpVersionStaticAccessor; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\ObjectType; use Symfony\Component\Finder\Finder; use function array_diff_key; use function array_map; @@ -28,13 +42,16 @@ use function count; use function dirname; use function extension_loaded; +use function getenv; use function ini_get; +use function is_array; use function is_dir; use function is_file; use function is_readable; +use function spl_object_id; use function sprintf; use function str_ends_with; -use function sys_get_temp_dir; +use function substr; use function time; use function unlink; @@ -48,6 +65,8 @@ class ContainerFactory private string $configDirectory; + private static ?int $lastInitializedContainerId = null; + /** @api */ public function __construct(private string $currentWorkingDirectory, private bool $checkDuplicateFiles = false) { @@ -80,15 +99,14 @@ public function create( string $usedLevel = CommandHelper::DEFAULT_LEVEL, ?string $generateBaselineFile = null, ?string $cliAutoloadFile = null, - ?string $singleReflectionFile = null, - ?string $singleReflectionInsteadOfFile = null, ): Container { - $allConfigFiles = $this->detectDuplicateIncludedFiles( - $additionalConfigFiles, + [$allConfigFiles, $projectConfig] = $this->detectDuplicateIncludedFiles( + array_merge([__DIR__ . '/../../conf/parametersSchema.neon'], $additionalConfigFiles), [ 'rootDir' => $this->rootDirectory, 'currentWorkingDirectory' => $this->currentWorkingDirectory, + 'env' => getenv(), ], ); @@ -110,17 +128,16 @@ public function create( 'cliArgumentsVariablesRegistered' => ini_get('register_argc_argv') === '1', 'tmpDir' => $tempDirectory, 'additionalConfigFiles' => $additionalConfigFiles, + 'allConfigFiles' => $allConfigFiles, 'composerAutoloaderProjectPaths' => $composerAutoloaderProjectPaths, 'generateBaselineFile' => $generateBaselineFile, 'usedLevel' => $usedLevel, 'cliAutoloadFile' => $cliAutoloadFile, - 'fixerTmpDir' => sys_get_temp_dir() . '/phpstan-fixer', ]); $configurator->addDynamicParameters([ - 'singleReflectionFile' => $singleReflectionFile, - 'singleReflectionInsteadOfFile' => $singleReflectionInsteadOfFile, 'analysedPaths' => $analysedPaths, 'analysedPathsFromConfig' => $analysedPathsFromConfig, + 'env' => getenv(), ]); $configurator->addConfig($this->configDirectory . '/config.neon'); foreach ($additionalConfigFiles as $additionalConfigFile) { @@ -129,7 +146,22 @@ public function create( $configurator->setAllConfigFiles($allConfigFiles); - $container = $configurator->createContainer(); + $container = $configurator->createContainer()->getByType(Container::class); + $this->validateParameters($container->getParameters(), $projectConfig['parametersSchema']); + self::postInitializeContainer($container); + + return $container; + } + + /** @internal */ + public static function postInitializeContainer(Container $container): void + { + $containerId = spl_object_id($container); + if ($containerId === self::$lastInitializedContainerId) { + return; + } + + self::$lastInitializedContainerId = $containerId; /** @var SourceLocator $sourceLocator */ $sourceLocator = $container->getService('betterReflectionSourceLocator'); @@ -146,17 +178,19 @@ public function create( $reflector, $phpParser, $container->getByType(PhpStormStubsSourceStubber::class), + $container->getByType(Printer::class), ); - /** @var Broker $broker */ $broker = $container->getByType(Broker::class); Broker::registerInstance($broker); ReflectionProviderStaticAccessor::registerInstance($container->getByType(ReflectionProvider::class)); + PhpVersionStaticAccessor::registerInstance($container->getByType(PhpVersion::class)); + ObjectType::resetCaches(); $container->getService('typeSpecifier'); - BleedingEdgeToggle::setBleedingEdge($container->parameters['featureToggles']['bleedingEdge']); - - return $container->getByType(Container::class); + BleedingEdgeToggle::setBleedingEdge($container->getParameter('featureToggles')['bleedingEdge']); + AccessoryArrayListType::setListTypeEnabled($container->getParameter('featureToggles')['listType']); + TemplateTypeVariance::setInvarianceCompositionEnabled($container->getParameter('featureToggles')['invarianceComposition']); } public function clearOldContainers(string $tempDirectory): void @@ -212,8 +246,8 @@ public function getConfigDirectory(): string /** * @param string[] $configFiles - * @param array $loaderParameters - * @return string[] + * @param array $loaderParameters + * @return array{list, array} * @throws DuplicateIncludedFilesException */ private function detectDuplicateIncludedFiles( @@ -224,19 +258,24 @@ private function detectDuplicateIncludedFiles( $neonAdapter = new NeonAdapter(); $phpAdapter = new PhpAdapter(); $allConfigFiles = []; + $configArray = []; foreach ($configFiles as $configFile) { - $allConfigFiles = array_merge($allConfigFiles, self::getConfigFiles($this->fileHelper, $neonAdapter, $phpAdapter, $configFile, $loaderParameters, null)); + [$tmpConfigFiles, $tmpConfigArray] = self::getConfigFiles($this->fileHelper, $neonAdapter, $phpAdapter, $configFile, $loaderParameters, null); + $allConfigFiles = array_merge($allConfigFiles, $tmpConfigFiles); + + /** @var array $configArray */ + $configArray = \Nette\Schema\Helpers::merge($tmpConfigArray, $configArray); } $normalized = array_map(fn (string $file): string => $this->fileHelper->normalizePath($file), $allConfigFiles); $deduplicated = array_unique($normalized); if (count($normalized) <= count($deduplicated)) { - return $normalized; + return [$normalized, $configArray]; } if (!$this->checkDuplicateFiles) { - return $normalized; + return [$normalized, $configArray]; } $duplicateFiles = array_unique(array_diff_key($normalized, $deduplicated)); @@ -246,7 +285,7 @@ private function detectDuplicateIncludedFiles( /** * @param array $loaderParameters - * @return string[] + * @return array{list, array} */ private static function getConfigFiles( FileHelper $fileHelper, @@ -258,10 +297,10 @@ private static function getConfigFiles( ): array { if ($generateBaselineFile === $fileHelper->normalizePath($configFile)) { - return []; + return [[], []]; } if (!is_file($configFile) || !is_readable($configFile)) { - return []; + return [[], []]; } if (str_ends_with($configFile, '.php')) { @@ -275,11 +314,15 @@ private static function getConfigFiles( $includes = Helpers::expand($data['includes'], $loaderParameters); foreach ($includes as $include) { $include = self::expandIncludedFile($include, $configFile); - $allConfigFiles = array_merge($allConfigFiles, self::getConfigFiles($fileHelper, $neonAdapter, $phpAdapter, $include, $loaderParameters, $generateBaselineFile)); + [$tmpConfigFiles, $tmpConfigArray] = self::getConfigFiles($fileHelper, $neonAdapter, $phpAdapter, $include, $loaderParameters, $generateBaselineFile); + $allConfigFiles = array_merge($allConfigFiles, $tmpConfigFiles); + + /** @var array $data */ + $data = \Nette\Schema\Helpers::merge($tmpConfigArray, $data); } } - return $allConfigFiles; + return [$allConfigFiles, $data]; } private static function expandIncludedFile(string $includedFile, string $mainFile): string @@ -289,4 +332,92 @@ private static function expandIncludedFile(string $includedFile, string $mainFil : dirname($mainFile) . '/' . $includedFile; } + /** + * @param array $parameters + * @param array $parametersSchema + */ + private function validateParameters(array $parameters, array $parametersSchema): void + { + if (!(bool) $parameters['__validate']) { + return; + } + + $schema = $this->processArgument( + new Statement('schema', [ + new Statement('structure', [$parametersSchema]), + ]), + ); + $processor = new Processor(); + $processor->onNewContext[] = static function (SchemaContext $context): void { + $context->path = ['parameters']; + }; + $processor->process($schema, $parameters); + } + + /** + * @param Statement[] $statements + */ + private function processSchema(array $statements, bool $required = true): Schema + { + if (count($statements) === 0) { + throw new ShouldNotHappenException(); + } + + $parameterSchema = null; + foreach ($statements as $statement) { + $processedArguments = array_map(fn ($argument) => $this->processArgument($argument), $statement->arguments); + if ($parameterSchema === null) { + /** @var Type|AnyOf|Structure $parameterSchema */ + $parameterSchema = Expect::{$statement->getEntity()}(...$processedArguments); + } else { + $parameterSchema->{$statement->getEntity()}(...$processedArguments); + } + } + + if ($required) { + $parameterSchema->required(); + } + + return $parameterSchema; + } + + /** + * @param mixed $argument + * @return mixed + */ + private function processArgument($argument, bool $required = true) + { + if ($argument instanceof Statement) { + if ($argument->entity === 'schema') { + $arguments = []; + foreach ($argument->arguments as $schemaArgument) { + if (!$schemaArgument instanceof Statement) { + throw new ShouldNotHappenException('schema() should contain another statement().'); + } + + $arguments[] = $schemaArgument; + } + + if (count($arguments) === 0) { + throw new ShouldNotHappenException('schema() should have at least one argument.'); + } + + return $this->processSchema($arguments, $required); + } + + return $this->processSchema([$argument], $required); + } elseif (is_array($argument)) { + $processedArray = []; + foreach ($argument as $key => $val) { + $required = $key[0] !== '?'; + $key = $required ? $key : substr($key, 1); + $processedArray[$key] = $this->processArgument($val, $required); + } + + return $processedArray; + } + + return $argument; + } + } diff --git a/src/DependencyInjection/DerivativeContainerFactory.php b/src/DependencyInjection/DerivativeContainerFactory.php index c859340da9..48270f714d 100644 --- a/src/DependencyInjection/DerivativeContainerFactory.php +++ b/src/DependencyInjection/DerivativeContainerFactory.php @@ -23,8 +23,6 @@ public function __construct( private string $usedLevel, private ?string $generateBaselineFile, private ?string $cliAutoloadFile, - private ?string $singleReflectionFile, - private ?string $singleReflectionInsteadOfFile, ) { } @@ -47,8 +45,6 @@ public function create(array $additionalConfigFiles): Container $this->usedLevel, $this->generateBaselineFile, $this->cliAutoloadFile, - $this->singleReflectionFile, - $this->singleReflectionInsteadOfFile, ); } diff --git a/src/DependencyInjection/LoaderFactory.php b/src/DependencyInjection/LoaderFactory.php index d12900f5f1..600fede435 100644 --- a/src/DependencyInjection/LoaderFactory.php +++ b/src/DependencyInjection/LoaderFactory.php @@ -4,6 +4,7 @@ use Nette\DI\Config\Loader; use PHPStan\File\FileHelper; +use function getenv; class LoaderFactory { @@ -25,6 +26,7 @@ public function createLoader(): Loader $loader->setParameters([ 'rootDir' => $this->rootDir, 'currentWorkingDirectory' => $this->currentWorkingDirectory, + 'env' => getenv(), ]); return $loader; diff --git a/src/DependencyInjection/NeonAdapter.php b/src/DependencyInjection/NeonAdapter.php index 1f52ff84f5..15fb210b33 100644 --- a/src/DependencyInjection/NeonAdapter.php +++ b/src/DependencyInjection/NeonAdapter.php @@ -22,13 +22,14 @@ use function is_string; use function ltrim; use function sprintf; -use function strpos; +use function str_contains; +use function str_starts_with; use function substr; class NeonAdapter implements Adapter { - public const CACHE_KEY = 'v17-validate-schema'; + public const CACHE_KEY = 'v25-nette-di-again'; private const PREVENT_MERGING_SUFFIX = '!'; @@ -112,6 +113,7 @@ public function process(array $arr, string $fileKey, string $file): array '[parameters][scanFiles][]', '[parameters][scanDirectories][]', '[parameters][tmpDir]', + '[parameters][pro][tmpDir]', '[parameters][memoryLimitFile]', '[parameters][benchmarkFile]', '[parameters][stubFiles][]', @@ -120,7 +122,7 @@ public function process(array $arr, string $fileKey, string $file): array '[parameters][symfony][container_xml_path]', '[parameters][symfony][containerXmlPath]', '[parameters][doctrine][objectManagerLoader]', - ], true) && is_string($val) && strpos($val, '%') === false && strpos($val, '*') !== 0) { + ], true) && is_string($val) && !str_contains($val, '%') && !str_starts_with($val, '*')) { $fileHelper = $this->createFileHelperByFile($file); $val = $fileHelper->normalizePath($fileHelper->absolutizePath($val)); } diff --git a/src/DependencyInjection/Nette/NetteContainer.php b/src/DependencyInjection/Nette/NetteContainer.php index 9d9466c8d7..b5e488ca32 100644 --- a/src/DependencyInjection/Nette/NetteContainer.php +++ b/src/DependencyInjection/Nette/NetteContainer.php @@ -64,12 +64,12 @@ public function getServicesByTag(string $tagName): array */ public function getParameters(): array { - return $this->container->parameters; + return $this->container->getParameters(); } public function hasParameter(string $parameterName): bool { - return array_key_exists($parameterName, $this->container->parameters); + return array_key_exists($parameterName, $this->container->getParameters()); } /** @@ -81,7 +81,7 @@ public function getParameter(string $parameterName) throw new ParameterNotFoundException($parameterName); } - return $this->container->parameters[$parameterName]; + return $this->container->getParameter($parameterName); } /** diff --git a/src/DependencyInjection/ParametersSchemaExtension.php b/src/DependencyInjection/ParametersSchemaExtension.php index f9f98d4bd0..2b19bafe56 100644 --- a/src/DependencyInjection/ParametersSchemaExtension.php +++ b/src/DependencyInjection/ParametersSchemaExtension.php @@ -4,19 +4,8 @@ use Nette\DI\CompilerExtension; use Nette\DI\Definitions\Statement; -use Nette\Schema\Context as SchemaContext; -use Nette\Schema\DynamicParameter; -use Nette\Schema\Elements\AnyOf; -use Nette\Schema\Elements\Structure; -use Nette\Schema\Elements\Type; use Nette\Schema\Expect; -use Nette\Schema\Processor; use Nette\Schema\Schema; -use PHPStan\ShouldNotHappenException; -use function array_map; -use function count; -use function is_array; -use function substr; class ParametersSchemaExtension extends CompilerExtension { @@ -26,95 +15,4 @@ public function getConfigSchema(): Schema return Expect::arrayOf(Expect::type(Statement::class))->min(1); } - public function loadConfiguration(): void - { - $builder = $this->getContainerBuilder(); - if (!$builder->parameters['__validate']) { - return; - } - - /** @var mixed[] $config */ - $config = $this->config; - $config['analysedPaths'] = new Statement(DynamicParameter::class); - $config['analysedPathsFromConfig'] = new Statement(DynamicParameter::class); - $config['singleReflectionFile'] = new Statement(DynamicParameter::class); - $config['singleReflectionInsteadOfFile'] = new Statement(DynamicParameter::class); - $schema = $this->processArgument( - new Statement('schema', [ - new Statement('structure', [$config]), - ]), - ); - $processor = new Processor(); - $processor->onNewContext[] = static function (SchemaContext $context): void { - $context->path = ['parameters']; - }; - $processor->process($schema, $builder->parameters); - } - - /** - * @param Statement[] $statements - */ - private function processSchema(array $statements, bool $required = true): Schema - { - if (count($statements) === 0) { - throw new ShouldNotHappenException(); - } - - $parameterSchema = null; - foreach ($statements as $statement) { - $processedArguments = array_map(fn ($argument) => $this->processArgument($argument), $statement->arguments); - if ($parameterSchema === null) { - /** @var Type|AnyOf|Structure $parameterSchema */ - $parameterSchema = Expect::{$statement->getEntity()}(...$processedArguments); - } else { - $parameterSchema->{$statement->getEntity()}(...$processedArguments); - } - } - - if ($required) { - $parameterSchema->required(); - } - - return $parameterSchema; - } - - /** - * @param mixed $argument - * @return mixed - */ - private function processArgument($argument, bool $required = true) - { - if ($argument instanceof Statement) { - if ($argument->entity === 'schema') { - $arguments = []; - foreach ($argument->arguments as $schemaArgument) { - if (!$schemaArgument instanceof Statement) { - throw new ShouldNotHappenException('schema() should contain another statement().'); - } - - $arguments[] = $schemaArgument; - } - - if (count($arguments) === 0) { - throw new ShouldNotHappenException('schema() should have at least one argument.'); - } - - return $this->processSchema($arguments, $required); - } - - return $this->processSchema([$argument], $required); - } elseif (is_array($argument)) { - $processedArray = []; - foreach ($argument as $key => $val) { - $required = $key[0] !== '?'; - $key = $required ? $key : substr($key, 1); - $processedArray[$key] = $this->processArgument($val, $required); - } - - return $processedArray; - } - - return $argument; - } - } diff --git a/src/DependencyInjection/ProjectConfigHelper.php b/src/DependencyInjection/ProjectConfigHelper.php new file mode 100644 index 0000000000..0ac6ef97a2 --- /dev/null +++ b/src/DependencyInjection/ProjectConfigHelper.php @@ -0,0 +1,66 @@ + $projectConfig + * @return list + */ + public static function getServiceClassNames(array $projectConfig): array + { + $services = array_merge( + $projectConfig['services'] ?? [], + $projectConfig['rules'] ?? [], + ); + $classes = []; + foreach ($services as $service) { + $classes = array_merge($classes, self::getClassesFromConfigDefinition($service)); + if (!is_array($service)) { + continue; + } + + foreach (['class', 'factory', 'implement'] as $key) { + if (!isset($service[$key])) { + continue; + } + + $classes = array_merge($classes, self::getClassesFromConfigDefinition($service[$key])); + } + } + + return array_values(array_unique($classes)); + } + + /** + * @param mixed $definition + * @return string[] + */ + private static function getClassesFromConfigDefinition($definition): array + { + if (is_string($definition)) { + return [$definition]; + } + + if ($definition instanceof Statement) { + $entity = $definition->entity; + if (is_string($entity)) { + return [$entity]; + } elseif (is_array($entity) && isset($entity[0]) && is_string($entity[0])) { + return [$entity[0]]; + } + } + + return []; + } + +} diff --git a/src/DependencyInjection/Reflection/DirectClassReflectionExtensionRegistryProvider.php b/src/DependencyInjection/Reflection/DirectClassReflectionExtensionRegistryProvider.php deleted file mode 100644 index fa557b070a..0000000000 --- a/src/DependencyInjection/Reflection/DirectClassReflectionExtensionRegistryProvider.php +++ /dev/null @@ -1,53 +0,0 @@ -broker = $broker; - } - - public function addPropertiesClassReflectionExtension(PropertiesClassReflectionExtension $extension): void - { - $this->propertiesClassReflectionExtensions[] = $extension; - } - - public function addMethodsClassReflectionExtension(MethodsClassReflectionExtension $extension): void - { - $this->methodsClassReflectionExtensions[] = $extension; - } - - public function getRegistry(): ClassReflectionExtensionRegistry - { - return new ClassReflectionExtensionRegistry( - $this->broker, - $this->propertiesClassReflectionExtensions, - $this->methodsClassReflectionExtensions, - ); - } - -} diff --git a/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php b/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php index 259600a280..899b9153d4 100644 --- a/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php +++ b/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php @@ -9,6 +9,8 @@ use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension; use PHPStan\Reflection\ClassReflectionExtensionRegistry; use PHPStan\Reflection\Php\PhpClassReflectionExtension; +use PHPStan\Reflection\RequireExtension\RequireExtendsMethodsClassReflectionExtension; +use PHPStan\Reflection\RequireExtension\RequireExtendsPropertiesClassReflectionExtension; use function array_merge; class LazyClassReflectionExtensionRegistryProvider implements ClassReflectionExtensionRegistryProvider @@ -31,6 +33,9 @@ public function getRegistry(): ClassReflectionExtensionRegistry $this->container->getByType(Broker::class), array_merge([$phpClassReflectionExtension], $this->container->getServicesByTag(BrokerFactory::PROPERTIES_CLASS_REFLECTION_EXTENSION_TAG), [$annotationsPropertiesClassReflectionExtension]), array_merge([$phpClassReflectionExtension], $this->container->getServicesByTag(BrokerFactory::METHODS_CLASS_REFLECTION_EXTENSION_TAG), [$annotationsMethodsClassReflectionExtension]), + $this->container->getServicesByTag(BrokerFactory::ALLOWED_SUB_TYPES_CLASS_REFLECTION_EXTENSION_TAG), + $this->container->getByType(RequireExtendsPropertiesClassReflectionExtension::class), + $this->container->getByType(RequireExtendsMethodsClassReflectionExtension::class), ); } diff --git a/src/DependencyInjection/Type/ExpressionTypeResolverExtensionRegistryProvider.php b/src/DependencyInjection/Type/ExpressionTypeResolverExtensionRegistryProvider.php new file mode 100644 index 0000000000..5eb6180ca0 --- /dev/null +++ b/src/DependencyInjection/Type/ExpressionTypeResolverExtensionRegistryProvider.php @@ -0,0 +1,12 @@ +registry === null) { + $this->registry = new ExpressionTypeResolverExtensionRegistry( + $this->container->getServicesByTag(BrokerFactory::EXPRESSION_TYPE_RESOLVER_EXTENSION_TAG), + ); + } + + return $this->registry; + } + +} diff --git a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php index 756d1a7bd2..5432fc0eec 100644 --- a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php +++ b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php @@ -20,9 +20,11 @@ use PHPStan\PhpDocParser\Parser\ConstExprParser; use PHPStan\PhpDocParser\Parser\TypeParser; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\PhpVersionStaticAccessor; use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider; use PHPStan\Reflection\ReflectionProvider\DummyReflectionProvider; use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\Type\Constant\OversizedArrayBuilder; use PHPStan\Type\DirectTypeAliasResolverProvider; use PHPStan\Type\OperatorTypeSpecifyingExtensionRegistry; use PHPStan\Type\Type; @@ -58,12 +60,13 @@ public function loadConfiguration(): void $reflectionProvider = new DummyReflectionProvider(); $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider); ReflectionProviderStaticAccessor::registerInstance($reflectionProvider); + PhpVersionStaticAccessor::registerInstance(new PhpVersion(PHP_VERSION_ID)); $constantResolver = new ConstantResolver($reflectionProviderProvider, []); $ignoredRegexValidator = new IgnoredRegexValidator( $parser, new TypeStringResolver( new Lexer(), - new TypeParser(new ConstExprParser()), + new TypeParser(new ConstExprParser($builder->parameters['featureToggles']['unescapeStrings'])), new TypeNodeResolver( new DirectTypeNodeResolverExtensionRegistryProvider( new class implements TypeNodeResolverExtensionRegistry { @@ -97,7 +100,7 @@ public function getRegistry(): OperatorTypeSpecifyingExtensionRegistry return new OperatorTypeSpecifyingExtensionRegistry(null, []); } - }), + }, new OversizedArrayBuilder()), ), ), ); @@ -110,8 +113,10 @@ public function getRegistry(): OperatorTypeSpecifyingExtensionRegistry } if (isset($ignoreError['messages'])) { $ignoreMessages = $ignoreError['messages']; - } else { + } elseif (isset($ignoreError['message'])) { $ignoreMessages = [$ignoreError['message']]; + } else { + continue; } } else { $ignoreMessages = [$ignoreError]; diff --git a/src/File/FileExcluder.php b/src/File/FileExcluder.php index ff4f63b613..2ea5271d5d 100644 --- a/src/File/FileExcluder.php +++ b/src/File/FileExcluder.php @@ -2,13 +2,11 @@ namespace PHPStan\File; -use PHPStan\PhpDoc\StubFilesProvider; -use function array_merge; use function fnmatch; use function in_array; use function preg_match; +use function str_starts_with; use function strlen; -use function strpos; use const DIRECTORY_SEPARATOR; use const FNM_CASEFOLD; use const FNM_NOESCAPE; @@ -36,11 +34,10 @@ class FileExcluder */ public function __construct( FileHelper $fileHelper, - StubFilesProvider $stubFilesProvider, array $analyseExcludes, ) { - foreach (array_merge($analyseExcludes, $stubFilesProvider->getStubFiles()) as $exclude) { + foreach ($analyseExcludes as $exclude) { $len = strlen($exclude); $trailingDirSeparator = ($len > 0 && in_array($exclude[$len - 1], ['\\', '/'], true)); @@ -68,7 +65,7 @@ public function __construct( public function isExcludedFromAnalysing(string $file): bool { foreach ($this->literalAnalyseExcludes as $exclude) { - if (strpos($file, $exclude) === 0) { + if (str_starts_with($file, $exclude)) { return true; } } diff --git a/src/File/FileFinder.php b/src/File/FileFinder.php index d549d9c2cc..6ca9db6ddd 100644 --- a/src/File/FileFinder.php +++ b/src/File/FileFinder.php @@ -4,6 +4,7 @@ use Symfony\Component\Finder\Finder; use function array_filter; +use function array_unique; use function array_values; use function file_exists; use function implode; @@ -45,7 +46,7 @@ public function findFiles(array $paths): FileFinderResult } } - $files = array_values(array_filter($files, fn (string $file): bool => !$this->fileExcluder->isExcludedFromAnalysing($file))); + $files = array_values(array_unique(array_filter($files, fn (string $file): bool => !$this->fileExcluder->isExcludedFromAnalysing($file)))); return new FileFinderResult($files, $onlyFiles); } diff --git a/src/File/FileHelper.php b/src/File/FileHelper.php index 06bfcdcf11..06bca961f3 100644 --- a/src/File/FileHelper.php +++ b/src/File/FileHelper.php @@ -7,10 +7,13 @@ use function explode; use function implode; use function ltrim; +use function preg_match; use function rtrim; +use function str_ends_with; use function str_replace; use function str_starts_with; -use function strpos; +use function strlen; +use function strtolower; use function substr; use function trim; use const DIRECTORY_SEPARATOR; @@ -34,15 +37,14 @@ public function getWorkingDirectory(): string public function absolutizePath(string $path): string { if (DIRECTORY_SEPARATOR === '/') { - if (substr($path, 0, 1) === '/') { - return $path; - } - } else { - if (substr($path, 1, 1) === ':') { + if (str_starts_with($path, '/')) { return $path; } + } elseif (substr($path, 1, 1) === ':') { + return $path; } - if (str_starts_with($path, 'phar://')) { + + if (preg_match('~^[a-z0-9+\-.]+://~i', $path) === 1) { return $path; } @@ -52,15 +54,23 @@ public function absolutizePath(string $path): string /** @api */ public function normalizePath(string $originalPath, string $directorySeparator = DIRECTORY_SEPARATOR): string { - $isLocalPath = $originalPath !== '' && $originalPath[0] === '/'; + $isLocalPath = false; + if ($originalPath !== '') { + if ($originalPath[0] === '/') { + $isLocalPath = true; + } elseif (strlen($originalPath) >= 3 && $originalPath[1] === ':' && $originalPath[2] === '\\') { // e.g. C:\ + $isLocalPath = true; + } + } $matches = null; if (!$isLocalPath) { - $matches = Strings::match($originalPath, '~^([a-z]+)\\:\\/\\/(.+)~'); + $matches = Strings::match($originalPath, '~^([a-z0-9+\-.]+)://(.+)$~is'); } if ($matches !== null) { [, $scheme, $path] = $matches; + $scheme = strtolower($scheme); } else { $scheme = null; $path = $originalPath; @@ -68,7 +78,7 @@ public function normalizePath(string $originalPath, string $directorySeparator = $path = str_replace(['\\', '//', '///', '////'], '/', $path); - $pathRoot = strpos($path, '/') === 0 ? $directorySeparator : ''; + $pathRoot = str_starts_with($path, '/') ? $directorySeparator : ''; $pathParts = explode('/', trim($path, '/')); $normalizedPathParts = []; @@ -77,12 +87,10 @@ public function normalizePath(string $originalPath, string $directorySeparator = continue; } if ($pathPart === '..') { - /** @var string $removedPart */ $removedPart = array_pop($normalizedPathParts); - if ($scheme === 'phar' && substr($removedPart, -5) === '.phar') { + if ($scheme === 'phar' && $removedPart !== null && str_ends_with($removedPart, '.phar')) { $scheme = null; } - } else { $normalizedPathParts[] = $pathPart; } diff --git a/src/File/FileMonitor.php b/src/File/FileMonitor.php index 52d91b0729..26e27454ff 100644 --- a/src/File/FileMonitor.php +++ b/src/File/FileMonitor.php @@ -6,7 +6,7 @@ use function array_key_exists; use function array_keys; use function count; -use function sha1; +use function sha1_file; class FileMonitor { @@ -81,7 +81,13 @@ public function getChanges(): FileMonitorResult private function getFileHash(string $filePath): string { - return sha1(FileReader::read($filePath)); + $hash = sha1_file($filePath); + + if ($hash === false) { + throw new CouldNotReadFileException($filePath); + } + + return $hash; } } diff --git a/src/File/FileMonitorResult.php b/src/File/FileMonitorResult.php index 9da77e0bcc..940c21e965 100644 --- a/src/File/FileMonitorResult.php +++ b/src/File/FileMonitorResult.php @@ -21,6 +21,14 @@ public function __construct( { } + /** + * @return string[] + */ + public function getChangedFiles(): array + { + return $this->changedFiles; + } + public function hasAnyChanges(): bool { return count($this->newFiles) > 0 diff --git a/src/File/FileReader.php b/src/File/FileReader.php index 40b65477b9..46f8933916 100644 --- a/src/File/FileReader.php +++ b/src/File/FileReader.php @@ -3,24 +3,29 @@ namespace PHPStan\File; use function file_get_contents; -use function is_file; use function stream_resolve_include_path; class FileReader { + /** + * @throws CouldNotReadFileException + */ public static function read(string $fileName): string { $path = $fileName; - if (!is_file($path)) { + $contents = @file_get_contents($path); + if ($contents === false) { $path = stream_resolve_include_path($fileName); if ($path === false) { throw new CouldNotReadFileException($fileName); } + + $contents = @file_get_contents($path); } - $contents = @file_get_contents($path); + if ($contents === false) { throw new CouldNotReadFileException($fileName); } diff --git a/src/File/FuzzyRelativePathHelper.php b/src/File/FuzzyRelativePathHelper.php index 47452d60a3..dc2d44e505 100644 --- a/src/File/FuzzyRelativePathHelper.php +++ b/src/File/FuzzyRelativePathHelper.php @@ -9,8 +9,8 @@ use function ltrim; use function realpath; use function str_ends_with; +use function str_starts_with; use function strlen; -use function strpos; use function substr; use const DIRECTORY_SEPARATOR; @@ -40,7 +40,7 @@ public function __construct( $pathBeginning = null; $pathToTrimArray = null; $trimBeginning = static function (string $path): array { - if (substr($path, 0, 1) === '/') { + if (str_starts_with($path, '/')) { return [ '/', substr($path, 1), @@ -61,17 +61,16 @@ public function __construct( ) { [$pathBeginning, $currentWorkingDirectory] = $trimBeginning($currentWorkingDirectory); - /** @var string[] $pathToTrimArray */ $pathToTrimArray = explode($directorySeparator, $currentWorkingDirectory); } foreach ($analysedPaths as $pathNumber => $path) { [$tempPathBeginning, $path] = $trimBeginning($path); - /** @var string[] $pathArray */ $pathArray = explode($directorySeparator, $path); $pathTempParts = []; + $pathArraySize = count($pathArray); foreach ($pathArray as $i => $pathPart) { - if (str_ends_with($pathPart, '.php')) { + if ($i === $pathArraySize - 1 && str_ends_with($pathPart, '.php')) { continue; } if (!isset($pathToTrimArray[$i])) { @@ -108,7 +107,7 @@ public function getRelativePath(string $filename): string { if ( $this->pathToTrim !== null - && strpos($filename, $this->pathToTrim) === 0 + && str_starts_with($filename, $this->pathToTrim) ) { return ltrim(substr($filename, strlen($this->pathToTrim)), $this->directorySeparator); } diff --git a/src/File/SimpleRelativePathHelper.php b/src/File/SimpleRelativePathHelper.php index 14181895ca..854f584b47 100644 --- a/src/File/SimpleRelativePathHelper.php +++ b/src/File/SimpleRelativePathHelper.php @@ -3,8 +3,8 @@ namespace PHPStan\File; use function str_replace; +use function str_starts_with; use function strlen; -use function strpos; use function substr; class SimpleRelativePathHelper implements RelativePathHelper @@ -16,7 +16,7 @@ public function __construct(private string $currentWorkingDirectory) public function getRelativePath(string $filename): string { - if ($this->currentWorkingDirectory !== '' && strpos($filename, $this->currentWorkingDirectory) === 0) { + if ($this->currentWorkingDirectory !== '' && str_starts_with($filename, $this->currentWorkingDirectory)) { return str_replace('\\', '/', substr($filename, strlen($this->currentWorkingDirectory) + 1)); } diff --git a/src/Internal/CombinationsHelper.php b/src/Internal/CombinationsHelper.php new file mode 100644 index 0000000000..feeb550400 --- /dev/null +++ b/src/Internal/CombinationsHelper.php @@ -0,0 +1,35 @@ + $arrays + * @return iterable + */ + public static function combinations(array $arrays): iterable + { + // from https://stackoverflow.com/a/70800936/565782 by Arnaud Le Blanc + if ($arrays === []) { + yield []; + return; + } + + $head = array_shift($arrays); + + foreach ($head as $elem) { + foreach (self::combinations($arrays) as $combination) { + $comb = [$elem]; + foreach ($combination as $c) { + $comb[] = $c; + } + yield $comb; + } + } + } + +} diff --git a/src/Internal/ComposerHelper.php b/src/Internal/ComposerHelper.php index 50e21572d5..322d7f1048 100644 --- a/src/Internal/ComposerHelper.php +++ b/src/Internal/ComposerHelper.php @@ -19,7 +19,7 @@ final class ComposerHelper private static ?string $phpstanVersion = null; - /** @return array */ + /** @return array|null */ public static function getComposerConfig(string $root): ?array { $composerJsonPath = self::getComposerJsonPath($root); @@ -56,6 +56,16 @@ public static function getVendorDirFromComposerConfig(string $root, array $compo return $root . '/' . trim($vendorDirectory, '/'); } + /** + * @param array $composerConfig + */ + public static function getBinDirFromComposerConfig(string $root, array $composerConfig): string + { + $vendorDirectory = $composerConfig['config']['bin-dir'] ?? 'vendor/bin'; + + return $root . '/' . trim($vendorDirectory, '/'); + } + public static function getPhpStanVersion(): string { if (self::$phpstanVersion !== null) { diff --git a/src/Internal/ContainerDynamicReturnTypeExtension.php b/src/Internal/ContainerDynamicReturnTypeExtension.php index afb196dbbd..f4b0a340bb 100644 --- a/src/Internal/ContainerDynamicReturnTypeExtension.php +++ b/src/Internal/ContainerDynamicReturnTypeExtension.php @@ -7,7 +7,6 @@ use PHPStan\DependencyInjection\Container; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\ObjectType; @@ -44,7 +43,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method $type = new ObjectType($argType->getValue()); if ($methodReflection->getName() === 'getByType' && count($methodCall->getArgs()) >= 2) { $argType = $scope->getType($methodCall->getArgs()[1]->value); - if ($argType instanceof ConstantBooleanType && $argType->getValue()) { + if ($argType->isTrue()->yes()) { $type = TypeCombinator::addNull($type); } } diff --git a/src/Internal/DirectoryCreator.php b/src/Internal/DirectoryCreator.php new file mode 100644 index 0000000000..3ed7c2ff11 --- /dev/null +++ b/src/Internal/DirectoryCreator.php @@ -0,0 +1,36 @@ +typeSpecifier = $typeSpecifier; - } - - public function getClass(): string - { - return ClassMemberAccessAnswerer::class; - } - - public function isMethodSupported( - MethodReflection $methodReflection, - MethodCall $node, - TypeSpecifierContext $context, - ): bool - { - return $methodReflection->getName() === $this->isInMethodName - && !$context->null(); - } - - public function specifyTypes( - MethodReflection $methodReflection, - MethodCall $node, - Scope $scope, - TypeSpecifierContext $context, - ): SpecifiedTypes - { - $scopeClass = $this->reflectionProvider->getClass(Scope::class); - $methodVariants = $scopeClass - ->getMethod($this->removeNullMethodName, $scope) - ->getVariants(); - - return $this->typeSpecifier->create( - new MethodCall($node->var, $this->removeNullMethodName), - TypeCombinator::removeNull( - ParametersAcceptorSelector::selectSingle($methodVariants)->getReturnType(), - ), - $context, - false, - $scope, - ); - } - -} diff --git a/src/Node/ClassConstantsNode.php b/src/Node/ClassConstantsNode.php index 4572ff79eb..dee7d0019a 100644 --- a/src/Node/ClassConstantsNode.php +++ b/src/Node/ClassConstantsNode.php @@ -6,6 +6,7 @@ use PhpParser\Node\Stmt\ClassLike; use PhpParser\NodeAbstract; use PHPStan\Node\Constant\ClassConstantFetch; +use PHPStan\Reflection\ClassReflection; /** @api */ class ClassConstantsNode extends NodeAbstract implements VirtualNode @@ -15,7 +16,7 @@ class ClassConstantsNode extends NodeAbstract implements VirtualNode * @param ClassConst[] $constants * @param ClassConstantFetch[] $fetches */ - public function __construct(private ClassLike $class, private array $constants, private array $fetches) + public function __construct(private ClassLike $class, private array $constants, private array $fetches, private ClassReflection $classReflection) { parent::__construct($class->getAttributes()); } @@ -54,4 +55,9 @@ public function getSubNodeNames(): array return []; } + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + } diff --git a/src/Node/ClassMethod.php b/src/Node/ClassMethod.php index b807475f75..6ecfaeead8 100644 --- a/src/Node/ClassMethod.php +++ b/src/Node/ClassMethod.php @@ -4,6 +4,7 @@ use PhpParser\Node\Stmt\ClassMethod as PhpParserClassMethod; +/** @api */ class ClassMethod extends PhpParserClassMethod { diff --git a/src/Node/ClassMethodsNode.php b/src/Node/ClassMethodsNode.php index 0b5c616c34..e02620532b 100644 --- a/src/Node/ClassMethodsNode.php +++ b/src/Node/ClassMethodsNode.php @@ -5,6 +5,7 @@ use PhpParser\Node\Stmt\ClassLike; use PhpParser\NodeAbstract; use PHPStan\Node\Method\MethodCall; +use PHPStan\Reflection\ClassReflection; /** @api */ class ClassMethodsNode extends NodeAbstract implements VirtualNode @@ -14,7 +15,7 @@ class ClassMethodsNode extends NodeAbstract implements VirtualNode * @param ClassMethod[] $methods * @param array $methodCalls */ - public function __construct(private ClassLike $class, private array $methods, private array $methodCalls) + public function __construct(private ClassLike $class, private array $methods, private array $methodCalls, private ClassReflection $classReflection) { parent::__construct($class->getAttributes()); } @@ -53,4 +54,9 @@ public function getSubNodeNames(): array return []; } + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + } diff --git a/src/Node/ClassPropertiesNode.php b/src/Node/ClassPropertiesNode.php index 16bdfafacc..8d9659a68b 100644 --- a/src/Node/ClassPropertiesNode.php +++ b/src/Node/ClassPropertiesNode.php @@ -11,19 +11,22 @@ use PhpParser\Node\Stmt\ClassLike; use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; +use PHPStan\Node\Expr\PropertyInitializationExpr; use PHPStan\Node\Method\MethodCall; use PHPStan\Node\Property\PropertyRead; use PHPStan\Node\Property\PropertyWrite; +use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; -use PHPStan\ShouldNotHappenException; -use PHPStan\Type\MixedType; -use PHPStan\Type\ObjectType; +use PHPStan\TrinaryLogic; +use PHPStan\Type\NeverType; +use PHPStan\Type\TypeUtils; +use function array_diff_key; use function array_key_exists; use function array_keys; -use function count; use function in_array; +use function strtolower; /** @api */ class ClassPropertiesNode extends NodeAbstract implements VirtualNode @@ -33,6 +36,7 @@ class ClassPropertiesNode extends NodeAbstract implements VirtualNode * @param ClassPropertyNode[] $properties * @param array $propertyUsages * @param array $methodCalls + * @param array $returnStatementNodes */ public function __construct( private ClassLike $class, @@ -40,6 +44,8 @@ public function __construct( private array $properties, private array $propertyUsages, private array $methodCalls, + private array $returnStatementNodes, + private ClassReflection $classReflection, ) { parent::__construct($class->getAttributes()); @@ -79,10 +85,15 @@ public function getSubNodeNames(): array return []; } + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + /** * @param string[] $constructors * @param ReadWritePropertiesExtension[]|null $extensions - * @return array{array, array, array} + * @return array{array, array, array} */ public function getUninitializedProperties( Scope $scope, @@ -93,12 +104,16 @@ public function getUninitializedProperties( if (!$this->getClass() instanceof Class_) { return [[], [], []]; } - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - $classReflection = $scope->getClassReflection(); + $classReflection = $this->getClassReflection(); - $properties = []; + $uninitializedProperties = []; + $originalProperties = []; + $initialInitializedProperties = []; + $initializedProperties = []; + if ($extensions === null) { + $extensions = $this->readWritePropertiesExtensionProvider->getExtensions(); + } + $initializedViaExtension = []; foreach ($this->getProperties() as $property) { if ($property->isStatic()) { continue; @@ -109,35 +124,43 @@ public function getUninitializedProperties( if ($property->getDefault() !== null) { continue; } - $properties[$property->getName()] = $property; - } + $originalProperties[$property->getName()] = $property; + $is = TrinaryLogic::createFromBoolean($property->isPromoted() && !$property->isPromotedFromTrait()); + if (!$is->yes() && $classReflection->hasNativeProperty($property->getName())) { + $propertyReflection = $classReflection->getNativeProperty($property->getName()); - if ($extensions === null) { - $extensions = $this->readWritePropertiesExtensionProvider->getExtensions(); - } - - foreach (array_keys($properties) as $name) { - foreach ($extensions as $extension) { - if (!$classReflection->hasNativeProperty($name)) { - continue; + foreach ($extensions as $extension) { + if (!$extension->isInitialized($propertyReflection, $property->getName())) { + continue; + } + $is = TrinaryLogic::createYes(); + $initializedViaExtension[$property->getName()] = true; + break; } - $propertyReflection = $classReflection->getNativeProperty($name); - if (!$extension->isInitialized($propertyReflection, $name)) { - continue; - } - unset($properties[$name]); - break; } + $initialInitializedProperties[$property->getName()] = $is; + foreach ($constructors as $constructor) { + $initializedProperties[$constructor][$property->getName()] = $is; + } + if ($is->yes()) { + continue; + } + $uninitializedProperties[$property->getName()] = $property; } if ($constructors === []) { - return [$properties, [], []]; + return [$uninitializedProperties, [], []]; } - $classType = new ObjectType($scope->getClassReflection()->getName()); - $methodsCalledFromConstructor = $this->getMethodsCalledFromConstructor($classType, $this->methodCalls, $constructors); + + $initializedInConstructor = []; + if ($classReflection->hasConstructor()) { + $initializedInConstructor = array_diff_key($uninitializedProperties, $this->collectUninitializedProperties([$classReflection->getConstructor()->getName()], $uninitializedProperties)); + } + + $methodsCalledFromConstructor = $this->getMethodsCalledFromConstructor($classReflection, $initialInitializedProperties, $initializedProperties, $constructors, $initializedInConstructor); $prematureAccess = []; $additionalAssigns = []; - $originalProperties = $properties; + foreach ($this->getPropertyUsages() as $usage) { $fetch = $usage->getFetch(); if (!$fetch instanceof PropertyFetch) { @@ -154,61 +177,156 @@ public function getUninitializedProperties( if ($function->getDeclaringClass()->getName() !== $classReflection->getName()) { continue; } - if (!in_array($function->getName(), $methodsCalledFromConstructor, true)) { + if (!array_key_exists($function->getName(), $methodsCalledFromConstructor)) { continue; } + $initializedPropertiesMap = $methodsCalledFromConstructor[$function->getName()]; + if (!$fetch->name instanceof Identifier) { continue; } $propertyName = $fetch->name->toString(); $fetchedOnType = $usageScope->getType($fetch->var); - if ($classType->isSuperTypeOf($fetchedOnType)->no()) { + if (TypeUtils::findThisType($fetchedOnType) === null) { + continue; + } + + $propertyReflection = $usageScope->getPropertyReflection($fetchedOnType, $propertyName); + if ($propertyReflection === null) { continue; } - if ($fetchedOnType instanceof MixedType) { + if ($propertyReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { continue; } if ($usage instanceof PropertyWrite) { - if (array_key_exists($propertyName, $properties)) { - unset($properties[$propertyName]); - } elseif (array_key_exists($propertyName, $originalProperties)) { - $additionalAssigns[] = [ + if (array_key_exists($propertyName, $initializedPropertiesMap)) { + $hasInitialization = $initializedPropertiesMap[$propertyName]->or($usageScope->hasExpressionType(new PropertyInitializationExpr($propertyName))); + if ( + !$hasInitialization->no() + && !$usage->isPromotedPropertyWrite() + && !array_key_exists($propertyName, $initializedViaExtension) + ) { + $additionalAssigns[] = [ + $propertyName, + $fetch->getStartLine(), + $originalProperties[$propertyName], + ]; + } + } + } elseif (array_key_exists($propertyName, $initializedPropertiesMap)) { + if ( + strtolower($function->getName()) !== '__construct' + && array_key_exists($propertyName, $initializedInConstructor) + && in_array($function->getName(), $constructors, true) + ) { + continue; + } + $hasInitialization = $initializedPropertiesMap[$propertyName]->or($usageScope->hasExpressionType(new PropertyInitializationExpr($propertyName))); + if (!$hasInitialization->yes() && $usageScope->isInAnonymousFunction() && $usageScope->getParentScope() !== null) { + $hasInitialization = $hasInitialization->or($usageScope->getParentScope()->hasExpressionType(new PropertyInitializationExpr($propertyName))); + } + if (!$hasInitialization->yes()) { + $prematureAccess[] = [ $propertyName, - $fetch->getLine(), + $fetch->getStartLine(), $originalProperties[$propertyName], + $usageScope->getFile(), + $usageScope->getFileDescription(), ]; } - } elseif (array_key_exists($propertyName, $properties)) { - $prematureAccess[] = [ - $propertyName, - $fetch->getLine(), - $properties[$propertyName], - ]; } } return [ - $properties, + $this->collectUninitializedProperties(array_keys($methodsCalledFromConstructor), $uninitializedProperties), $prematureAccess, $additionalAssigns, ]; } /** - * @param MethodCall[] $methodCalls + * @param list $constructors + * @param array $uninitializedProperties + * @return array + */ + private function collectUninitializedProperties(array $constructors, array $uninitializedProperties): array + { + foreach ($constructors as $constructor) { + $lowerConstructorName = strtolower($constructor); + if (!array_key_exists($lowerConstructorName, $this->returnStatementNodes)) { + continue; + } + + $returnStatementsNode = $this->returnStatementNodes[$lowerConstructorName]; + $methodScope = null; + foreach ($returnStatementsNode->getExecutionEnds() as $executionEnd) { + $statementResult = $executionEnd->getStatementResult(); + $endNode = $executionEnd->getNode(); + if ($statementResult->isAlwaysTerminating()) { + if ($endNode instanceof Node\Stmt\Throw_) { + continue; + } + if ($endNode instanceof Node\Stmt\Expression) { + $exprType = $statementResult->getScope()->getType($endNode->expr); + if ($exprType instanceof NeverType && $exprType->isExplicit()) { + continue; + } + } + } + if ($methodScope === null) { + $methodScope = $statementResult->getScope(); + continue; + } + + $methodScope = $methodScope->mergeWith($statementResult->getScope()); + } + + foreach ($returnStatementsNode->getReturnStatements() as $returnStatement) { + if ($methodScope === null) { + $methodScope = $returnStatement->getScope(); + continue; + } + $methodScope = $methodScope->mergeWith($returnStatement->getScope()); + } + + if ($methodScope === null) { + continue; + } + + foreach (array_keys($uninitializedProperties) as $propertyName) { + if (!$methodScope->hasExpressionType(new PropertyInitializationExpr($propertyName))->yes()) { + continue; + } + + unset($uninitializedProperties[$propertyName]); + } + } + + return $uninitializedProperties; + } + + /** * @param string[] $methods - * @return string[] + * @param array $initialInitializedProperties + * @param array> $initializedProperties + * @param array $initializedInConstructorProperties + * + * @return array> */ private function getMethodsCalledFromConstructor( - ObjectType $classType, - array $methodCalls, + ClassReflection $classReflection, + array $initialInitializedProperties, + array $initializedProperties, array $methods, + array $initializedInConstructorProperties, ): array { - $originalCount = count($methods); - foreach ($methodCalls as $methodCall) { + $originalMap = $initializedProperties; + $originalMethods = $methods; + + foreach ($this->methodCalls as $methodCall) { $methodCallNode = $methodCall->getNode(); if ($methodCallNode instanceof Array_) { continue; @@ -226,31 +344,61 @@ private function getMethodsCalledFromConstructor( $calledOnType = $callScope->resolveTypeByName($methodCallNode->class); } - if ($classType->isSuperTypeOf($calledOnType)->no()) { + + if (TypeUtils::findThisType($calledOnType) === null) { + continue; + } + + $inMethod = $callScope->getFunction(); + if (!$inMethod instanceof MethodReflection) { continue; } - if ($calledOnType instanceof MixedType) { + if (!in_array($inMethod->getName(), $methods, true)) { continue; } + + if ($inMethod->getName() !== '__construct') { + foreach ($initializedInConstructorProperties as $propertyName => $propertyNode) { + $initializedProperties[$inMethod->getName()][$propertyName] = TrinaryLogic::createYes(); + } + } + $methodName = $methodCallNode->name->toString(); - if (in_array($methodName, $methods, true)) { + if (array_key_exists($methodName, $initializedProperties)) { + foreach ($this->getInitializedProperties($callScope, $initializedProperties[$inMethod->getName()] ?? $initialInitializedProperties) as $propertyName => $isInitialized) { + $initializedProperties[$methodName][$propertyName] = $initializedProperties[$methodName][$propertyName]->and($isInitialized); + } continue; } - $inMethod = $callScope->getFunction(); - if (!$inMethod instanceof MethodReflection) { + $methodReflection = $callScope->getMethodReflection($calledOnType, $methodName); + if ($methodReflection === null) { continue; } - if (!in_array($inMethod->getName(), $methods, true)) { + if ($methodReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { continue; } + $initializedProperties[$methodName] = $this->getInitializedProperties($callScope, $initializedProperties[$inMethod->getName()] ?? $initialInitializedProperties); $methods[] = $methodName; } - if ($originalCount === count($methods)) { - return $methods; + if ($originalMap === $initializedProperties && $originalMethods === $methods) { + return $initializedProperties; + } + + return $this->getMethodsCalledFromConstructor($classReflection, $initialInitializedProperties, $initializedProperties, $methods, $initializedInConstructorProperties); + } + + /** + * @param array $initialInitializedProperties + * @return array + */ + private function getInitializedProperties(Scope $scope, array $initialInitializedProperties): array + { + foreach ($initialInitializedProperties as $propertyName => $isInitialized) { + $initialInitializedProperties[$propertyName] = $isInitialized->or($scope->hasExpressionType(new PropertyInitializationExpr($propertyName))); } - return $this->getMethodsCalledFromConstructor($classType, $methodCalls, $methods); + return $initialInitializedProperties; } } diff --git a/src/Node/ClassPropertyNode.php b/src/Node/ClassPropertyNode.php index 3a313a6c24..a7c81ff9d4 100644 --- a/src/Node/ClassPropertyNode.php +++ b/src/Node/ClassPropertyNode.php @@ -8,6 +8,7 @@ use PhpParser\Node\Name; use PhpParser\Node\Stmt\Class_; use PhpParser\NodeAbstract; +use PHPStan\Reflection\ClassReflection; use PHPStan\Type\Type; /** @api */ @@ -22,10 +23,13 @@ public function __construct( private ?string $phpDoc, private ?Type $phpDocType, private bool $isPromoted, + private bool $isPromotedFromTrait, Node $originalNode, private bool $isReadonlyByPhpDoc, private bool $isDeclaredInTrait, private bool $isReadonlyClass, + private bool $isAllowedPrivateMutation, + private ClassReflection $classReflection, ) { parent::__construct($originalNode->getAttributes()); @@ -51,6 +55,11 @@ public function isPromoted(): bool return $this->isPromoted; } + public function isPromotedFromTrait(): bool + { + return $this->isPromotedFromTrait; + } + public function getPhpDoc(): ?string { return $this->phpDoc; @@ -97,6 +106,11 @@ public function isDeclaredInTrait(): bool return $this->isDeclaredInTrait; } + public function isAllowedPrivateMutation(): bool + { + return $this->isAllowedPrivateMutation; + } + /** * @return Identifier|Name|Node\ComplexType|null */ @@ -105,6 +119,11 @@ public function getNativeType() return $this->type; } + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + public function getType(): string { return 'PHPStan_Node_ClassPropertyNode'; diff --git a/src/Node/ClassStatementsGatherer.php b/src/Node/ClassStatementsGatherer.php index 782df7bd38..904186224f 100644 --- a/src/Node/ClassStatementsGatherer.php +++ b/src/Node/ClassStatementsGatherer.php @@ -17,11 +17,19 @@ use PHPStan\Node\Property\PropertyWrite; use PHPStan\Reflection\ClassReflection; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\TypeUtils; use function count; +use function in_array; +use function strtolower; class ClassStatementsGatherer { + private const PROPERTY_ENUMERATING_FUNCTIONS = [ + 'get_object_vars', + 'array_walk', + ]; + /** @var callable(Node $node, Scope $scope): void */ private $nodeCallback; @@ -43,6 +51,9 @@ class ClassStatementsGatherer /** @var ClassConstantFetch[] */ private array $constantFetches = []; + /** @var array */ + private array $returnStatementNodes = []; + /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ @@ -102,6 +113,14 @@ public function getConstantFetches(): array return $this->constantFetches; } + /** + * @return array + */ + public function getReturnStatementsNodes(): array + { + return $this->returnStatementNodes; + } + public function __invoke(Node $node, Scope $scope): void { $nodeCallback = $this->nodeCallback; @@ -123,6 +142,7 @@ private function gatherNodes(Node $node, Scope $scope): void $this->propertyUsages[] = new PropertyWrite( new PropertyFetch(new Expr\Variable('this'), new Identifier($node->getName())), $scope, + true, ); } return; @@ -143,6 +163,18 @@ private function gatherNodes(Node $node, Scope $scope): void $this->methodCalls[] = new \PHPStan\Node\Method\MethodCall($node->getOriginalNode(), $scope); return; } + if ($node instanceof MethodReturnStatementsNode) { + $this->returnStatementNodes[strtolower($node->getMethodName())] = $node; + return; + } + if ( + $node instanceof Expr\FuncCall + && $node->name instanceof Node\Name + && in_array($node->name->toLowerString(), self::PROPERTY_ENUMERATING_FUNCTIONS, true) + ) { + $this->tryToApplyPropertyReads($node, $scope); + return; + } if ($node instanceof Array_ && count($node->items) === 2) { $this->methodCalls[] = new \PHPStan\Node\Method\MethodCall($node, $scope); return; @@ -152,7 +184,7 @@ private function gatherNodes(Node $node, Scope $scope): void return; } if ($node instanceof PropertyAssignNode) { - $this->propertyUsages[] = new PropertyWrite($node->getPropertyFetch(), $scope); + $this->propertyUsages[] = new PropertyWrite($node->getPropertyFetch(), $scope, false); return; } if (!$node instanceof Expr) { @@ -169,7 +201,7 @@ private function gatherNodes(Node $node, Scope $scope): void } $this->propertyUsages[] = new PropertyRead($node->expr, $scope); - $this->propertyUsages[] = new PropertyWrite($node->expr, $scope); + $this->propertyUsages[] = new PropertyWrite($node->expr, $scope, false); return; } if ($node instanceof Node\Scalar\EncapsedStringPart) { @@ -196,4 +228,28 @@ private function gatherNodes(Node $node, Scope $scope): void $this->propertyUsages[] = new PropertyRead($node, $scope); } + private function tryToApplyPropertyReads(Expr\FuncCall $node, Scope $scope): void + { + $args = $node->getArgs(); + if (count($args) === 0) { + return; + } + + $firstArgValue = $args[0]->value; + if (TypeUtils::findThisType($scope->getType($firstArgValue)) === null) { + return; + } + + $classProperties = $this->classReflection->getNativeReflection()->getProperties(); + foreach ($classProperties as $property) { + if ($property->isStatic()) { + continue; + } + $this->propertyUsages[] = new PropertyRead( + new PropertyFetch(new Expr\Variable('this'), new Identifier($property->getName())), + $scope, + ); + } + } + } diff --git a/src/Node/ClosureReturnStatementsNode.php b/src/Node/ClosureReturnStatementsNode.php index 04c229b438..0a20615ca8 100644 --- a/src/Node/ClosureReturnStatementsNode.php +++ b/src/Node/ClosureReturnStatementsNode.php @@ -7,7 +7,9 @@ use PhpParser\Node\Expr\Yield_; use PhpParser\Node\Expr\YieldFrom; use PhpParser\NodeAbstract; +use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\StatementResult; +use function count; /** @api */ class ClosureReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode @@ -16,14 +18,18 @@ class ClosureReturnStatementsNode extends NodeAbstract implements ReturnStatemen private Node\Expr\Closure $closureExpr; /** - * @param ReturnStatement[] $returnStatements - * @param array $yieldStatements + * @param list $returnStatements + * @param list $yieldStatements + * @param list $executionEnds + * @param ImpurePoint[] $impurePoints */ public function __construct( Closure $closureExpr, private array $returnStatements, private array $yieldStatements, private StatementResult $statementResult, + private array $executionEnds, + private array $impurePoints, ) { parent::__construct($closureExpr->getAttributes()); @@ -35,22 +41,36 @@ public function getClosureExpr(): Closure return $this->closureExpr; } - /** - * @return ReturnStatement[] - */ + public function hasNativeReturnTypehint(): bool + { + return $this->closureExpr->returnType !== null; + } + public function getReturnStatements(): array { return $this->returnStatements; } - /** - * @return array - */ + public function getExecutionEnds(): array + { + return $this->executionEnds; + } + + public function getImpurePoints(): array + { + return $this->impurePoints; + } + public function getYieldStatements(): array { return $this->yieldStatements; } + public function isGenerator(): bool + { + return count($this->yieldStatements) > 0; + } + public function getStatementResult(): StatementResult { return $this->statementResult; diff --git a/src/Node/CollectedDataNode.php b/src/Node/CollectedDataNode.php index 7ebc61eb5d..8f7684e1ec 100644 --- a/src/Node/CollectedDataNode.php +++ b/src/Node/CollectedDataNode.php @@ -15,7 +15,7 @@ class CollectedDataNode extends NodeAbstract /** * @param CollectedData[] $collectedData */ - public function __construct(private array $collectedData) + public function __construct(private array $collectedData, private bool $onlyFiles) { parent::__construct([]); } @@ -45,6 +45,16 @@ public function get(string $collectorType): array return $result; } + /** + * Indicates that only files were passed to the analyser, not directory paths. + * + * True being returned strongly suggests that it's a partial analysis, not full project analysis. + */ + public function isOnlyFilesAnalysis(): bool + { + return $this->onlyFiles; + } + public function getType(): string { return 'PHPStan_Node_CollectedDataNode'; diff --git a/src/Node/Expr/AlwaysRememberedExpr.php b/src/Node/Expr/AlwaysRememberedExpr.php new file mode 100644 index 0000000000..049ad46a6b --- /dev/null +++ b/src/Node/Expr/AlwaysRememberedExpr.php @@ -0,0 +1,45 @@ +expr; + } + + public function getExprType(): Type + { + return $this->type; + } + + public function getNativeExprType(): Type + { + return $this->nativeType; + } + + public function getType(): string + { + return 'PHPStan_Node_AlwaysRememberedExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return ['expr']; + } + +} diff --git a/src/Node/Expr/ExistingArrayDimFetch.php b/src/Node/Expr/ExistingArrayDimFetch.php new file mode 100644 index 0000000000..80412b4fa3 --- /dev/null +++ b/src/Node/Expr/ExistingArrayDimFetch.php @@ -0,0 +1,39 @@ +var; + } + + public function getDim(): Expr + { + return $this->dim; + } + + public function getType(): string + { + return 'PHPStan_Node_ExistingArrayDimFetch'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/ParameterVariableOriginalValueExpr.php b/src/Node/Expr/ParameterVariableOriginalValueExpr.php new file mode 100644 index 0000000000..277d51c040 --- /dev/null +++ b/src/Node/Expr/ParameterVariableOriginalValueExpr.php @@ -0,0 +1,34 @@ +variableName; + } + + public function getType(): string + { + return 'PHPStan_Node_ParameterVariableOriginalValueExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/PropertyInitializationExpr.php b/src/Node/Expr/PropertyInitializationExpr.php new file mode 100644 index 0000000000..942fa08d5c --- /dev/null +++ b/src/Node/Expr/PropertyInitializationExpr.php @@ -0,0 +1,34 @@ +propertyName; + } + + public function getType(): string + { + return 'PHPStan_Node_PropertyInitializationExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/SetExistingOffsetValueTypeExpr.php b/src/Node/Expr/SetExistingOffsetValueTypeExpr.php new file mode 100644 index 0000000000..52eb9a4b37 --- /dev/null +++ b/src/Node/Expr/SetExistingOffsetValueTypeExpr.php @@ -0,0 +1,44 @@ +var; + } + + public function getDim(): Expr + { + return $this->dim; + } + + public function getValue(): Expr + { + return $this->value; + } + + public function getType(): string + { + return 'PHPStan_Node_SetExistingOffsetValueTypeExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/UnsetOffsetExpr.php b/src/Node/Expr/UnsetOffsetExpr.php new file mode 100644 index 0000000000..55c81eef92 --- /dev/null +++ b/src/Node/Expr/UnsetOffsetExpr.php @@ -0,0 +1,39 @@ +var; + } + + public function getDim(): Expr + { + return $this->dim; + } + + public function getType(): string + { + return 'PHPStan_Node_UnsetOffsetExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/FunctionReturnStatementsNode.php b/src/Node/FunctionReturnStatementsNode.php index 459da89468..86df35e24c 100644 --- a/src/Node/FunctionReturnStatementsNode.php +++ b/src/Node/FunctionReturnStatementsNode.php @@ -2,31 +2,39 @@ namespace PHPStan\Node; +use PhpParser\Node\Expr\Yield_; +use PhpParser\Node\Expr\YieldFrom; +use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Function_; use PhpParser\NodeAbstract; +use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\StatementResult; +use PHPStan\Reflection\FunctionReflection; +use function count; /** @api */ class FunctionReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode { /** - * @param ReturnStatement[] $returnStatements - * @param ExecutionEndNode[] $executionEnds + * @param list $returnStatements + * @param list $yieldStatements + * @param list $executionEnds + * @param ImpurePoint[] $impurePoints */ public function __construct( private Function_ $function, private array $returnStatements, + private array $yieldStatements, private StatementResult $statementResult, private array $executionEnds, + private array $impurePoints, + private FunctionReflection $functionReflection, ) { parent::__construct($function->getAttributes()); } - /** - * @return ReturnStatement[] - */ public function getReturnStatements(): array { return $this->returnStatements; @@ -37,14 +45,16 @@ public function getStatementResult(): StatementResult return $this->statementResult; } - /** - * @return ExecutionEndNode[] - */ public function getExecutionEnds(): array { return $this->executionEnds; } + public function getImpurePoints(): array + { + return $this->impurePoints; + } + public function returnsByRef(): bool { return $this->function->byRef; @@ -55,6 +65,16 @@ public function hasNativeReturnTypehint(): bool return $this->function->returnType !== null; } + public function getYieldStatements(): array + { + return $this->yieldStatements; + } + + public function isGenerator(): bool + { + return count($this->yieldStatements) > 0; + } + public function getType(): string { return 'PHPStan_Node_FunctionReturnStatementsNode'; @@ -68,4 +88,17 @@ public function getSubNodeNames(): array return []; } + public function getFunctionReflection(): FunctionReflection + { + return $this->functionReflection; + } + + /** + * @return Stmt[] + */ + public function getStatements(): array + { + return $this->function->getStmts(); + } + } diff --git a/src/Node/InArrowFunctionNode.php b/src/Node/InArrowFunctionNode.php index dd018f32d7..7716f332b4 100644 --- a/src/Node/InArrowFunctionNode.php +++ b/src/Node/InArrowFunctionNode.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\ArrowFunction; use PhpParser\NodeAbstract; +use PHPStan\Type\ClosureType; /** @api */ class InArrowFunctionNode extends NodeAbstract implements VirtualNode @@ -12,12 +13,17 @@ class InArrowFunctionNode extends NodeAbstract implements VirtualNode private Node\Expr\ArrowFunction $originalNode; - public function __construct(ArrowFunction $originalNode) + public function __construct(private ClosureType $closureType, ArrowFunction $originalNode) { parent::__construct($originalNode->getAttributes()); $this->originalNode = $originalNode; } + public function getClosureType(): ClosureType + { + return $this->closureType; + } + public function getOriginalNode(): Node\Expr\ArrowFunction { return $this->originalNode; diff --git a/src/Node/InClassMethodNode.php b/src/Node/InClassMethodNode.php index 78ab0c7c06..da1d8a09b7 100644 --- a/src/Node/InClassMethodNode.php +++ b/src/Node/InClassMethodNode.php @@ -3,21 +3,28 @@ namespace PHPStan\Node; use PhpParser\Node; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; /** @api */ class InClassMethodNode extends Node\Stmt implements VirtualNode { public function __construct( - private MethodReflection $methodReflection, + private ClassReflection $classReflection, + private PhpMethodFromParserNodeReflection $methodReflection, private Node\Stmt\ClassMethod $originalNode, ) { parent::__construct($originalNode->getAttributes()); } - public function getMethodReflection(): MethodReflection + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + + public function getMethodReflection(): PhpMethodFromParserNodeReflection { return $this->methodReflection; } diff --git a/src/Node/InFunctionNode.php b/src/Node/InFunctionNode.php index 4e850a18b9..548c2ac422 100644 --- a/src/Node/InFunctionNode.php +++ b/src/Node/InFunctionNode.php @@ -3,21 +3,21 @@ namespace PHPStan\Node; use PhpParser\Node; -use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; /** @api */ class InFunctionNode extends Node\Stmt implements VirtualNode { public function __construct( - private FunctionReflection $functionReflection, + private PhpFunctionFromParserNodeReflection $functionReflection, private Node\Stmt\Function_ $originalNode, ) { parent::__construct($originalNode->getAttributes()); } - public function getFunctionReflection(): FunctionReflection + public function getFunctionReflection(): PhpFunctionFromParserNodeReflection { return $this->functionReflection; } diff --git a/src/Node/InTraitNode.php b/src/Node/InTraitNode.php new file mode 100644 index 0000000000..688c04499b --- /dev/null +++ b/src/Node/InTraitNode.php @@ -0,0 +1,40 @@ +getAttributes()); + } + + public function getOriginalNode(): Node\Stmt\Trait_ + { + return $this->originalNode; + } + + public function getTraitReflection(): ClassReflection + { + return $this->traitReflection; + } + + public function getType(): string + { + return 'PHPStan_Stmt_InTraitNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/InvalidateExprNode.php b/src/Node/InvalidateExprNode.php new file mode 100644 index 0000000000..fa8ab6b061 --- /dev/null +++ b/src/Node/InvalidateExprNode.php @@ -0,0 +1,35 @@ +getAttributes()); + } + + public function getExpr(): Expr + { + return $this->expr; + } + + public function getType(): string + { + return 'PHPStan_Node_InvalidateExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/IssetExpr.php b/src/Node/IssetExpr.php new file mode 100644 index 0000000000..5c45df0ebc --- /dev/null +++ b/src/Node/IssetExpr.php @@ -0,0 +1,35 @@ +expr; + } + + public function getType(): string + { + return 'PHPStan_Node_IssetExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/MethodReturnStatementsNode.php b/src/Node/MethodReturnStatementsNode.php index a678c74502..3151d12ae3 100644 --- a/src/Node/MethodReturnStatementsNode.php +++ b/src/Node/MethodReturnStatementsNode.php @@ -2,9 +2,16 @@ namespace PHPStan\Node; +use PhpParser\Node\Expr\Yield_; +use PhpParser\Node\Expr\YieldFrom; +use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\NodeAbstract; +use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\StatementResult; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use function count; /** @api */ class MethodReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode @@ -13,23 +20,26 @@ class MethodReturnStatementsNode extends NodeAbstract implements ReturnStatement private ClassMethod $classMethod; /** - * @param ReturnStatement[] $returnStatements - * @param ExecutionEndNode[] $executionEnds + * @param list $returnStatements + * @param list $yieldStatements + * @param list $executionEnds + * @param ImpurePoint[] $impurePoints */ public function __construct( ClassMethod $method, private array $returnStatements, + private array $yieldStatements, private StatementResult $statementResult, private array $executionEnds, + private array $impurePoints, + private ClassReflection $classReflection, + private ExtendedMethodReflection $methodReflection, ) { parent::__construct($method->getAttributes()); $this->classMethod = $method; } - /** - * @return ReturnStatement[] - */ public function getReturnStatements(): array { return $this->returnStatements; @@ -40,14 +50,16 @@ public function getStatementResult(): StatementResult return $this->statementResult; } - /** - * @return ExecutionEndNode[] - */ public function getExecutionEnds(): array { return $this->executionEnds; } + public function getImpurePoints(): array + { + return $this->impurePoints; + } + public function returnsByRef(): bool { return $this->classMethod->byRef; @@ -58,6 +70,44 @@ public function hasNativeReturnTypehint(): bool return $this->classMethod->returnType !== null; } + public function getMethodName(): string + { + return $this->classMethod->name->toString(); + } + + public function getYieldStatements(): array + { + return $this->yieldStatements; + } + + public function getClassReflection(): ClassReflection + { + return $this->classReflection; + } + + public function getMethodReflection(): ExtendedMethodReflection + { + return $this->methodReflection; + } + + /** + * @return Stmt[] + */ + public function getStatements(): array + { + $stmts = $this->classMethod->getStmts(); + if ($stmts === null) { + return []; + } + + return $stmts; + } + + public function isGenerator(): bool + { + return count($this->yieldStatements) > 0; + } + public function getType(): string { return 'PHPStan_Node_MethodReturnStatementsNode'; diff --git a/src/Node/NoopExpressionNode.php b/src/Node/NoopExpressionNode.php new file mode 100644 index 0000000000..38e9222a8c --- /dev/null +++ b/src/Node/NoopExpressionNode.php @@ -0,0 +1,39 @@ +originalExpr->getAttributes()); + } + + public function getOriginalExpr(): Expr + { + return $this->originalExpr; + } + + public function hasAssign(): bool + { + return $this->hasAssign; + } + + public function getType(): string + { + return 'PHPStan_Node_NoopExpressionNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index d8127309c4..953174fc70 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -3,18 +3,30 @@ namespace PHPStan\Node\Printer; use PhpParser\PrettyPrinter\Standard; +use PHPStan\Node\Expr\AlwaysRememberedExpr; +use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\GetIterableValueTypeExpr; use PHPStan\Node\Expr\GetOffsetValueTypeExpr; use PHPStan\Node\Expr\OriginalPropertyTypeExpr; +use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; +use PHPStan\Node\Expr\PropertyInitializationExpr; +use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr; use PHPStan\Node\Expr\SetOffsetValueTypeExpr; use PHPStan\Node\Expr\TypeExpr; +use PHPStan\Node\Expr\UnsetOffsetExpr; +use PHPStan\Node\IssetExpr; use PHPStan\Type\VerbosityLevel; use function sprintf; class Printer extends Standard { + public function __construct() + { + parent::__construct(['shortArraySyntax' => true]); + } + protected function pPHPStan_Node_TypeExpr(TypeExpr $expr): string // phpcs:ignore { return sprintf('__phpstanType(%s)', $expr->getExprType()->describe(VerbosityLevel::precise())); @@ -25,6 +37,11 @@ protected function pPHPStan_Node_GetOffsetValueTypeExpr(GetOffsetValueTypeExpr $ return sprintf('__phpstanGetOffsetValueType(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); } + protected function pPHPStan_Node_UnsetOffsetExpr(UnsetOffsetExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanUnsetOffset(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); + } + protected function pPHPStan_Node_GetIterableValueTypeExpr(GetIterableValueTypeExpr $expr): string // phpcs:ignore { return sprintf('__phpstanGetIterableValueType(%s)', $this->p($expr->getExpr())); @@ -35,6 +52,11 @@ protected function pPHPStan_Node_GetIterableKeyTypeExpr(GetIterableKeyTypeExpr $ return sprintf('__phpstanGetIterableKeyType(%s)', $this->p($expr->getExpr())); } + protected function pPHPStan_Node_ExistingArrayDimFetch(ExistingArrayDimFetch $expr): string // phpcs:ignore + { + return sprintf('__phpstanExistingArrayDimFetch(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); + } + protected function pPHPStan_Node_OriginalPropertyTypeExpr(OriginalPropertyTypeExpr $expr): string // phpcs:ignore { return sprintf('__phpstanOriginalPropertyType(%s)', $this->p($expr->getPropertyFetch())); @@ -45,4 +67,29 @@ protected function pPHPStan_Node_SetOffsetValueTypeExpr(SetOffsetValueTypeExpr $ return sprintf('__phpstanSetOffsetValueType(%s, %s, %s)', $this->p($expr->getVar()), $expr->getDim() !== null ? $this->p($expr->getDim()) : 'null', $this->p($expr->getValue())); } + protected function pPHPStan_Node_SetExistingOffsetValueTypeExpr(SetExistingOffsetValueTypeExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanSetExistingOffsetValueType(%s, %s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim()), $this->p($expr->getValue())); + } + + protected function pPHPStan_Node_AlwaysRememberedExpr(AlwaysRememberedExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanRembered(%s)', $this->p($expr->getExpr())); + } + + protected function pPHPStan_Node_PropertyInitializationExpr(PropertyInitializationExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanPropertyInitialization(%s)', $expr->getPropertyName()); + } + + protected function pPHPStan_Node_ParameterVariableOriginalValueExpr(ParameterVariableOriginalValueExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanParameterVariableOriginalValue(%s)', $expr->getVariableName()); + } + + protected function pPHPStan_Node_IssetExpr(IssetExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanIssetExpr(%s)', $this->p($expr->getExpr())); + } + } diff --git a/src/Node/Property/PropertyWrite.php b/src/Node/Property/PropertyWrite.php index 00970b832d..9577dc7fa8 100644 --- a/src/Node/Property/PropertyWrite.php +++ b/src/Node/Property/PropertyWrite.php @@ -10,7 +10,7 @@ class PropertyWrite { - public function __construct(private PropertyFetch|StaticPropertyFetch $fetch, private Scope $scope) + public function __construct(private PropertyFetch|StaticPropertyFetch $fetch, private Scope $scope, private bool $promotedPropertyWrite) { } @@ -27,4 +27,9 @@ public function getScope(): Scope return $this->scope; } + public function isPromotedPropertyWrite(): bool + { + return $this->promotedPropertyWrite; + } + } diff --git a/src/Node/ReturnStatementsNode.php b/src/Node/ReturnStatementsNode.php index f54506d201..34c28ef538 100644 --- a/src/Node/ReturnStatementsNode.php +++ b/src/Node/ReturnStatementsNode.php @@ -2,6 +2,9 @@ namespace PHPStan\Node; +use PhpParser\Node\Expr\Yield_; +use PhpParser\Node\Expr\YieldFrom; +use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\StatementResult; /** @api */ @@ -9,12 +12,31 @@ interface ReturnStatementsNode extends VirtualNode { /** - * @return ReturnStatement[] + * @return list */ public function getReturnStatements(): array; public function getStatementResult(): StatementResult; + /** + * @return list + */ + public function getExecutionEnds(): array; + + /** + * @return ImpurePoint[] + */ + public function getImpurePoints(): array; + public function returnsByRef(): bool; + public function hasNativeReturnTypehint(): bool; + + /** + * @return list + */ + public function getYieldStatements(): array; + + public function isGenerator(): bool; + } diff --git a/src/Node/VarTagChangedExpressionTypeNode.php b/src/Node/VarTagChangedExpressionTypeNode.php new file mode 100644 index 0000000000..6689aa7a0d --- /dev/null +++ b/src/Node/VarTagChangedExpressionTypeNode.php @@ -0,0 +1,40 @@ +getAttributes()); + } + + public function getVarTag(): VarTag + { + return $this->varTag; + } + + public function getExpr(): Expr + { + return $this->expr; + } + + public function getType(): string + { + return 'PHPStan_Node_VarTagChangedExpressionType'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/VariableAssignNode.php b/src/Node/VariableAssignNode.php new file mode 100644 index 0000000000..2857e853ba --- /dev/null +++ b/src/Node/VariableAssignNode.php @@ -0,0 +1,48 @@ +getAttributes()); + } + + public function getVariable(): Expr\Variable + { + return $this->variable; + } + + public function getAssignedExpr(): Expr + { + return $this->assignedExpr; + } + + public function isAssignOp(): bool + { + return $this->assignOp; + } + + public function getType(): string + { + return 'PHPStan_Node_VariableAssignNodeNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/NodeVisitor/StatementOrderVisitor.php b/src/NodeVisitor/StatementOrderVisitor.php deleted file mode 100644 index 896c230275..0000000000 --- a/src/NodeVisitor/StatementOrderVisitor.php +++ /dev/null @@ -1,89 +0,0 @@ -orderStack = [0]; - $this->depth = 0; - - return null; - } - - /** - * @return null - */ - public function enterNode(Node $node) - { - $order = $this->orderStack[count($this->orderStack) - 1]; - $node->setAttribute('statementOrder', $order); - $node->setAttribute('statementDepth', $this->depth); - - if ( - ($node instanceof Node\Expr || $node instanceof Node\Arg) - && count($this->expressionOrderStack) > 0 - ) { - $expressionOrder = $this->expressionOrderStack[count($this->expressionOrderStack) - 1]; - $node->setAttribute('expressionOrder', $expressionOrder); - $node->setAttribute('expressionDepth', $this->expressionDepth); - $this->expressionOrderStack[count($this->expressionOrderStack) - 1] = $expressionOrder + 1; - $this->expressionOrderStack[] = 0; - $this->expressionDepth++; - } - - if (!$node instanceof Node\Stmt) { - return null; - } - - $this->orderStack[count($this->orderStack) - 1] = $order + 1; - $this->orderStack[] = 0; - $this->depth++; - - $this->expressionOrderStack = [0]; - $this->expressionDepth = 0; - - return null; - } - - /** - * @return null - */ - public function leaveNode(Node $node) - { - if ($node instanceof Node\Expr) { - array_pop($this->expressionOrderStack); - $this->expressionDepth--; - } - if (!$node instanceof Node\Stmt) { - return null; - } - - array_pop($this->orderStack); - $this->depth--; - - return null; - } - -} diff --git a/src/Parallel/ParallelAnalyser.php b/src/Parallel/ParallelAnalyser.php index 80cbd7a255..8bc740dd2c 100644 --- a/src/Parallel/ParallelAnalyser.php +++ b/src/Parallel/ParallelAnalyser.php @@ -11,7 +11,9 @@ use PHPStan\Collectors\CollectedData; use PHPStan\Dependency\RootExportedNode; use PHPStan\Process\ProcessHelper; -use React\EventLoop\StreamSelectLoop; +use React\EventLoop\LoopInterface; +use React\Promise\Deferred; +use React\Promise\PromiseInterface; use React\Socket\ConnectionInterface; use React\Socket\TcpServer; use Symfony\Component\Console\Input\InputInterface; @@ -19,13 +21,16 @@ use function array_map; use function array_pop; use function array_reverse; +use function array_sum; use function count; use function defined; -use function escapeshellarg; +use function ini_get; use function is_string; use function max; +use function memory_get_usage; use function parse_url; use function sprintf; +use function str_contains; use const PHP_URL_PORT; class ParallelAnalyser @@ -48,27 +53,60 @@ public function __construct( /** * @param Closure(int ): void|null $postFileCallback + * @param (callable(list, list, string[]): void)|null $onFileAnalysisHandler */ public function analyse( + LoopInterface $loop, Schedule $schedule, string $mainScript, ?Closure $postFileCallback, ?string $projectConfigFile, - ?string $tmpFile, - ?string $insteadOfFile, InputInterface $input, - ): AnalyserResult + ?callable $onFileAnalysisHandler, + ): PromiseInterface { $jobs = array_reverse($schedule->getJobs()); - $loop = new StreamSelectLoop(); $numberOfProcesses = $schedule->getNumberOfProcesses(); + $someChildEnded = false; $errors = []; + $filteredPhpErrors = []; + $allPhpErrors = []; + $locallyIgnoredErrors = []; + $linesToIgnore = []; + $unmatchedLineIgnores = []; + $peakMemoryUsages = []; $internalErrors = []; + $internalErrorsCount = 0; $collectedData = []; + $dependencies = []; + $reachedInternalErrorsCountLimit = false; + $exportedNodes = []; + + $deferred = new Deferred(); $server = new TcpServer('127.0.0.1:0', $loop); - $this->processPool = new ProcessPool($server); + $this->processPool = new ProcessPool($server, static function () use ($deferred, &$jobs, &$internalErrors, &$internalErrorsCount, &$reachedInternalErrorsCountLimit, &$errors, &$filteredPhpErrors, &$allPhpErrors, &$locallyIgnoredErrors, &$linesToIgnore, &$unmatchedLineIgnores, &$collectedData, &$dependencies, &$exportedNodes, &$peakMemoryUsages): void { + if (count($jobs) > 0 && $internalErrorsCount === 0) { + $internalErrors[] = 'Some parallel worker jobs have not finished.'; + $internalErrorsCount++; + } + + $deferred->resolve(new AnalyserResult( + $errors, + $filteredPhpErrors, + $allPhpErrors, + $locallyIgnoredErrors, + $linesToIgnore, + $unmatchedLineIgnores, + $internalErrors, + $collectedData, + $internalErrorsCount === 0 ? $dependencies : null, + $exportedNodes, + $reachedInternalErrorsCountLimit, + array_sum($peakMemoryUsages), // not 100% correct as the peak usages of workers might not have met + )); + }); $server->on('connection', function (ConnectionInterface $connection) use (&$jobs): void { // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; @@ -95,13 +133,9 @@ public function analyse( /** @var string $serverAddress */ $serverAddress = $server->getAddress(); - /** @var int $serverPort */ + /** @var int<0, 65535> $serverPort */ $serverPort = parse_url($serverAddress, PHP_URL_PORT); - $internalErrorsCount = 0; - - $reachedInternalErrorsCountLimit = false; - $handleError = function (Throwable $error) use (&$internalErrors, &$internalErrorsCount, &$reachedInternalErrorsCountLimit): void { $internalErrors[] = sprintf('Internal error: ' . $error->getMessage()); $internalErrorsCount++; @@ -109,8 +143,6 @@ public function analyse( $this->processPool->quitAll(); }; - $dependencies = []; - $exportedNodes = []; for ($i = 0; $i < $numberOfProcesses; $i++) { if (count($jobs) === 0) { break; @@ -124,13 +156,6 @@ public function analyse( $processIdentifier, ]; - if ($tmpFile !== null && $insteadOfFile !== null) { - $commandOptions[] = '--tmp-file'; - $commandOptions[] = escapeshellarg($tmpFile); - $commandOptions[] = '--instead-of'; - $commandOptions[] = escapeshellarg($insteadOfFile); - } - $process = new Process(ProcessHelper::getWorkerCommand( $mainScript, 'worker', @@ -138,14 +163,40 @@ public function analyse( $commandOptions, $input, ), $loop, $this->processTimeout); - $process->start(function (array $json) use ($process, &$internalErrors, &$errors, &$collectedData, &$dependencies, &$exportedNodes, &$jobs, $postFileCallback, &$internalErrorsCount, &$reachedInternalErrorsCountLimit, $processIdentifier): void { + $process->start(function (array $json) use ($process, &$internalErrors, &$errors, &$filteredPhpErrors, &$allPhpErrors, &$locallyIgnoredErrors, &$linesToIgnore, &$unmatchedLineIgnores, &$collectedData, &$dependencies, &$exportedNodes, &$peakMemoryUsages, &$jobs, $postFileCallback, &$internalErrorsCount, &$reachedInternalErrorsCountLimit, $processIdentifier, $onFileAnalysisHandler): void { + $fileErrors = []; foreach ($json['errors'] as $jsonError) { if (is_string($jsonError)) { $internalErrors[] = sprintf('Internal error: %s', $jsonError); continue; } - $errors[] = Error::decode($jsonError); + $fileErrors[] = Error::decode($jsonError); + } + + foreach ($json['filteredPhpErrors'] as $filteredPhpError) { + $filteredPhpErrors[] = Error::decode($filteredPhpError); + } + + foreach ($json['allPhpErrors'] as $allPhpError) { + $allPhpErrors[] = Error::decode($allPhpError); + } + + $locallyIgnoredFileErrors = []; + foreach ($json['locallyIgnoredErrors'] as $locallyIgnoredJsonError) { + $locallyIgnoredFileErrors[] = Error::decode($locallyIgnoredJsonError); + } + + if ($onFileAnalysisHandler !== null) { + $onFileAnalysisHandler($fileErrors, $locallyIgnoredFileErrors, $json['files']); + } + + foreach ($fileErrors as $fileError) { + $errors[] = $fileError; + } + + foreach ($locallyIgnoredFileErrors as $locallyIgnoredFileError) { + $locallyIgnoredErrors[] = $locallyIgnoredFileError; } foreach ($json['collectedData'] as $jsonData) { @@ -160,6 +211,20 @@ public function analyse( $dependencies[$file] = $fileDependencies; } + foreach ($json['linesToIgnore'] as $file => $fileLinesToIgnore) { + if (count($fileLinesToIgnore) === 0) { + continue; + } + $linesToIgnore[$file] = $fileLinesToIgnore; + } + + foreach ($json['unmatchedLineIgnores'] as $file => $fileUnmatchedLineIgnores) { + if (count($fileUnmatchedLineIgnores) === 0) { + continue; + } + $unmatchedLineIgnores[$file] = $fileUnmatchedLineIgnores; + } + /** * @var string $file * @var array $fileExportedNodes @@ -176,7 +241,11 @@ public function analyse( } if ($postFileCallback !== null) { - $postFileCallback($json['filesCount']); + $postFileCallback(count($json['files'])); + } + + if (!isset($peakMemoryUsages[$processIdentifier]) || $peakMemoryUsages[$processIdentifier] < $json['memoryUsage']) { + $peakMemoryUsages[$processIdentifier] = $json['memoryUsage']; } $internalErrorsCount += $json['internalErrorsCount']; @@ -192,7 +261,12 @@ public function analyse( $job = array_pop($jobs); $process->request(['action' => 'analyse', 'files' => $job]); - }, $handleError, function ($exitCode, string $output) use (&$internalErrors, &$internalErrorsCount, $processIdentifier): void { + }, $handleError, function ($exitCode, string $output) use (&$someChildEnded, &$peakMemoryUsages, &$internalErrors, &$internalErrorsCount, $processIdentifier): void { + if ($someChildEnded === false) { + $peakMemoryUsages['main'] = memory_get_usage(true); + } + $someChildEnded = true; + $this->processPool->tryQuitProcess($processIdentifier); if ($exitCode === 0) { return; @@ -201,27 +275,32 @@ public function analyse( return; } - $internalErrors[] = sprintf('Child process error (exit code %d): %s', $exitCode, $output); + $memoryLimitMessage = 'PHPStan process crashed because it reached configured PHP memory limit'; + if (str_contains($output, $memoryLimitMessage)) { + foreach ($internalErrors as $internalError) { + if (!str_contains($internalError, $memoryLimitMessage)) { + continue; + } + + return; + } + $internalErrors[] = sprintf(sprintf( + "Child process error: %s: %s\n%s\n", + $memoryLimitMessage, + ini_get('memory_limit'), + 'Increase your memory limit in php.ini or run PHPStan with --memory-limit CLI option.', + )); + $internalErrorsCount++; + return; + } + + $internalErrors[] = sprintf('Child process error (exit code %d): %s', $exitCode, $output); $internalErrorsCount++; }); $this->processPool->attachProcess($processIdentifier, $process); } - $loop->run(); - - if (count($jobs) > 0 && $internalErrorsCount === 0) { - $internalErrors[] = 'Some parallel worker jobs have not finished.'; - $internalErrorsCount++; - } - - return new AnalyserResult( - $errors, - $internalErrors, - $collectedData, - $internalErrorsCount === 0 ? $dependencies : null, - $exportedNodes, - $reachedInternalErrorsCountLimit, - ); + return $deferred->promise(); } } diff --git a/src/Parallel/Process.php b/src/Parallel/Process.php index 0cb84ee154..34fc0dcc5c 100644 --- a/src/Parallel/Process.php +++ b/src/Parallel/Process.php @@ -21,7 +21,7 @@ class Process public \React\ChildProcess\Process $process; - private WritableStreamInterface $in; + private ?WritableStreamInterface $in = null; /** @var resource */ private $stdOut; @@ -106,6 +106,9 @@ private function cancelTimer(): void public function request(array $data): void { $this->cancelTimer(); + if ($this->in === null) { + throw new ShouldNotHappenException(); + } $this->in->write($data); $this->timer = $this->loop->addTimer($this->timeoutSeconds, function (): void { $onError = $this->onError; @@ -124,6 +127,10 @@ public function quit(): void $pipe->close(); } + if ($this->in === null) { + return; + } + $this->in->end(); } diff --git a/src/Parallel/ProcessPool.php b/src/Parallel/ProcessPool.php index bebb8f8293..ac0569c509 100644 --- a/src/Parallel/ProcessPool.php +++ b/src/Parallel/ProcessPool.php @@ -15,8 +15,15 @@ class ProcessPool /** @var array */ private array $processes = []; - public function __construct(private TcpServer $server) + /** @var callable(): void */ + private $onServerClose; + + /** + * @param callable(): void $onServerClose + */ + public function __construct(private TcpServer $server, callable $onServerClose) { + $this->onServerClose = $onServerClose; } public function getProcess(string $identifier): Process @@ -52,6 +59,8 @@ private function quitProcess(string $identifier): void } $this->server->close(); + $callback = $this->onServerClose; + $callback(); } public function quitAll(): void diff --git a/src/Parser/CleaningVisitor.php b/src/Parser/CleaningVisitor.php index b5d406cf17..0a2c9aecff 100644 --- a/src/Parser/CleaningVisitor.php +++ b/src/Parser/CleaningVisitor.php @@ -52,11 +52,20 @@ private function keepVariadicsAndYields(array $stmts): array return in_array($node->name->toLowerString(), ParametersAcceptor::VARIADIC_FUNCTIONS, true); } + if ($node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction) { + return true; + } + return false; }); $newStmts = []; foreach ($results as $result) { - if ($result instanceof Node\Expr\Yield_ || $result instanceof Node\Expr\YieldFrom) { + if ( + $result instanceof Node\Expr\Yield_ + || $result instanceof Node\Expr\YieldFrom + || $result instanceof Node\Expr\Closure + || $result instanceof Node\Expr\ArrowFunction + ) { $newStmts[] = new Node\Stmt\Expression($result); continue; } diff --git a/src/Parser/ClosureBindArgVisitor.php b/src/Parser/ClosureBindArgVisitor.php new file mode 100644 index 0000000000..de341a57d9 --- /dev/null +++ b/src/Parser/ClosureBindArgVisitor.php @@ -0,0 +1,33 @@ +class instanceof Node\Name + && $node->class->toLowerString() === 'closure' + && $node->name instanceof Identifier + && $node->name->toLowerString() === 'bind' + && !$node->isFirstClassCallable() + ) { + $args = $node->getArgs(); + if (count($args) > 1) { + $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); + } + } + return null; + } + +} diff --git a/src/Parser/ClosureBindToVarVisitor.php b/src/Parser/ClosureBindToVarVisitor.php new file mode 100644 index 0000000000..f3582ba617 --- /dev/null +++ b/src/Parser/ClosureBindToVarVisitor.php @@ -0,0 +1,30 @@ +name instanceof Identifier + && $node->name->toLowerString() === 'bindto' + && !$node->isFirstClassCallable() + ) { + $args = $node->getArgs(); + if (isset($args[0])) { + $args[0]->setAttribute(self::ATTRIBUTE_NAME, $node->var); + } + } + return null; + } + +} diff --git a/src/Parser/DeclarePositionVisitor.php b/src/Parser/DeclarePositionVisitor.php new file mode 100644 index 0000000000..08818c1652 --- /dev/null +++ b/src/Parser/DeclarePositionVisitor.php @@ -0,0 +1,44 @@ +isFirstStatement = true; + return null; + } + + public function enterNode(Node $node): ?Node + { + // ignore shebang + if ( + $this->isFirstStatement + && $node instanceof Node\Stmt\InlineHTML + && str_starts_with($node->value, '#!') + ) { + return null; + } + + if ($node instanceof Node\Stmt) { + if ($node instanceof Node\Stmt\Declare_) { + $node->setAttribute(self::ATTRIBUTE_NAME, $this->isFirstStatement); + } + + $this->isFirstStatement = false; + } + + return null; + } + +} diff --git a/src/Parser/LastConditionVisitor.php b/src/Parser/LastConditionVisitor.php new file mode 100644 index 0000000000..5edd559ed0 --- /dev/null +++ b/src/Parser/LastConditionVisitor.php @@ -0,0 +1,83 @@ +elseifs !== []) { + $lastElseIf = count($node->elseifs) - 1; + + $elseIsMissingOrThrowing = $node->else === null + || (count($node->else->stmts) === 1 && $node->else->stmts[0] instanceof Node\Stmt\Throw_); + + foreach ($node->elseifs as $i => $elseif) { + $isLast = $i === $lastElseIf && $elseIsMissingOrThrowing; + $elseif->cond->setAttribute(self::ATTRIBUTE_NAME, $isLast); + } + } + + if ($node instanceof Node\Expr\Match_ && $node->arms !== []) { + $lastArm = count($node->arms) - 1; + + foreach ($node->arms as $i => $arm) { + if ($arm->conds === null || $arm->conds === []) { + continue; + } + + $isLast = $i === $lastArm; + $index = count($arm->conds) - 1; + $arm->conds[$index]->setAttribute(self::ATTRIBUTE_NAME, $isLast); + $arm->conds[$index]->setAttribute(self::ATTRIBUTE_IS_MATCH_NAME, true); + } + } + + if ( + $node instanceof Node\Stmt\Function_ + || $node instanceof Node\Stmt\ClassMethod + || $node instanceof Node\Stmt\If_ + || $node instanceof Node\Stmt\ElseIf_ + || $node instanceof Node\Stmt\Else_ + || $node instanceof Node\Stmt\Case_ + || $node instanceof Node\Stmt\Catch_ + || $node instanceof Node\Stmt\Do_ + || $node instanceof Node\Stmt\Finally_ + || $node instanceof Node\Stmt\For_ + || $node instanceof Node\Stmt\Foreach_ + || $node instanceof Node\Stmt\Namespace_ + || $node instanceof Node\Stmt\TryCatch + || $node instanceof Node\Stmt\While_ + ) { + $statements = $node->stmts ?? []; + $statementCount = count($statements); + + if ($statementCount < 2) { + return null; + } + + if (!$statements[$statementCount - 1] instanceof Node\Stmt\Throw_) { + return null; + } + + if (!$statements[$statementCount - 2] instanceof Node\Stmt\If_ || $statements[$statementCount - 2]->else !== null) { + return null; + } + + $if = $statements[$statementCount - 2]; + $cond = count($if->elseifs) > 0 ? $if->elseifs[count($if->elseifs) - 1]->cond : $if->cond; + $cond->setAttribute(self::ATTRIBUTE_NAME, true); + } + + return null; + } + +} diff --git a/src/Parser/MagicConstantParamDefaultVisitor.php b/src/Parser/MagicConstantParamDefaultVisitor.php new file mode 100644 index 0000000000..5bc27ada08 --- /dev/null +++ b/src/Parser/MagicConstantParamDefaultVisitor.php @@ -0,0 +1,21 @@ +default instanceof Node\Scalar\MagicConst) { + $node->default->setAttribute(self::ATTRIBUTE_NAME, true); + } + return null; + } + +} diff --git a/src/Parser/NodeList.php b/src/Parser/NodeList.php deleted file mode 100644 index 13e69eef13..0000000000 --- a/src/Parser/NodeList.php +++ /dev/null @@ -1,37 +0,0 @@ -next !== null) { - $current = $current->next; - } - - $new = new self($node); - $current->next = $new; - - return $new; - } - - public function getNode(): Node - { - return $this->node; - } - - public function getNext(): ?self - { - return $this->next; - } - -} diff --git a/src/Parser/ParserErrorsException.php b/src/Parser/ParserErrorsException.php index dbae41eacc..1013be73d4 100644 --- a/src/Parser/ParserErrorsException.php +++ b/src/Parser/ParserErrorsException.php @@ -22,7 +22,7 @@ public function __construct( private ?string $parsedFile, ) { - parent::__construct(implode(', ', array_map(static fn (Error $error): string => $error->getMessage(), $errors))); + parent::__construct(implode(', ', array_map(static fn (Error $error): string => $error->getRawMessage(), $errors))); if (count($errors) > 0) { $this->attributes = $errors[0]->getAttributes(); } else { diff --git a/src/Parser/PathRoutingParser.php b/src/Parser/PathRoutingParser.php index d9c804740e..436416bcf9 100644 --- a/src/Parser/PathRoutingParser.php +++ b/src/Parser/PathRoutingParser.php @@ -4,13 +4,11 @@ use PHPStan\File\FileHelper; use function array_fill_keys; -use function strpos; +use function str_contains; class PathRoutingParser implements Parser { - private ?string $singleReflectionFile; - /** @var bool[] filePath(string) => bool(true) */ private array $analysedFiles = []; @@ -19,10 +17,8 @@ public function __construct( private Parser $currentPhpVersionRichParser, private Parser $currentPhpVersionSimpleParser, private Parser $php8Parser, - ?string $singleReflectionFile, ) { - $this->singleReflectionFile = $singleReflectionFile !== null ? $fileHelper->normalizePath($singleReflectionFile) : null; } /** @@ -36,15 +32,15 @@ public function setAnalysedFiles(array $files): void public function parseFile(string $file): array { $normalizedPath = $this->fileHelper->normalizePath($file, '/'); - if (strpos($normalizedPath, 'vendor/jetbrains/phpstorm-stubs') !== false) { + if (str_contains($normalizedPath, 'vendor/jetbrains/phpstorm-stubs')) { return $this->php8Parser->parseFile($file); } - if (strpos($normalizedPath, 'vendor/phpstan/php-8-stubs/stubs') !== false) { + if (str_contains($normalizedPath, 'vendor/phpstan/php-8-stubs/stubs')) { return $this->php8Parser->parseFile($file); } $file = $this->fileHelper->normalizePath($file); - if (!isset($this->analysedFiles[$file]) && $file !== $this->singleReflectionFile) { + if (!isset($this->analysedFiles[$file])) { return $this->currentPhpVersionSimpleParser->parseFile($file); } diff --git a/src/Parser/RemoveUnusedCodeByPhpVersionIdVisitor.php b/src/Parser/RemoveUnusedCodeByPhpVersionIdVisitor.php index 076ddcccfd..eed9b93bf7 100644 --- a/src/Parser/RemoveUnusedCodeByPhpVersionIdVisitor.php +++ b/src/Parser/RemoveUnusedCodeByPhpVersionIdVisitor.php @@ -17,13 +17,6 @@ public function __construct(private string $phpVersionString) public function enterNode(Node $node): Node|int|null { - if ($node instanceof Node\Stmt\ClassLike) { - return null; - } - if ($node instanceof Node\FunctionLike) { - return null; - } - if (!$node instanceof Node\Stmt\If_) { return null; } @@ -53,8 +46,7 @@ public function enterNode(Node $node): Node|int|null $operator = $cond->getOperatorSigil(); if ($operator === '===') { $operator = '=='; - } - if ($operator === '!==') { + } elseif ($operator === '!==') { $operator = '!='; } diff --git a/src/Parser/RichParser.php b/src/Parser/RichParser.php index 483229d8f0..f7ef05a228 100644 --- a/src/Parser/RichParser.php +++ b/src/Parser/RichParser.php @@ -7,25 +7,47 @@ use PhpParser\Node; use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\NameResolver; +use PHPStan\Analyser\Ignore\IgnoreLexer; +use PHPStan\Analyser\Ignore\IgnoreParseException; use PHPStan\DependencyInjection\Container; use PHPStan\File\FileReader; use PHPStan\ShouldNotHappenException; +use function array_filter; +use function array_pop; +use function array_values; +use function count; +use function implode; +use function in_array; use function is_string; +use function preg_match_all; +use function sprintf; +use function str_contains; +use function strlen; use function strpos; +use function substr; use function substr_count; +use const ARRAY_FILTER_USE_KEY; +use const PREG_OFFSET_CAPTURE; use const T_COMMENT; use const T_DOC_COMMENT; +use const T_WHITESPACE; class RichParser implements Parser { public const VISITOR_SERVICE_TAG = 'phpstan.parser.richParserNodeVisitor'; + private const PHPDOC_TAG_REGEX = '(@(?:[a-z][a-z0-9-\\\\]+:)?[a-z][a-z0-9-\\\\]*+)'; + + private const PHPDOC_DOCTRINE_TAG_REGEX = '(@[a-z_\\\\][a-z0-9_\:\\\\]*[a-z_][a-z0-9_]*)'; + public function __construct( private \PhpParser\Parser $parser, private Lexer $lexer, private NameResolver $nameResolver, private Container $container, + private IgnoreLexer $ignoreLexer, + private bool $enableIgnoreErrorsWithinPhpDocs = false, ) { } @@ -50,6 +72,8 @@ public function parseString(string $sourceCode): array { $errorHandler = new Collecting(); $nodes = $this->parser->parse($sourceCode, $errorHandler); + + /** @var list $tokens */ $tokens = $this->lexer->getTokens(); if ($errorHandler->hasErrors()) { throw new ParserErrorsException($errorHandler->getErrors(), null); @@ -61,50 +85,251 @@ public function parseString(string $sourceCode): array $nodeTraverser = new NodeTraverser(); $nodeTraverser->addVisitor($this->nameResolver); + $traitCollectingVisitor = new TraitCollectingVisitor(); + $nodeTraverser->addVisitor($traitCollectingVisitor); + foreach ($this->container->getServicesByTag(self::VISITOR_SERVICE_TAG) as $visitor) { $nodeTraverser->addVisitor($visitor); } /** @var array */ $nodes = $nodeTraverser->traverse($nodes); + ['lines' => $linesToIgnore, 'errors' => $ignoreParseErrors] = $this->getLinesToIgnore($tokens); if (isset($nodes[0])) { - $nodes[0]->setAttribute('linesToIgnore', $this->getLinesToIgnore($tokens)); + $nodes[0]->setAttribute('linesToIgnore', $linesToIgnore); + if (count($ignoreParseErrors) > 0) { + $nodes[0]->setAttribute('linesToIgnoreParseErrors', $ignoreParseErrors); + } + } + + foreach ($traitCollectingVisitor->traits as $trait) { + $trait->setAttribute('linesToIgnore', array_filter($linesToIgnore, static fn (int $line): bool => $line >= $trait->getStartLine() && $line <= $trait->getEndLine(), ARRAY_FILTER_USE_KEY)); } return $nodes; } /** - * @param mixed[] $tokens - * @return int[] + * @param list $tokens + * @return array{lines: array|null>, errors: array>} */ private function getLinesToIgnore(array $tokens): array { $lines = []; + $previousToken = null; + $pendingToken = null; + $errors = []; foreach ($tokens as $token) { if (is_string($token)) { continue; } $type = $token[0]; + $line = $token[2]; if ($type !== T_COMMENT && $type !== T_DOC_COMMENT) { + if ($type !== T_WHITESPACE) { + if ($pendingToken !== null) { + [$pendingText, $pendingIgnorePos, $tokenLine, $pendingLine] = $pendingToken; + + try { + $identifiers = $this->parseIdentifiers($pendingText, $pendingIgnorePos); + } catch (IgnoreParseException $e) { + $errors[] = [$tokenLine + $e->getPhpDocLine(), $e->getMessage()]; + $pendingToken = null; + continue; + } + + if ($line !== $pendingLine + 1) { + $lineToAdd = $pendingLine; + } else { + $lineToAdd = $line; + } + + foreach ($identifiers as $identifier) { + $lines[$lineToAdd][] = $identifier; + } + + $pendingToken = null; + } + $previousToken = $token; + } continue; } $text = $token[1]; - $line = $token[2]; - if (strpos($text, '@phpstan-ignore-next-line') !== false) { - $line++; - } elseif (strpos($text, '@phpstan-ignore-line') === false) { + $isNextLine = str_contains($text, '@phpstan-ignore-next-line'); + $isCurrentLine = str_contains($text, '@phpstan-ignore-line'); + + if ($this->enableIgnoreErrorsWithinPhpDocs && $type === T_DOC_COMMENT) { + $lines += $this->getLinesToIgnoreForTokenByIgnoreComment($text, $line, '@phpstan-ignore-line'); + if ($isNextLine) { + $pattern = sprintf('~%s~si', implode('|', [self::PHPDOC_TAG_REGEX, self::PHPDOC_DOCTRINE_TAG_REGEX])); + $r = preg_match_all($pattern, $text, $pregMatches, PREG_OFFSET_CAPTURE); + if ($r !== false) { + $c = count($pregMatches[0]); + if ($c > 0) { + [$lastMatchTag, $lastMatchOffset] = $pregMatches[0][$c - 1]; + if ($lastMatchTag === '@phpstan-ignore-next-line') { + // this will let us ignore errors outside of PHPDoc + // and also cut off the PHPDoc text before the last tag + $lineToIgnore = $line + 1 + substr_count($text, "\n"); + $lines[$lineToIgnore] = null; + $text = substr($text, 0, $lastMatchOffset); + } + } + } + + $lines += $this->getLinesToIgnoreForTokenByIgnoreComment($text, $line, '@phpstan-ignore-next-line', true); + } + + if ($isNextLine || $isCurrentLine) { + continue; + } + + } else { + if ($isNextLine) { + $line++; + } + if ($isNextLine || $isCurrentLine) { + $line += substr_count($token[1], "\n"); + + $lines[$line] = null; + continue; + } + } + + $ignorePos = strpos($text, '@phpstan-ignore'); + if ($ignorePos === false) { + continue; + } + + $ignoreLine = substr_count(substr($text, 0, $ignorePos), "\n") - 1; + + if ($previousToken !== null && $previousToken[2] === $line) { + try { + foreach ($this->parseIdentifiers($text, $ignorePos) as $identifier) { + $lines[$line][] = $identifier; + } + } catch (IgnoreParseException $e) { + $errors[] = [$token[2] + $e->getPhpDocLine() + $ignoreLine, $e->getMessage()]; + } + continue; } $line += substr_count($token[1], "\n"); + $pendingToken = [$text, $ignorePos, $token[2] + $ignoreLine, $line]; + } + + if ($pendingToken !== null) { + [$pendingText, $pendingIgnorePos, $tokenLine, $pendingLine] = $pendingToken; + + try { + foreach ($this->parseIdentifiers($pendingText, $pendingIgnorePos) as $identifier) { + $lines[$pendingLine][] = $identifier; + } + } catch (IgnoreParseException $e) { + $errors[] = [$tokenLine + $e->getPhpDocLine(), $e->getMessage()]; + } + } + + $processedErrors = []; + foreach ($errors as [$line, $message]) { + $processedErrors[$line][] = $message; + } + + return [ + 'lines' => $lines, + 'errors' => $processedErrors, + ]; + } + + /** + * @return array + */ + private function getLinesToIgnoreForTokenByIgnoreComment( + string $tokenText, + int $tokenLine, + string $ignoreComment, + bool $ignoreNextLine = false, + ): array + { + $lines = []; + $positionsOfIgnoreComment = []; + $offset = 0; - $lines[] = $line; + while (($pos = strpos($tokenText, $ignoreComment, $offset)) !== false) { + $positionsOfIgnoreComment[] = $pos; + $offset = $pos + 1; + } + + foreach ($positionsOfIgnoreComment as $pos) { + $line = $tokenLine + substr_count(substr($tokenText, 0, $pos), "\n") + ($ignoreNextLine ? 1 : 0); + $lines[$line] = null; } return $lines; } + /** + * @return non-empty-list + * @throws IgnoreParseException + */ + private function parseIdentifiers(string $text, int $ignorePos): array + { + $text = substr($text, $ignorePos + strlen('@phpstan-ignore')); + $tokens = $this->ignoreLexer->tokenize($text); + $tokens = array_values(array_filter($tokens, static fn (array $token) => !in_array($token[IgnoreLexer::TYPE_OFFSET], [IgnoreLexer::TOKEN_WHITESPACE, IgnoreLexer::TOKEN_EOL], true))); + $c = count($tokens); + + $identifiers = []; + $depth = 0; + $parenthesisStack = []; + for ($i = 0; $i < $c; $i++) { + [IgnoreLexer::VALUE_OFFSET => $content, IgnoreLexer::TYPE_OFFSET => $tokenType, IgnoreLexer::LINE_OFFSET => $tokenLine] = $tokens[$i]; + if ($tokenType === IgnoreLexer::TOKEN_IDENTIFIER && $depth === 0) { + $identifiers[] = $content; + if (isset($tokens[$i + 1])) { + if ($tokens[$i + 1][IgnoreLexer::TYPE_OFFSET] === IgnoreLexer::TOKEN_COMMA) { + $i++; + } + } + continue; + } + if ($i === 0) { + throw new IgnoreParseException('First token is not an identifier', $tokenLine); + } + if ($tokenType === IgnoreLexer::TOKEN_COMMA && $depth === 0) { + throw new IgnoreParseException('Unexpected comma (,)', $tokenLine); + } + if ($tokenType === IgnoreLexer::TOKEN_CLOSE_PARENTHESIS) { + if ($depth < 1) { + throw new IgnoreParseException('Closing parenthesis ")" before opening parenthesis "("', $tokenLine); + } + + $depth--; + array_pop($parenthesisStack); + if ($depth === 0) { + break; + } + } + if ($tokenType !== IgnoreLexer::TOKEN_OPEN_PARENTHESIS) { + continue; + } + + $depth++; + $parenthesisStack[] = $tokenLine; + } + + if (count($parenthesisStack) > 0) { + throw new IgnoreParseException('Unclosed opening parenthesis "(" without closing parenthesis ")"', $parenthesisStack[count($parenthesisStack) - 1]); + } + + if (count($identifiers) === 0) { + throw new IgnoreParseException('Missing identifier', 1); + } + + return $identifiers; + } + } diff --git a/src/Parser/TraitCollectingVisitor.php b/src/Parser/TraitCollectingVisitor.php new file mode 100644 index 0000000000..e6341a9de6 --- /dev/null +++ b/src/Parser/TraitCollectingVisitor.php @@ -0,0 +1,25 @@ + */ + public array $traits = []; + + public function enterNode(Node $node): ?Node + { + if (!$node instanceof Node\Stmt\Trait_) { + return null; + } + + $this->traits[] = $node; + + return null; + } + +} diff --git a/src/Parser/TypeTraverserInstanceofVisitor.php b/src/Parser/TypeTraverserInstanceofVisitor.php new file mode 100644 index 0000000000..a39226bbb2 --- /dev/null +++ b/src/Parser/TypeTraverserInstanceofVisitor.php @@ -0,0 +1,56 @@ +depth = 0; + return null; + } + + public function enterNode(Node $node): ?Node + { + if ($node instanceof Node\Expr\Instanceof_ && $this->depth > 0) { + $node->setAttribute(self::ATTRIBUTE_NAME, true); + return null; + } + + if ( + $node instanceof Node\Expr\StaticCall + && $node->class instanceof Node\Name + && $node->class->toLowerString() === 'phpstan\\type\\typetraverser' + && $node->name instanceof Node\Identifier + && $node->name->toLowerString() === 'map' + ) { + $this->depth++; + } + + return null; + } + + public function leaveNode(Node $node): ?Node + { + if ( + $node instanceof Node\Expr\StaticCall + && $node->class instanceof Node\Name + && $node->class->toLowerString() === 'phpstan\\type\\typetraverser' + && $node->name instanceof Node\Identifier + && $node->name->toLowerString() === 'map' + ) { + $this->depth--; + } + + return null; + } + +} diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index d9c63aa40d..6a1fe74bf2 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -41,6 +41,11 @@ public function supportsReturnCovariance(): bool return $this->versionId >= 70400; } + public function supportsNoncapturingCatches(): bool + { + return $this->versionId >= 80000; + } + public function supportsNativeUnionTypes(): bool { return $this->versionId >= 80000; @@ -51,6 +56,16 @@ public function deprecatesRequiredParameterAfterOptional(): bool return $this->versionId >= 80000; } + public function deprecatesRequiredParameterAfterOptionalNullableAndDefaultNull(): bool + { + return $this->versionId >= 80100; + } + + public function deprecatesRequiredParameterAfterOptionalUnionOrMixed(): bool + { + return $this->versionId >= 80300; + } + public function supportsLessOverridenParametersWithVariadic(): bool { return $this->versionId >= 80000; @@ -201,4 +216,75 @@ public function strSplitReturnsEmptyArray(): bool return $this->versionId >= 80200; } + public function supportsDisjunctiveNormalForm(): bool + { + return $this->versionId >= 80200; + } + + public function serializableRequiresMagicMethods(): bool + { + return $this->versionId >= 80100; + } + + public function arrayFunctionsReturnNullWithNonArray(): bool + { + return $this->versionId < 80000; + } + + // see https://www.php.net/manual/en/migration80.incompatible.php#migration80.incompatible.core.string-number-comparision + public function castsNumbersToStringsOnLooseComparison(): bool + { + return $this->versionId >= 80000; + } + + public function supportsCallableInstanceMethods(): bool + { + return $this->versionId < 80000; + } + + public function supportsJsonValidate(): bool + { + return $this->versionId >= 80300; + } + + public function supportsConstantsInTraits(): bool + { + return $this->versionId >= 80200; + } + + public function supportsNativeTypesInClassConstants(): bool + { + return $this->versionId >= 80300; + } + + public function supportsAbstractTraitMethods(): bool + { + return $this->versionId >= 80000; + } + + public function supportsOverrideAttribute(): bool + { + return $this->versionId >= 80300; + } + + public function supportsDynamicClassConstantFetch(): bool + { + return $this->versionId >= 80300; + } + + public function supportsReadOnlyClasses(): bool + { + return $this->versionId >= 80200; + } + + public function supportsReadOnlyAnonymousClasses(): bool + { + return $this->versionId >= 80300; + } + + public function supportsNeverReturnTypeInArrowFunction(): bool + { + return $this->versionId >= 80200; + } + } diff --git a/src/Php/PhpVersionFactory.php b/src/Php/PhpVersionFactory.php index c635c3aa67..2ed8fe6781 100644 --- a/src/Php/PhpVersionFactory.php +++ b/src/Php/PhpVersionFactory.php @@ -24,7 +24,7 @@ public function create(): PhpVersion $parts = explode('.', $this->composerPhpVersion); $tmp = (int) $parts[0] * 10000 + (int) ($parts[1] ?? 0) * 100 + (int) ($parts[2] ?? 0); $tmp = max($tmp, 70100); - $versionId = min($tmp, 80299); + $versionId = min($tmp, 80399); } if ($versionId === null) { diff --git a/src/PhpDoc/ConstExprParserFactory.php b/src/PhpDoc/ConstExprParserFactory.php new file mode 100644 index 0000000000..aa2ca2657d --- /dev/null +++ b/src/PhpDoc/ConstExprParserFactory.php @@ -0,0 +1,19 @@ +unescapeStrings, $this->unescapeStrings); + } + +} diff --git a/src/PhpDoc/DefaultStubFilesProvider.php b/src/PhpDoc/DefaultStubFilesProvider.php index fe43beb63d..da52332a1f 100644 --- a/src/PhpDoc/DefaultStubFilesProvider.php +++ b/src/PhpDoc/DefaultStubFilesProvider.php @@ -6,7 +6,8 @@ use PHPStan\Internal\ComposerHelper; use function array_filter; use function array_values; -use function strpos; +use function str_contains; +use function strtr; class DefaultStubFilesProvider implements StubFilesProvider { @@ -57,9 +58,12 @@ public function getProjectStubFiles(): array return $this->getStubFiles(); } + $vendorDir = ComposerHelper::getVendorDirFromComposerConfig($this->currentWorkingDirectory, $composerConfig); + $vendorDir = strtr($vendorDir, '\\', '/'); + return $this->cachedProjectFiles = array_values(array_filter( $this->getStubFiles(), - fn (string $file): bool => strpos($file, ComposerHelper::getVendorDirFromComposerConfig($this->currentWorkingDirectory, $composerConfig)) === false + static fn (string $file): bool => !str_contains(strtr($file, '\\', '/'), $vendorDir) )); } diff --git a/src/PhpDoc/EmptyStubFilesProvider.php b/src/PhpDoc/EmptyStubFilesProvider.php deleted file mode 100644 index 0f5bb2124d..0000000000 --- a/src/PhpDoc/EmptyStubFilesProvider.php +++ /dev/null @@ -1,18 +0,0 @@ -phpVersion->supportsJsonValidate()) { + return []; + } + + return [__DIR__ . '/../../stubs/json_validate.stub']; + } + +} diff --git a/src/PhpDoc/PhpDocBlock.php b/src/PhpDoc/PhpDocBlock.php index 345991dfb0..c4a6f6ce43 100644 --- a/src/PhpDoc/PhpDocBlock.php +++ b/src/PhpDoc/PhpDocBlock.php @@ -2,6 +2,7 @@ namespace PHPStan\PhpDoc; +use PHPStan\PhpDoc\Tag\AssertTagParameter; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\MethodReflection; @@ -102,6 +103,16 @@ public function transformConditionalReturnTypeWithParameterNameMapping(Type $typ }); } + public function transformAssertTagParameterWithParameterNameMapping(AssertTagParameter $parameter): AssertTagParameter + { + $parameterName = substr($parameter->getParameterName(), 1); + if (array_key_exists($parameterName, $this->parameterNameMapping)) { + $parameter = $parameter->changeParameterName('$' . $this->parameterNameMapping[$parameterName]); + } + + return $parameter; + } + /** * @param array $originalPositionalParameterNames * @param array $newPositionalParameterNames @@ -221,7 +232,7 @@ private static function resolvePhpDocBlockTree( ); return new self( - $docComment ?? '/** */', + $docComment ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, $file, $classReflection, $trait, @@ -364,7 +375,7 @@ private static function resolvePhpDocBlockFromClass( : null; return self::$resolveMethodName( - $parentReflection->getDocComment() ?? '/** */', + $parentReflection->getDocComment() ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, $classReflection, $trait, $name, diff --git a/src/PhpDoc/PhpDocInheritanceResolver.php b/src/PhpDoc/PhpDocInheritanceResolver.php index 19cde20787..a0b34db3e0 100644 --- a/src/PhpDoc/PhpDocInheritanceResolver.php +++ b/src/PhpDoc/PhpDocInheritanceResolver.php @@ -94,9 +94,9 @@ private function docBlockTreeToResolvedDocBlock(PhpDocBlock $phpDocBlock, ?strin foreach ($phpDocBlock->getParents() as $parentPhpDocBlock) { if ( - $parentPhpDocBlock->getClassReflection()->isBuiltin() - && $functionName !== null + $functionName !== null && strtolower($functionName) === '__construct' + && $parentPhpDocBlock->getClassReflection()->isBuiltin() ) { continue; } diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index aac5b62766..c11c26d7b0 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -3,15 +3,22 @@ namespace PHPStan\PhpDoc; use PHPStan\Analyser\NameScope; +use PHPStan\PhpDoc\Tag\AssertTag; +use PHPStan\PhpDoc\Tag\AssertTagParameter; use PHPStan\PhpDoc\Tag\DeprecatedTag; use PHPStan\PhpDoc\Tag\ExtendsTag; use PHPStan\PhpDoc\Tag\ImplementsTag; use PHPStan\PhpDoc\Tag\MethodTag; use PHPStan\PhpDoc\Tag\MethodTagParameter; use PHPStan\PhpDoc\Tag\MixinTag; +use PHPStan\PhpDoc\Tag\ParamClosureThisTag; +use PHPStan\PhpDoc\Tag\ParamOutTag; use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\PhpDoc\Tag\PropertyTag; +use PHPStan\PhpDoc\Tag\RequireExtendsTag; +use PHPStan\PhpDoc\Tag\RequireImplementsTag; use PHPStan\PhpDoc\Tag\ReturnTag; +use PHPStan\PhpDoc\Tag\SelfOutTypeTag; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDoc\Tag\ThrowsTag; use PHPStan\PhpDoc\Tag\TypeAliasImportTag; @@ -24,15 +31,21 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\Reflection\PassedByReference; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Type\Generic\TemplateTypeFactory; +use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_key_exists; use function array_map; +use function array_merge; use function array_reverse; use function count; use function in_array; -use function strpos; +use function method_exists; +use function str_starts_with; use function substr; class PhpDocNodeResolver @@ -53,7 +66,7 @@ public function resolveVarTags(PhpDocNode $phpDocNode, NameScope $nameScope): ar { $resolved = []; $resolvedByTag = []; - foreach (['@var', '@psalm-var', '@phpstan-var'] as $tagName) { + foreach (['@var', '@phan-var', '@psalm-var', '@phpstan-var'] as $tagName) { $tagResolved = []; foreach ($phpDocNode->getVarTagValues($tagName) as $tagValue) { $type = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); @@ -97,8 +110,8 @@ public function resolvePropertyTags(PhpDocNode $phpDocNode, NameScope $nameScope $resolved[$propertyName] = new PropertyTag( $propertyType, - true, - true, + $propertyType, + $propertyType, ); } } @@ -108,10 +121,15 @@ public function resolvePropertyTags(PhpDocNode $phpDocNode, NameScope $nameScope $propertyName = substr($tagValue->propertyName, 1); $propertyType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); + $writableType = null; + if (array_key_exists($propertyName, $resolved)) { + $writableType = $resolved[$propertyName]->getWritableType(); + } + $resolved[$propertyName] = new PropertyTag( $propertyType, - true, - false, + $propertyType, + $writableType, ); } } @@ -121,10 +139,15 @@ public function resolvePropertyTags(PhpDocNode $phpDocNode, NameScope $nameScope $propertyName = substr($tagValue->propertyName, 1); $propertyType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); + $readableType = null; + if (array_key_exists($propertyName, $resolved)) { + $readableType = $resolved[$propertyName]->getReadableType(); + } + $resolved[$propertyName] = new PropertyTag( + $readableType ?? $propertyType, + $readableType, $propertyType, - false, - true, ); } } @@ -138,9 +161,29 @@ public function resolvePropertyTags(PhpDocNode $phpDocNode, NameScope $nameScope public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { $resolved = []; + $originalNameScope = $nameScope; - foreach (['@method', '@psalm-method', '@phpstan-method'] as $tagName) { + foreach (['@method', '@phan-method', '@psalm-method', '@phpstan-method'] as $tagName) { foreach ($phpDocNode->getMethodTagValues($tagName) as $tagValue) { + $nameScope = $originalNameScope; + $templateTags = []; + + if (count($tagValue->templateTypes) > 0 && $nameScope->getClassName() !== null) { + foreach ($tagValue->templateTypes as $templateType) { + $templateTags[$templateType->name] = new TemplateTag( + $templateType->name, + $templateType->bound !== null + ? $this->typeNodeResolver->resolve($templateType->bound, $nameScope) + : new MixedType(), + TemplateTypeVariance::createInvariant(), + ); + } + + $templateTypeScope = TemplateTypeScope::createWithMethod($nameScope->getClassName(), $tagValue->methodName); + $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags)); + $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap); + } + $parameters = []; foreach ($tagValue->parameters as $parameterNode) { $parameterName = substr($parameterNode->parameterName, 1); @@ -172,6 +215,7 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): : new MixedType(), $tagValue->isStatic, $parameters, + $templateTags, ); } } @@ -186,7 +230,7 @@ public function resolveExtendsTags(PhpDocNode $phpDocNode, NameScope $nameScope) { $resolved = []; - foreach (['@extends', '@template-extends', '@phpstan-extends'] as $tagName) { + foreach (['@extends', '@phan-extends', '@phan-inherits', '@template-extends', '@phpstan-extends'] as $tagName) { foreach ($phpDocNode->getExtendsTagValues($tagName) as $tagValue) { $resolved[$nameScope->resolveStringName($tagValue->type->type->name)] = new ExtendsTag( $this->typeNodeResolver->resolve($tagValue->type, $nameScope), @@ -243,8 +287,9 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope $prefixPriority = [ '' => 0, - 'psalm' => 1, - 'phpstan' => 2, + 'phan' => 1, + 'psalm' => 2, + 'phpstan' => 3, ]; foreach ($phpDocNode->getTags() as $phpDocTagNode) { @@ -254,17 +299,21 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope } $tagName = $phpDocTagNode->name; - if (in_array($tagName, ['@template', '@psalm-template', '@phpstan-template'], true)) { + if (in_array($tagName, ['@template', '@phan-template', '@psalm-template', '@phpstan-template'], true)) { $variance = TemplateTypeVariance::createInvariant(); } elseif (in_array($tagName, ['@template-covariant', '@psalm-template-covariant', '@phpstan-template-covariant'], true)) { $variance = TemplateTypeVariance::createCovariant(); + } elseif (in_array($tagName, ['@template-contravariant', '@psalm-template-contravariant', '@phpstan-template-contravariant'], true)) { + $variance = TemplateTypeVariance::createContravariant(); } else { continue; } - if (strpos($tagName, '@psalm-') === 0) { + if (str_starts_with($tagName, '@phan-')) { + $prefix = 'phan'; + } elseif (str_starts_with($tagName, '@psalm-')) { $prefix = 'psalm'; - } elseif (strpos($tagName, '@phpstan-') === 0) { + } elseif (str_starts_with($tagName, '@phpstan-')) { $prefix = 'phpstan'; } else { $prefix = ''; @@ -295,7 +344,7 @@ public function resolveParamTags(PhpDocNode $phpDocNode, NameScope $nameScope): { $resolved = []; - foreach (['@param', '@psalm-param', '@phpstan-param'] as $tagName) { + foreach (['@param', '@phan-param', '@psalm-param', '@phpstan-param'] as $tagName) { foreach ($phpDocNode->getParamTagValues($tagName) as $tagValue) { $parameterName = substr($tagValue->parameterName, 1); $parameterType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); @@ -313,11 +362,77 @@ public function resolveParamTags(PhpDocNode $phpDocNode, NameScope $nameScope): return $resolved; } + /** + * @return array + */ + public function resolveParamOutTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + if (!method_exists($phpDocNode, 'getParamOutTypeTagValues')) { + return []; + } + + $resolved = []; + + foreach (['@param-out', '@psalm-param-out', '@phpstan-param-out'] as $tagName) { + foreach ($phpDocNode->getParamOutTypeTagValues($tagName) as $tagValue) { + $parameterName = substr($tagValue->parameterName, 1); + $parameterType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); + if ($this->shouldSkipType($tagName, $parameterType)) { + continue; + } + + $resolved[$parameterName] = new ParamOutTag( + $parameterType, + ); + } + } + + return $resolved; + } + + /** + * @return array + */ + public function resolveParamImmediatelyInvokedCallable(PhpDocNode $phpDocNode): array + { + $parameters = []; + foreach (['@param-immediately-invoked-callable', '@phpstan-param-immediately-invoked-callable'] as $tagName) { + foreach ($phpDocNode->getParamImmediatelyInvokedCallableTagValues($tagName) as $tagValue) { + $parameterName = substr($tagValue->parameterName, 1); + $parameters[$parameterName] = true; + } + } + foreach (['@param-later-invoked-callable', '@phpstan-param-later-invoked-callable'] as $tagName) { + foreach ($phpDocNode->getParamLaterInvokedCallableTagValues($tagName) as $tagValue) { + $parameterName = substr($tagValue->parameterName, 1); + $parameters[$parameterName] = false; + } + } + + return $parameters; + } + + /** + * @return array + */ + public function resolveParamClosureThisTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + $closureThisTypes = []; + foreach (['@param-closure-this', '@phpstan-param-closure-this'] as $tagName) { + foreach ($phpDocNode->getParamClosureThisTagValues($tagName) as $tagValue) { + $parameterName = substr($tagValue->parameterName, 1); + $closureThisTypes[$parameterName] = new ParamClosureThisTag($this->typeNodeResolver->resolve($tagValue->type, $nameScope)); + } + } + + return $closureThisTypes; + } + public function resolveReturnTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?ReturnTag { $resolved = null; - foreach (['@return', '@psalm-return', '@phpstan-return'] as $tagName) { + foreach (['@return', '@phan-return', '@phan-real-return', '@psalm-return', '@phpstan-return'] as $tagName) { foreach ($phpDocNode->getReturnTagValues($tagName) as $tagValue) { $type = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); if ($this->shouldSkipType($tagName, $type)) { @@ -362,6 +477,42 @@ public function resolveMixinTags(PhpDocNode $phpDocNode, NameScope $nameScope): ), $phpDocNode->getMixinTagValues()); } + /** + * @return array + */ + public function resolveRequireExtendsTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + $resolved = []; + + foreach (['@psalm-require-extends', '@phpstan-require-extends'] as $tagName) { + foreach ($phpDocNode->getRequireExtendsTagValues($tagName) as $tagValue) { + $resolved[] = new RequireExtendsTag( + $this->typeNodeResolver->resolve($tagValue->type, $nameScope), + ); + } + } + + return $resolved; + } + + /** + * @return array + */ + public function resolveRequireImplementsTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + $resolved = []; + + foreach (['@psalm-require-implements', '@phpstan-require-implements'] as $tagName) { + foreach ($phpDocNode->getRequireImplementsTagValues($tagName) as $tagValue) { + $resolved[] = new RequireImplementsTag( + $this->typeNodeResolver->resolve($tagValue->type, $nameScope), + ); + } + } + + return $resolved; + } + /** * @return array */ @@ -369,7 +520,7 @@ public function resolveTypeAliasTags(PhpDocNode $phpDocNode, NameScope $nameScop { $resolved = []; - foreach (['@psalm-type', '@phpstan-type'] as $tagName) { + foreach (['@phan-type', '@psalm-type', '@phpstan-type'] as $tagName) { foreach ($phpDocNode->getTypeAliasTagValues($tagName) as $typeAliasTagValue) { $alias = $typeAliasTagValue->alias; $typeNode = $typeAliasTagValue->type; @@ -399,6 +550,71 @@ public function resolveTypeAliasImportTags(PhpDocNode $phpDocNode, NameScope $na return $resolved; } + /** + * @return AssertTag[] + */ + public function resolveAssertTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + foreach (['@phpstan', '@psalm', '@phan'] as $prefix) { + $resolved = array_merge( + $this->resolveAssertTagsFor($phpDocNode, $nameScope, $prefix . '-assert', AssertTag::NULL), + $this->resolveAssertTagsFor($phpDocNode, $nameScope, $prefix . '-assert-if-true', AssertTag::IF_TRUE), + $this->resolveAssertTagsFor($phpDocNode, $nameScope, $prefix . '-assert-if-false', AssertTag::IF_FALSE), + ); + + if (count($resolved) > 0) { + return $resolved; + } + } + + return []; + } + + /** + * @param AssertTag::NULL|AssertTag::IF_TRUE|AssertTag::IF_FALSE $if + * @return AssertTag[] + */ + private function resolveAssertTagsFor(PhpDocNode $phpDocNode, NameScope $nameScope, string $tagName, string $if): array + { + $resolved = []; + + foreach ($phpDocNode->getAssertTagValues($tagName) as $assertTagValue) { + $type = $this->typeNodeResolver->resolve($assertTagValue->type, $nameScope); + $parameter = new AssertTagParameter($assertTagValue->parameter, null, null); + $resolved[] = new AssertTag($if, $type, $parameter, $assertTagValue->isNegated, $assertTagValue->isEquality ?? false, true); + } + + foreach ($phpDocNode->getAssertPropertyTagValues($tagName) as $assertTagValue) { + $type = $this->typeNodeResolver->resolve($assertTagValue->type, $nameScope); + $parameter = new AssertTagParameter($assertTagValue->parameter, $assertTagValue->property, null); + $resolved[] = new AssertTag($if, $type, $parameter, $assertTagValue->isNegated, $assertTagValue->isEquality ?? false, true); + } + + foreach ($phpDocNode->getAssertMethodTagValues($tagName) as $assertTagValue) { + $type = $this->typeNodeResolver->resolve($assertTagValue->type, $nameScope); + $parameter = new AssertTagParameter($assertTagValue->parameter, null, $assertTagValue->method); + $resolved[] = new AssertTag($if, $type, $parameter, $assertTagValue->isNegated, $assertTagValue->isEquality ?? false, true); + } + + return $resolved; + } + + public function resolveSelfOutTypeTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?SelfOutTypeTag + { + if (!method_exists($phpDocNode, 'getSelfOutTypeTagValues')) { + return null; + } + + foreach (['@phpstan-this-out', '@phpstan-self-out', '@psalm-this-out', '@psalm-self-out'] as $tagName) { + foreach ($phpDocNode->getSelfOutTypeTagValues($tagName) as $selfOutTypeTagValue) { + $type = $this->typeNodeResolver->resolve($selfOutTypeTagValue->type, $nameScope); + return new SelfOutTypeTag($type); + } + } + + return null; + } + public function resolveDeprecatedTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?DeprecatedTag { foreach ($phpDocNode->getDeprecatedTagValues() as $deprecatedTagValue) { @@ -416,6 +632,13 @@ public function resolveIsDeprecated(PhpDocNode $phpDocNode): bool return count($deprecatedTags) > 0; } + public function resolveIsNotDeprecated(PhpDocNode $phpDocNode): bool + { + $notDeprecatedTags = $phpDocNode->getTagsByName('@not-deprecated'); + + return count($notDeprecatedTags) > 0; + } + public function resolveIsInternal(PhpDocNode $phpDocNode): bool { $internalTags = $phpDocNode->getTagsByName('@internal'); @@ -433,7 +656,7 @@ public function resolveIsFinal(PhpDocNode $phpDocNode): bool public function resolveIsPure(PhpDocNode $phpDocNode): bool { foreach ($phpDocNode->getTags() as $phpDocTagNode) { - if (in_array($phpDocTagNode->name, ['@pure', '@psalm-pure', '@phpstan-pure'], true)) { + if (in_array($phpDocTagNode->name, ['@pure', '@phan-pure', '@phan-side-effect-free', '@psalm-pure', '@phpstan-pure'], true)) { return true; } } @@ -454,7 +677,7 @@ public function resolveIsImpure(PhpDocNode $phpDocNode): bool public function resolveIsReadOnly(PhpDocNode $phpDocNode): bool { - foreach (['@readonly', '@psalm-readonly', '@phpstan-readonly', '@phpstan-readonly-allow-private-mutation', '@psalm-readonly-allow-private-mutation'] as $tagName) { + foreach (['@readonly', '@phan-read-only', '@psalm-readonly', '@phpstan-readonly', '@phpstan-readonly-allow-private-mutation', '@psalm-readonly-allow-private-mutation'] as $tagName) { $tags = $phpDocNode->getTagsByName($tagName); if (count($tags) > 0) { @@ -467,7 +690,7 @@ public function resolveIsReadOnly(PhpDocNode $phpDocNode): bool public function resolveIsImmutable(PhpDocNode $phpDocNode): bool { - foreach (['@immutable', '@psalm-immutable', '@phpstan-immutable'] as $tagName) { + foreach (['@immutable', '@phan-immutable', '@psalm-immutable', '@phpstan-immutable'] as $tagName) { $tags = $phpDocNode->getTagsByName($tagName); if (count($tags) > 0) { @@ -498,7 +721,7 @@ public function resolveAcceptsNamedArguments(PhpDocNode $phpDocNode): bool private function shouldSkipType(string $tagName, Type $type): bool { - if (strpos($tagName, '@psalm-') !== 0) { + if (!str_starts_with($tagName, '@psalm-')) { return false; } diff --git a/src/PhpDoc/PhpDocStringResolver.php b/src/PhpDoc/PhpDocStringResolver.php index 73b968826d..d9d6203e5b 100644 --- a/src/PhpDoc/PhpDocStringResolver.php +++ b/src/PhpDoc/PhpDocStringResolver.php @@ -18,7 +18,7 @@ public function resolve(string $phpDocString): PhpDocNode { $tokens = new TokenIterator($this->phpDocLexer->tokenize($phpDocString)); $phpDocNode = $this->phpDocParser->parse($tokens); - $tokens->consumeTokenType(Lexer::TOKEN_END); // @phpstan-ignore-line + $tokens->consumeTokenType(Lexer::TOKEN_END); // @phpstan-ignore missingType.checkedException return $phpDocNode; } diff --git a/src/PhpDoc/ReflectionEnumStubFilesExtension.php b/src/PhpDoc/ReflectionEnumStubFilesExtension.php new file mode 100644 index 0000000000..0fe8fbb5b2 --- /dev/null +++ b/src/PhpDoc/ReflectionEnumStubFilesExtension.php @@ -0,0 +1,23 @@ +phpVersion->supportsEnums()) { + return []; + } + + return [__DIR__ . '/../../stubs/ReflectionEnum.stub']; + } + +} diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index 2712ffc898..e3931a60ea 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -3,14 +3,20 @@ namespace PHPStan\PhpDoc; use PHPStan\Analyser\NameScope; +use PHPStan\PhpDoc\Tag\AssertTag; use PHPStan\PhpDoc\Tag\DeprecatedTag; use PHPStan\PhpDoc\Tag\ExtendsTag; use PHPStan\PhpDoc\Tag\ImplementsTag; use PHPStan\PhpDoc\Tag\MethodTag; use PHPStan\PhpDoc\Tag\MixinTag; +use PHPStan\PhpDoc\Tag\ParamClosureThisTag; +use PHPStan\PhpDoc\Tag\ParamOutTag; use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\PhpDoc\Tag\PropertyTag; +use PHPStan\PhpDoc\Tag\RequireExtendsTag; +use PHPStan\PhpDoc\Tag\RequireImplementsTag; use PHPStan\PhpDoc\Tag\ReturnTag; +use PHPStan\PhpDoc\Tag\SelfOutTypeTag; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDoc\Tag\ThrowsTag; use PHPStan\PhpDoc\Tag\TypeAliasImportTag; @@ -19,12 +25,17 @@ use PHPStan\PhpDoc\Tag\UsesTag; use PHPStan\PhpDoc\Tag\VarTag; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use function array_key_exists; +use function array_map; use function count; use function is_bool; use function substr; @@ -33,6 +44,8 @@ class ResolvedPhpDocBlock { + public const EMPTY_DOC_STRING = '/** */'; + private PhpDocNode $phpDocNode; /** @var PhpDocNode[] */ @@ -51,6 +64,8 @@ class ResolvedPhpDocBlock private PhpDocNodeResolver $phpDocNodeResolver; + private ReflectionProvider $reflectionProvider; + /** @var array<(string|int), VarTag>|false */ private array|false $varTags = false; @@ -72,6 +87,15 @@ class ResolvedPhpDocBlock /** @var array|false */ private array|false $paramTags = false; + /** @var array|false */ + private array|false $paramOutTags = false; + + /** @var array|false */ + private array|false $paramsImmediatelyInvokedCallable = false; + + /** @var array|false */ + private array|false $paramClosureThisTags = false; + private ReturnTag|false|null $returnTag = false; private ThrowsTag|false|null $throwsTag = false; @@ -79,16 +103,29 @@ class ResolvedPhpDocBlock /** @var array|false */ private array|false $mixinTags = false; + /** @var array|false */ + private array|false $requireExtendsTags = false; + + /** @var array|false */ + private array|false $requireImplementsTags = false; + /** @var array|false */ private array|false $typeAliasTags = false; /** @var array|false */ private array|false $typeAliasImportTags = false; + /** @var array|false */ + private array|false $assertTags = false; + + private SelfOutTypeTag|false|null $selfOutTypeTag = false; + private DeprecatedTag|false|null $deprecatedTag = false; private ?bool $isDeprecated = null; + private ?bool $isNotDeprecated = null; + private ?bool $isInternal = null; private ?bool $isFinal = null; @@ -121,6 +158,7 @@ public static function create( TemplateTypeMap $templateTypeMap, array $templateTags, PhpDocNodeResolver $phpDocNodeResolver, + ReflectionProvider $reflectionProvider, ): self { // new property also needs to be added to createEmpty() and merge() @@ -133,6 +171,7 @@ public static function create( $self->templateTypeMap = $templateTypeMap; $self->templateTags = $templateTags; $self->phpDocNodeResolver = $phpDocNodeResolver; + $self->reflectionProvider = $reflectionProvider; return $self; } @@ -141,7 +180,7 @@ public static function createEmpty(): self { // new property also needs to be added to merge() $self = new self(); - $self->phpDocString = '/** */'; + $self->phpDocString = self::EMPTY_DOC_STRING; $self->phpDocNodes = []; $self->filename = null; $self->templateTypeMap = TemplateTypeMap::createEmpty(); @@ -153,13 +192,21 @@ public static function createEmpty(): self $self->implementsTags = []; $self->usesTags = []; $self->paramTags = []; + $self->paramOutTags = []; + $self->paramsImmediatelyInvokedCallable = []; + $self->paramClosureThisTags = []; $self->returnTag = null; $self->throwsTag = null; $self->mixinTags = []; + $self->requireExtendsTags = []; + $self->requireImplementsTags = []; $self->typeAliasTags = []; $self->typeAliasImportTags = []; + $self->assertTags = []; + $self->selfOutTypeTag = null; $self->deprecatedTag = null; $self->isDeprecated = false; + $self->isNotDeprecated = false; $self->isInternal = false; $self->isFinal = false; $self->isPure = null; @@ -178,11 +225,15 @@ public static function createEmpty(): self */ public function merge(array $parents, array $parentPhpDocBlocks): self { + $className = $this->nameScope !== null ? $this->nameScope->getClassName() : null; + $classReflection = $className !== null && $this->reflectionProvider->hasClass($className) + ? $this->reflectionProvider->getClass($className) + : null; + // new property also needs to be added to createEmpty() $result = new self(); // we will resolve everything on $this here so these properties don't have to be populated // skip $result->phpDocNode - // skip $result->phpDocString - just for stubs $phpDocNodes = $this->phpDocNodes; $acceptsNamedArguments = $this->acceptsNamedArguments(); foreach ($parents as $parent) { @@ -192,6 +243,7 @@ public function merge(array $parents, array $parentPhpDocBlocks): self } } $result->phpDocNodes = $phpDocNodes; + $result->phpDocString = $this->phpDocString; $result->filename = $this->filename; // skip $result->nameScope $result->templateTypeMap = $this->templateTypeMap; @@ -204,16 +256,24 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $result->implementsTags = $this->getImplementsTags(); $result->usesTags = $this->getUsesTags(); $result->paramTags = self::mergeParamTags($this->getParamTags(), $parents, $parentPhpDocBlocks); - $result->returnTag = self::mergeReturnTags($this->getReturnTag(), $parents, $parentPhpDocBlocks); + $result->paramOutTags = self::mergeParamOutTags($this->getParamOutTags(), $parents, $parentPhpDocBlocks); + $result->paramsImmediatelyInvokedCallable = self::mergeParamsImmediatelyInvokedCallable($this->getParamsImmediatelyInvokedCallable(), $parents, $parentPhpDocBlocks); + $result->paramClosureThisTags = self::mergeParamClosureThisTags($this->getParamClosureThisTags(), $parents, $parentPhpDocBlocks); + $result->returnTag = self::mergeReturnTags($this->getReturnTag(), $classReflection, $parents, $parentPhpDocBlocks); $result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parents); $result->mixinTags = $this->getMixinTags(); + $result->requireExtendsTags = $this->getRequireExtendsTags(); + $result->requireImplementsTags = $this->getRequireImplementsTags(); $result->typeAliasTags = $this->getTypeAliasTags(); $result->typeAliasImportTags = $this->getTypeAliasImportTags(); - $result->deprecatedTag = self::mergeDeprecatedTags($this->getDeprecatedTag(), $parents); + $result->assertTags = self::mergeAssertTags($this->getAssertTags(), $parents, $parentPhpDocBlocks); + $result->selfOutTypeTag = self::mergeSelfOutTypeTags($this->getSelfOutTag(), $parents); + $result->deprecatedTag = self::mergeDeprecatedTags($this->getDeprecatedTag(), $this->isNotDeprecated(), $parents); $result->isDeprecated = $result->deprecatedTag !== null; + $result->isNotDeprecated = $this->isNotDeprecated(); $result->isInternal = $this->isInternal(); $result->isFinal = $this->isFinal(); - $result->isPure = $this->isPure(); + $result->isPure = self::mergePureTags($this->isPure(), $parents); $result->isReadOnly = $this->isReadOnly(); $result->isImmutable = $this->isImmutable(); $result->isAllowedPrivateMutation = $this->isAllowedPrivateMutation(); @@ -232,31 +292,73 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self return $this; } - $paramTags = $this->getParamTags(); + $mapParameterCb = static function (Type $type, callable $traverse) use ($parameterNameMapping): Type { + if ($type instanceof ConditionalTypeForParameter) { + $parameterName = substr($type->getParameterName(), 1); + if (array_key_exists($parameterName, $parameterNameMapping)) { + $type = $type->changeParameterName('$' . $parameterNameMapping[$parameterName]); + } + } + + return $traverse($type); + }; $newParamTags = []; - foreach ($paramTags as $key => $paramTag) { + foreach ($this->getParamTags() as $key => $paramTag) { if (!array_key_exists($key, $parameterNameMapping)) { continue; } - $newParamTags[$parameterNameMapping[$key]] = $paramTag; + $transformedType = TypeTraverser::map($paramTag->getType(), $mapParameterCb); + $newParamTags[$parameterNameMapping[$key]] = $paramTag->withType($transformedType); + } + + $newParamOutTags = []; + foreach ($this->getParamOutTags() as $key => $paramOutTag) { + if (!array_key_exists($key, $parameterNameMapping)) { + continue; + } + + $transformedType = TypeTraverser::map($paramOutTag->getType(), $mapParameterCb); + $newParamOutTags[$parameterNameMapping[$key]] = $paramOutTag->withType($transformedType); + } + + $newParamsImmediatelyInvokedCallable = []; + foreach ($this->getParamsImmediatelyInvokedCallable() as $key => $immediatelyInvokedCallable) { + if (!array_key_exists($key, $parameterNameMapping)) { + continue; + } + + $newParamsImmediatelyInvokedCallable[$parameterNameMapping[$key]] = $immediatelyInvokedCallable; + } + + $paramClosureThisTags = $this->getParamClosureThisTags(); + $newParamClosureThisTags = []; + foreach ($paramClosureThisTags as $key => $paramClosureThisTag) { + if (!array_key_exists($key, $parameterNameMapping)) { + continue; + } + + $transformedType = TypeTraverser::map($paramClosureThisTag->getType(), $mapParameterCb); + $newParamClosureThisTags[$parameterNameMapping[$key]] = $paramClosureThisTag->withType($transformedType); } $returnTag = $this->getReturnTag(); if ($returnTag !== null) { - $transformedType = TypeTraverser::map($returnTag->getType(), static function (Type $type, callable $traverse) use ($parameterNameMapping): Type { - if ($type instanceof ConditionalTypeForParameter) { - $parameterName = substr($type->getParameterName(), 1); - if (array_key_exists($parameterName, $parameterNameMapping)) { - $type = $type->changeParameterName('$' . $parameterNameMapping[$parameterName]); - } - } - - return $traverse($type); - }); + $transformedType = TypeTraverser::map($returnTag->getType(), $mapParameterCb); $returnTag = $returnTag->withType($transformedType); } + $assertTags = $this->getAssertTags(); + if (count($assertTags) > 0) { + $assertTags = array_map(static function (AssertTag $tag) use ($parameterNameMapping): AssertTag { + $parameterName = substr($tag->getParameter()->getParameterName(), 1); + if (array_key_exists($parameterName, $parameterNameMapping)) { + $tag = $tag->withParameter($tag->getParameter()->changeParameterName('$' . $parameterNameMapping[$parameterName])); + } + return $tag; + }, $assertTags); + } + $self = new self(); $self->phpDocNode = $this->phpDocNode; $self->phpDocNodes = $this->phpDocNodes; @@ -266,6 +368,7 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self $self->templateTypeMap = $this->templateTypeMap; $self->templateTags = $this->templateTags; $self->phpDocNodeResolver = $this->phpDocNodeResolver; + $self->reflectionProvider = $this->reflectionProvider; $self->varTags = $this->varTags; $self->methodTags = $this->methodTags; $self->propertyTags = $this->propertyTags; @@ -273,13 +376,21 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self $self->implementsTags = $this->implementsTags; $self->usesTags = $this->usesTags; $self->paramTags = $newParamTags; + $self->paramOutTags = $newParamOutTags; + $self->paramsImmediatelyInvokedCallable = $newParamsImmediatelyInvokedCallable; + $self->paramClosureThisTags = $newParamClosureThisTags; $self->returnTag = $returnTag; $self->throwsTag = $this->throwsTag; $self->mixinTags = $this->mixinTags; + $self->requireImplementsTags = $this->requireImplementsTags; + $self->requireExtendsTags = $this->requireExtendsTags; $self->typeAliasTags = $this->typeAliasTags; $self->typeAliasImportTags = $this->typeAliasImportTags; + $self->assertTags = $assertTags; + $self->selfOutTypeTag = $this->selfOutTypeTag; $self->deprecatedTag = $this->deprecatedTag; $self->isDeprecated = $this->isDeprecated; + $self->isNotDeprecated = $this->isNotDeprecated; $self->isInternal = $this->isInternal; $self->isFinal = $this->isFinal; $self->isPure = $this->isPure; @@ -287,6 +398,11 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self return $self; } + public function hasPhpDocString(): bool + { + return $this->phpDocString !== self::EMPTY_DOC_STRING; + } + public function getPhpDocString(): string { return $this->phpDocString; @@ -421,6 +537,47 @@ public function getParamTags(): array return $this->paramTags; } + /** + * @return array + */ + public function getParamOutTags(): array + { + if ($this->paramOutTags === false) { + $this->paramOutTags = $this->phpDocNodeResolver->resolveParamOutTags( + $this->phpDocNode, + $this->getNameScope(), + ); + } + return $this->paramOutTags; + } + + /** + * @return array + */ + public function getParamsImmediatelyInvokedCallable(): array + { + if ($this->paramsImmediatelyInvokedCallable === false) { + $this->paramsImmediatelyInvokedCallable = $this->phpDocNodeResolver->resolveParamImmediatelyInvokedCallable($this->phpDocNode); + } + + return $this->paramsImmediatelyInvokedCallable; + } + + /** + * @return array + */ + public function getParamClosureThisTags(): array + { + if ($this->paramClosureThisTags === false) { + $this->paramClosureThisTags = $this->phpDocNodeResolver->resolveParamClosureThisTags( + $this->phpDocNode, + $this->getNameScope(), + ); + } + + return $this->paramClosureThisTags; + } + public function getReturnTag(): ?ReturnTag { if (is_bool($this->returnTag)) { @@ -458,6 +615,36 @@ public function getMixinTags(): array return $this->mixinTags; } + /** + * @return array + */ + public function getRequireExtendsTags(): array + { + if ($this->requireExtendsTags === false) { + $this->requireExtendsTags = $this->phpDocNodeResolver->resolveRequireExtendsTags( + $this->phpDocNode, + $this->getNameScope(), + ); + } + + return $this->requireExtendsTags; + } + + /** + * @return array + */ + public function getRequireImplementsTags(): array + { + if ($this->requireImplementsTags === false) { + $this->requireImplementsTags = $this->phpDocNodeResolver->resolveRequireImplementsTags( + $this->phpDocNode, + $this->getNameScope(), + ); + } + + return $this->requireImplementsTags; + } + /** * @return array */ @@ -488,6 +675,33 @@ public function getTypeAliasImportTags(): array return $this->typeAliasImportTags; } + /** + * @return array + */ + public function getAssertTags(): array + { + if ($this->assertTags === false) { + $this->assertTags = $this->phpDocNodeResolver->resolveAssertTags( + $this->phpDocNode, + $this->getNameScope(), + ); + } + + return $this->assertTags; + } + + public function getSelfOutTag(): ?SelfOutTypeTag + { + if ($this->selfOutTypeTag === false) { + $this->selfOutTypeTag = $this->phpDocNodeResolver->resolveSelfOutTypeTag( + $this->phpDocNode, + $this->getNameScope(), + ); + } + + return $this->selfOutTypeTag; + } + public function getDeprecatedTag(): ?DeprecatedTag { if (is_bool($this->deprecatedTag)) { @@ -509,6 +723,19 @@ public function isDeprecated(): bool return $this->isDeprecated; } + /** + * @internal + */ + public function isNotDeprecated(): bool + { + if ($this->isNotDeprecated === null) { + $this->isNotDeprecated = $this->phpDocNodeResolver->resolveIsNotDeprecated( + $this->phpDocNode, + ); + } + return $this->isNotDeprecated; + } + public function isInternal(): bool { if ($this->isInternal === null) { @@ -563,14 +790,14 @@ public function isPure(): ?bool if ($pure) { $this->isPure = true; return $this->isPure; - } else { - $impure = $this->phpDocNodeResolver->resolveIsImpure( - $this->phpDocNode, - ); - if ($impure) { - $this->isPure = false; - return $this->isPure; - } + } + + $impure = $this->phpDocNodeResolver->resolveIsImpure( + $this->phpDocNode, + ); + if ($impure) { + $this->isPure = false; + return $this->isPure; } $this->isPure = null; @@ -636,13 +863,12 @@ private static function mergeVarTags(array $varTags, array $parents, array $pare } /** - * @param ResolvedPhpDocBlock $parent * @return array|null */ private static function mergeOneParentVarTags(self $parent, PhpDocBlock $phpDocBlock): ?array { foreach ($parent->getVarTags() as $key => $parentVarTag) { - return [$key => self::resolveTemplateTypeInTag($parentVarTag, $phpDocBlock)]; + return [$key => self::resolveTemplateTypeInTag($parentVarTag, $phpDocBlock, TemplateTypeVariance::createInvariant())]; } return null; @@ -665,7 +891,6 @@ private static function mergeParamTags(array $paramTags, array $parents, array $ /** * @param array $paramTags - * @param ResolvedPhpDocBlock $parent * @return array */ private static function mergeOneParentParamTags(array $paramTags, self $parent, PhpDocBlock $phpDocBlock): array @@ -677,7 +902,11 @@ private static function mergeOneParentParamTags(array $paramTags, self $parent, continue; } - $paramTags[$name] = self::resolveTemplateTypeInTag($parentParamTag, $phpDocBlock); + $paramTags[$name] = self::resolveTemplateTypeInTag( + $parentParamTag->withType($phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentParamTag->getType())), + $phpDocBlock, + TemplateTypeVariance::createContravariant(), + ); } return $paramTags; @@ -688,14 +917,14 @@ private static function mergeOneParentParamTags(array $paramTags, self $parent, * @param array $parentPhpDocBlocks * @return ReturnTag|Null */ - private static function mergeReturnTags(?ReturnTag $returnTag, array $parents, array $parentPhpDocBlocks): ?ReturnTag + private static function mergeReturnTags(?ReturnTag $returnTag, ?ClassReflection $classReflection, array $parents, array $parentPhpDocBlocks): ?ReturnTag { if ($returnTag !== null) { return $returnTag; } foreach ($parents as $i => $parent) { - $result = self::mergeOneParentReturnTag($returnTag, $parent, $parentPhpDocBlocks[$i]); + $result = self::mergeOneParentReturnTag($returnTag, $classReflection, $parent, $parentPhpDocBlocks[$i]); if ($result === null) { continue; } @@ -706,7 +935,7 @@ private static function mergeReturnTags(?ReturnTag $returnTag, array $parents, a return null; } - private static function mergeOneParentReturnTag(?ReturnTag $returnTag, self $parent, PhpDocBlock $phpDocBlock): ?ReturnTag + private static function mergeOneParentReturnTag(?ReturnTag $returnTag, ?ClassReflection $classReflection, self $parent, PhpDocBlock $phpDocBlock): ?ReturnTag { $parentReturnTag = $parent->getReturnTag(); if ($parentReturnTag === null) { @@ -715,6 +944,21 @@ private static function mergeOneParentReturnTag(?ReturnTag $returnTag, self $par $parentType = $parentReturnTag->getType(); + if ($classReflection !== null) { + $parentType = TypeTraverser::map( + $parentType, + static function (Type $type, callable $traverse) use ($classReflection): Type { + if ($type instanceof StaticType) { + return $type->changeBaseClass($classReflection); + } + + return $traverse($type); + }, + ); + + $parentReturnTag = $parentReturnTag->withType($parentType); + } + // Each parent would overwrite the previous one except if it returns a less specific type. // Do not care for incompatible types as there is a separate rule for that. if ($returnTag !== null && $parentType->isSuperTypeOf($returnTag->getType())->yes()) { @@ -726,20 +970,79 @@ private static function mergeOneParentReturnTag(?ReturnTag $returnTag, self $par $phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentReturnTag->getType()), )->toImplicit(), $phpDocBlock, + TemplateTypeVariance::createCovariant(), ); } + /** + * @param array $assertTags + * @param array $parents + * @param array $parentPhpDocBlocks + * @return array + */ + private static function mergeAssertTags(array $assertTags, array $parents, array $parentPhpDocBlocks): array + { + if (count($assertTags) > 0) { + return $assertTags; + } + foreach ($parents as $i => $parent) { + $result = $parent->getAssertTags(); + if (count($result) === 0) { + continue; + } + + $phpDocBlock = $parentPhpDocBlocks[$i]; + + return array_map( + static fn (AssertTag $assertTag) => self::resolveTemplateTypeInTag( + $assertTag->withParameter( + $phpDocBlock->transformAssertTagParameterWithParameterNameMapping($assertTag->getParameter()), + )->toImplicit(), + $phpDocBlock, + TemplateTypeVariance::createCovariant(), + ), + $result, + ); + } + + return $assertTags; + } + + /** + * @param array $parents + */ + private static function mergeSelfOutTypeTags(?SelfOutTypeTag $selfOutTypeTag, array $parents): ?SelfOutTypeTag + { + if ($selfOutTypeTag !== null) { + return $selfOutTypeTag; + } + foreach ($parents as $parent) { + $result = $parent->getSelfOutTag(); + if ($result === null) { + continue; + } + return $result; + } + + return null; + } + /** * @param array $parents */ - private static function mergeDeprecatedTags(?DeprecatedTag $deprecatedTag, array $parents): ?DeprecatedTag + private static function mergeDeprecatedTags(?DeprecatedTag $deprecatedTag, bool $hasNotDeprecatedTag, array $parents): ?DeprecatedTag { if ($deprecatedTag !== null) { return $deprecatedTag; } + + if ($hasNotDeprecatedTag) { + return null; + } + foreach ($parents as $parent) { $result = $parent->getDeprecatedTag(); - if ($result === null) { + if ($result === null && !$parent->isNotDeprecated()) { continue; } return $result; @@ -768,16 +1071,155 @@ private static function mergeThrowsTags(?ThrowsTag $throwsTag, array $parents): return null; } + /** + * @param array $paramOutTags + * @param array $parents + * @param array $parentPhpDocBlocks + * @return array + */ + private static function mergeParamOutTags(array $paramOutTags, array $parents, array $parentPhpDocBlocks): array + { + foreach ($parents as $i => $parent) { + $paramOutTags = self::mergeOneParentParamOutTags($paramOutTags, $parent, $parentPhpDocBlocks[$i]); + } + + return $paramOutTags; + } + + /** + * @param array $paramOutTags + * @return array + */ + private static function mergeOneParentParamOutTags(array $paramOutTags, self $parent, PhpDocBlock $phpDocBlock): array + { + $parentParamOutTags = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamOutTags()); + + foreach ($parentParamOutTags as $name => $parentParamTag) { + if (array_key_exists($name, $paramOutTags)) { + continue; + } + + $paramOutTags[$name] = self::resolveTemplateTypeInTag( + $parentParamTag->withType($phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentParamTag->getType())), + $phpDocBlock, + TemplateTypeVariance::createCovariant(), + ); + } + + return $paramOutTags; + } + + /** + * @param array $paramsImmediatelyInvokedCallable + * @param array $parents + * @param array $parentPhpDocBlocks + * @return array + */ + private static function mergeParamsImmediatelyInvokedCallable(array $paramsImmediatelyInvokedCallable, array $parents, array $parentPhpDocBlocks): array + { + foreach ($parents as $i => $parent) { + $paramsImmediatelyInvokedCallable = self::mergeOneParentParamImmediatelyInvokedCallable($paramsImmediatelyInvokedCallable, $parent, $parentPhpDocBlocks[$i]); + } + + return $paramsImmediatelyInvokedCallable; + } + + /** + * @param array $paramsImmediatelyInvokedCallable + * @return array + */ + private static function mergeOneParentParamImmediatelyInvokedCallable(array $paramsImmediatelyInvokedCallable, self $parent, PhpDocBlock $phpDocBlock): array + { + $parentImmediatelyInvokedCallable = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamsImmediatelyInvokedCallable()); + + foreach ($parentImmediatelyInvokedCallable as $name => $parentIsImmediatelyInvokedCallable) { + if (array_key_exists($name, $paramsImmediatelyInvokedCallable)) { + continue; + } + + $paramsImmediatelyInvokedCallable[$name] = $parentIsImmediatelyInvokedCallable; + } + + return $paramsImmediatelyInvokedCallable; + } + + /** + * @param array $paramsClosureThisTags + * @param array $parents + * @param array $parentPhpDocBlocks + * @return array + */ + private static function mergeParamClosureThisTags(array $paramsClosureThisTags, array $parents, array $parentPhpDocBlocks): array + { + foreach ($parents as $i => $parent) { + $paramsClosureThisTags = self::mergeOneParentParamClosureThisTag($paramsClosureThisTags, $parent, $parentPhpDocBlocks[$i]); + } + + return $paramsClosureThisTags; + } + + /** + * @param array $paramsClosureThisTags + * @return array + */ + private static function mergeOneParentParamClosureThisTag(array $paramsClosureThisTags, self $parent, PhpDocBlock $phpDocBlock): array + { + $parentClosureThisTags = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamClosureThisTags()); + + foreach ($parentClosureThisTags as $name => $parentParamClosureThisTag) { + if (array_key_exists($name, $paramsClosureThisTags)) { + continue; + } + + $paramsClosureThisTags[$name] = self::resolveTemplateTypeInTag( + $parentParamClosureThisTag->withType( + $phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentParamClosureThisTag->getType()), + ), + $phpDocBlock, + TemplateTypeVariance::createContravariant(), + ); + } + + return $paramsClosureThisTags; + } + + /** + * @param array $parents + */ + private static function mergePureTags(?bool $isPure, array $parents): ?bool + { + if ($isPure !== null) { + return $isPure; + } + + foreach ($parents as $parent) { + $parentIsPure = $parent->isPure(); + if ($parentIsPure === null) { + continue; + } + + return $parentIsPure; + } + + return null; + } + /** * @template T of TypedTag * @param T $tag * @return T */ - private static function resolveTemplateTypeInTag(TypedTag $tag, PhpDocBlock $phpDocBlock): TypedTag + private static function resolveTemplateTypeInTag( + TypedTag $tag, + PhpDocBlock $phpDocBlock, + TemplateTypeVariance $positionVariance, + ): TypedTag { $type = TemplateTypeHelper::resolveTemplateTypes( $tag->getType(), $phpDocBlock->getClassReflection()->getActiveTemplateTypeMap(), + $phpDocBlock->getClassReflection()->getCallSiteVarianceMap(), + $positionVariance, ); return $tag->withType($type); } diff --git a/src/PhpDoc/SocketSelectStubFilesExtension.php b/src/PhpDoc/SocketSelectStubFilesExtension.php new file mode 100644 index 0000000000..1113a10f89 --- /dev/null +++ b/src/PhpDoc/SocketSelectStubFilesExtension.php @@ -0,0 +1,23 @@ +phpVersion->getVersionId() >= 80000) { + return [__DIR__ . '/../../stubs/socket_select_php8.stub']; + } + + return [__DIR__ . '/../../stubs/socket_select.stub']; + } + +} diff --git a/src/PhpDoc/StubFilesExtension.php b/src/PhpDoc/StubFilesExtension.php index 237f2a04d3..91267fa583 100644 --- a/src/PhpDoc/StubFilesExtension.php +++ b/src/PhpDoc/StubFilesExtension.php @@ -2,7 +2,22 @@ namespace PHPStan\PhpDoc; -/** @api */ +/** + * This is the extension interface to implement if you want to dynamically + * load stub files based on your logic. As opposed to simply list them in the configuration file. + * + * To register it in the configuration file use the `phpstan.stubFilesExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.stubFilesExtension + * ``` + * + * @api + */ interface StubFilesExtension { diff --git a/src/PhpDoc/StubSourceLocatorFactory.php b/src/PhpDoc/StubSourceLocatorFactory.php index 597b761166..27384f41b1 100644 --- a/src/PhpDoc/StubSourceLocatorFactory.php +++ b/src/PhpDoc/StubSourceLocatorFactory.php @@ -6,10 +6,13 @@ use PHPStan\BetterReflection\SourceLocator\Ast\Locator; use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; use PHPStan\BetterReflection\SourceLocator\Type\AggregateSourceLocator; +use PHPStan\BetterReflection\SourceLocator\Type\Composer\Psr\Psr4Mapping; use PHPStan\BetterReflection\SourceLocator\Type\MemoizingSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\PhpInternalSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedPsrAutoloaderLocatorFactory; use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository; +use function dirname; class StubSourceLocatorFactory { @@ -18,6 +21,7 @@ public function __construct( private Parser $php8Parser, private PhpStormStubsSourceStubber $phpStormStubsSourceStubber, private OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository, + private OptimizedPsrAutoloaderLocatorFactory $optimizedPsrAutoloaderLocatorFactory, private StubFilesProvider $stubFilesProvider, ) { @@ -31,6 +35,17 @@ public function create(): SourceLocator $locators[] = $this->optimizedSingleFileSourceLocatorRepository->getOrCreate($stubFile); } + $locators[] = $this->optimizedPsrAutoloaderLocatorFactory->create( + Psr4Mapping::fromArrayMappings([ + 'PHPStan\\' => [dirname(__DIR__) . '/'], + ]), + ); + $locators[] = $this->optimizedPsrAutoloaderLocatorFactory->create( + Psr4Mapping::fromArrayMappings([ + 'PhpParser\\' => [dirname(__DIR__, 2) . '/vendor/nikic/php-parser/lib/PhpParser/'], + ]), + ); + $locators[] = new PhpInternalSourceLocator($astPhp8Locator, $this->phpStormStubsSourceStubber); return new MemoizingSourceLocator(new AggregateSourceLocator($locators)); diff --git a/src/PhpDoc/StubValidator.php b/src/PhpDoc/StubValidator.php index 4b2c484ef9..137a825b06 100644 --- a/src/PhpDoc/StubValidator.php +++ b/src/PhpDoc/StubValidator.php @@ -12,15 +12,23 @@ use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\PhpDocParser; +use PHPStan\Reflection\Php\PhpClassReflectionExtension; +use PHPStan\Reflection\PhpVersionStaticAccessor; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ReflectionProviderStaticAccessor; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\Classes\DuplicateClassDeclarationRule; +use PHPStan\Rules\Classes\DuplicateDeclarationRule; use PHPStan\Rules\Classes\ExistingClassesInClassImplementsRule; use PHPStan\Rules\Classes\ExistingClassesInInterfaceExtendsRule; use PHPStan\Rules\Classes\ExistingClassInClassExtendsRule; use PHPStan\Rules\Classes\ExistingClassInTraitUseRule; +use PHPStan\Rules\Classes\LocalTypeAliasesCheck; +use PHPStan\Rules\Classes\LocalTypeAliasesRule; +use PHPStan\Rules\Classes\LocalTypeTraitAliasesRule; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\DirectRegistry as DirectRuleRegistry; use PHPStan\Rules\FunctionDefinitionCheck; +use PHPStan\Rules\Functions\DuplicateFunctionDeclarationRule; use PHPStan\Rules\Functions\MissingFunctionParameterTypehintRule; use PHPStan\Rules\Functions\MissingFunctionReturnTypehintRule; use PHPStan\Rules\Generics\ClassAncestorsRule; @@ -44,9 +52,11 @@ use PHPStan\Rules\Methods\MissingMethodReturnTypehintRule; use PHPStan\Rules\Methods\OverridingMethodRule; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\PhpDoc\GenericCallableRuleHelper; use PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule; use PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule; use PHPStan\Rules\PhpDoc\InvalidPhpDocTagValueRule; +use PHPStan\Rules\PhpDoc\InvalidPHPStanDocTagRule; use PHPStan\Rules\PhpDoc\InvalidThrowsPhpDocValueRule; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Properties\ExistingClassesInPropertiesRule; @@ -64,13 +74,14 @@ class StubValidator public function __construct( private DerivativeContainerFactory $derivativeContainerFactory, + private bool $duplicateStubs, ) { } /** * @param string[] $stubFiles - * @return Error[] + * @return list */ public function validate(array $stubFiles, bool $debug): array { @@ -80,6 +91,7 @@ public function validate(array $stubFiles, bool $debug): array $originalBroker = Broker::getInstance(); $originalReflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + $originalPhpVerison = PhpVersionStaticAccessor::getInstance(); $container = $this->derivativeContainerFactory->create([ __DIR__ . '/../../conf/config.stubValidator.neon', ]); @@ -87,10 +99,8 @@ public function validate(array $stubFiles, bool $debug): array $ruleRegistry = $this->getRuleRegistry($container); $collectorRegistry = $this->getCollectorRegistry($container); - /** @var FileAnalyser $fileAnalyser */ $fileAnalyser = $container->getByType(FileAnalyser::class); - /** @var NodeScopeResolver $nodeScopeResolver */ $nodeScopeResolver = $container->getByType(NodeScopeResolver::class); $nodeScopeResolver->setAnalysedFiles($stubFiles); @@ -119,12 +129,13 @@ static function (): void { } $internalErrorMessage = sprintf('Internal error: %s', $e->getMessage()); - $errors[] = new Error($internalErrorMessage, $stubFile, null, $e); + $errors[] = (new Error($internalErrorMessage, $stubFile, null, $e))->withIdentifier('phpstan.internal'); } } Broker::registerInstance($originalBroker); ReflectionProviderStaticAccessor::registerInstance($originalReflectionProvider); + PhpVersionStaticAccessor::registerInstance($originalPhpVerison); ObjectType::resetCaches(); return $errors; @@ -138,23 +149,29 @@ private function getRuleRegistry(Container $container): RuleRegistry $templateTypeCheck = $container->getByType(TemplateTypeCheck::class); $varianceCheck = $container->getByType(VarianceCheck::class); $reflectionProvider = $container->getByType(ReflectionProvider::class); - $classCaseSensitivityCheck = $container->getByType(ClassCaseSensitivityCheck::class); + $classNameCheck = $container->getByType(ClassNameCheck::class); $functionDefinitionCheck = $container->getByType(FunctionDefinitionCheck::class); $missingTypehintCheck = $container->getByType(MissingTypehintCheck::class); $unresolvableTypeHelper = $container->getByType(UnresolvableTypeHelper::class); $crossCheckInterfacesHelper = $container->getByType(CrossCheckInterfacesHelper::class); $phpVersion = $container->getByType(PhpVersion::class); + $localTypeAliasesCheck = $container->getByType(LocalTypeAliasesCheck::class); + $phpClassReflectionExtension = $container->getByType(PhpClassReflectionExtension::class); + $genericCallableRuleHelper = $container->getByType(GenericCallableRuleHelper::class); $rules = [ // level 0 - new ExistingClassesInClassImplementsRule($classCaseSensitivityCheck, $reflectionProvider), - new ExistingClassesInInterfaceExtendsRule($classCaseSensitivityCheck, $reflectionProvider), - new ExistingClassInClassExtendsRule($classCaseSensitivityCheck, $reflectionProvider), - new ExistingClassInTraitUseRule($classCaseSensitivityCheck, $reflectionProvider), + new ExistingClassesInClassImplementsRule($classNameCheck, $reflectionProvider), + new ExistingClassesInInterfaceExtendsRule($classNameCheck, $reflectionProvider), + new ExistingClassInClassExtendsRule($classNameCheck, $reflectionProvider), + new ExistingClassInTraitUseRule($classNameCheck, $reflectionProvider), new ExistingClassesInTypehintsRule($functionDefinitionCheck), new \PHPStan\Rules\Functions\ExistingClassesInTypehintsRule($functionDefinitionCheck), - new ExistingClassesInPropertiesRule($reflectionProvider, $classCaseSensitivityCheck, $unresolvableTypeHelper, $phpVersion, true, false), - new OverridingMethodRule($phpVersion, new MethodSignatureRule(true, true), true, new MethodParameterComparisonHelper($phpVersion)), + new ExistingClassesInPropertiesRule($reflectionProvider, $classNameCheck, $unresolvableTypeHelper, $phpVersion, true, false), + new OverridingMethodRule($phpVersion, new MethodSignatureRule($phpClassReflectionExtension, true, true, $container->getParameter('featureToggles')['abstractTraitMethod']), true, new MethodParameterComparisonHelper($phpVersion, $container->getParameter('featureToggles')['genericPrototypeMessage']), $phpClassReflectionExtension, $container->getParameter('featureToggles')['genericPrototypeMessage'], $container->getParameter('featureToggles')['finalByPhpDoc'], $container->getParameter('checkMissingOverrideMethodAttribute')), + new DuplicateDeclarationRule(), + new LocalTypeAliasesRule($localTypeAliasesCheck), + new LocalTypeTraitAliasesRule($localTypeAliasesCheck, $reflectionProvider), // level 2 new ClassAncestorsRule($genericAncestorsCheck, $crossCheckInterfacesHelper), @@ -166,26 +183,39 @@ private function getRuleRegistry(Container $container): RuleRegistry new MethodTemplateTypeRule($fileTypeMapper, $templateTypeCheck), new MethodSignatureVarianceRule($varianceCheck), new TraitTemplateTypeRule($fileTypeMapper, $templateTypeCheck), - new IncompatiblePhpDocTypeRule( - $fileTypeMapper, - $genericObjectTypeCheck, - $unresolvableTypeHelper, - ), - new IncompatiblePropertyPhpDocTypeRule($genericObjectTypeCheck, $unresolvableTypeHelper), + new IncompatiblePhpDocTypeRule($fileTypeMapper, $genericObjectTypeCheck, $unresolvableTypeHelper, $genericCallableRuleHelper), + new IncompatiblePropertyPhpDocTypeRule($genericObjectTypeCheck, $unresolvableTypeHelper, $genericCallableRuleHelper), new InvalidPhpDocTagValueRule( $container->getByType(Lexer::class), $container->getByType(PhpDocParser::class), + $container->getParameter('featureToggles')['allInvalidPhpDocs'], + $container->getParameter('featureToggles')['invalidPhpDocTagLine'], ), new InvalidThrowsPhpDocValueRule($fileTypeMapper), // level 6 - new MissingFunctionParameterTypehintRule($missingTypehintCheck), + new MissingFunctionParameterTypehintRule($missingTypehintCheck, $container->getParameter('featureToggles')['paramOutType']), new MissingFunctionReturnTypehintRule($missingTypehintCheck), - new MissingMethodParameterTypehintRule($missingTypehintCheck), + new MissingMethodParameterTypehintRule($missingTypehintCheck, $container->getParameter('featureToggles')['paramOutType']), new MissingMethodReturnTypehintRule($missingTypehintCheck), new MissingPropertyTypehintRule($missingTypehintCheck), ]; + if ($this->duplicateStubs) { + $reflector = $container->getService('stubReflector'); + $relativePathHelper = $container->getService('simpleRelativePathHelper'); + $rules[] = new DuplicateClassDeclarationRule($reflector, $relativePathHelper); + $rules[] = new DuplicateFunctionDeclarationRule($reflector, $relativePathHelper); + } + + if ((bool) $container->getParameter('featureToggles')['allInvalidPhpDocs']) { + $rules[] = new InvalidPHPStanDocTagRule( + $container->getByType(Lexer::class), + $container->getByType(PhpDocParser::class), + true, + ); + } + return new DirectRuleRegistry($rules); } diff --git a/src/PhpDoc/Tag/AssertTag.php b/src/PhpDoc/Tag/AssertTag.php new file mode 100644 index 0000000000..301b5b4876 --- /dev/null +++ b/src/PhpDoc/Tag/AssertTag.php @@ -0,0 +1,96 @@ +if; + } + + public function getType(): Type + { + return $this->type; + } + + public function getOriginalType(): Type + { + return $this->originalType ??= $this->type; + } + + public function getParameter(): AssertTagParameter + { + return $this->parameter; + } + + public function isNegated(): bool + { + return $this->negated; + } + + public function isEquality(): bool + { + return $this->equality; + } + + /** + * @return static + */ + public function withType(Type $type): TypedTag + { + $tag = new self($this->if, $type, $this->parameter, $this->negated, $this->equality, $this->isExplicit); + $tag->originalType = $this->getOriginalType(); + return $tag; + } + + public function withParameter(AssertTagParameter $parameter): self + { + $tag = new self($this->if, $this->type, $parameter, $this->negated, $this->equality, $this->isExplicit); + $tag->originalType = $this->getOriginalType(); + return $tag; + } + + public function negate(): self + { + if ($this->isEquality()) { + throw new ShouldNotHappenException(); + } + + $tag = new self($this->if, $this->type, $this->parameter, !$this->negated, $this->equality, $this->isExplicit); + $tag->originalType = $this->getOriginalType(); + return $tag; + } + + public function isExplicit(): bool + { + return $this->isExplicit; + } + + public function toImplicit(): self + { + return new self($this->if, $this->type, $this->parameter, $this->negated, $this->equality, false); + } + +} diff --git a/src/PhpDoc/Tag/AssertTagParameter.php b/src/PhpDoc/Tag/AssertTagParameter.php new file mode 100644 index 0000000000..18717b3494 --- /dev/null +++ b/src/PhpDoc/Tag/AssertTagParameter.php @@ -0,0 +1,59 @@ +parameterName; + } + + public function changeParameterName(string $parameterName): self + { + return new self( + $parameterName, + $this->property, + $this->method, + ); + } + + public function describe(): string + { + if ($this->property !== null) { + return sprintf('%s->%s', $this->parameterName, $this->property); + } + + if ($this->method !== null) { + return sprintf('%s->%s()', $this->parameterName, $this->method); + } + + return $this->parameterName; + } + + public function getExpr(Expr $parameter): Expr + { + if ($this->property !== null) { + return new Expr\PropertyFetch($parameter, $this->property); + } + + if ($this->method !== null) { + return new Expr\MethodCall($parameter, $this->method); + } + + return $parameter; + } + +} diff --git a/src/PhpDoc/Tag/MethodTag.php b/src/PhpDoc/Tag/MethodTag.php index e640418ea8..9f46c124d2 100644 --- a/src/PhpDoc/Tag/MethodTag.php +++ b/src/PhpDoc/Tag/MethodTag.php @@ -10,11 +10,13 @@ class MethodTag /** * @param array $parameters + * @param array $templateTags */ public function __construct( private Type $returnType, private bool $isStatic, private array $parameters, + private array $templateTags = [], ) { } @@ -37,4 +39,12 @@ public function getParameters(): array return $this->parameters; } + /** + * @return array + */ + public function getTemplateTags(): array + { + return $this->templateTags; + } + } diff --git a/src/PhpDoc/Tag/ParamClosureThisTag.php b/src/PhpDoc/Tag/ParamClosureThisTag.php new file mode 100644 index 0000000000..1dacb4c41d --- /dev/null +++ b/src/PhpDoc/Tag/ParamClosureThisTag.php @@ -0,0 +1,30 @@ +type; + } + + /** + * @return self + */ + public function withType(Type $type): TypedTag + { + return new self($type); + } + +} diff --git a/src/PhpDoc/Tag/ParamOutTag.php b/src/PhpDoc/Tag/ParamOutTag.php new file mode 100644 index 0000000000..c40018cb45 --- /dev/null +++ b/src/PhpDoc/Tag/ParamOutTag.php @@ -0,0 +1,28 @@ +type; + } + + /** + * @return self + */ + public function withType(Type $type): TypedTag + { + return new self($type); + } + +} diff --git a/src/PhpDoc/Tag/ParamTag.php b/src/PhpDoc/Tag/ParamTag.php index 651064cec1..8515df30b9 100644 --- a/src/PhpDoc/Tag/ParamTag.php +++ b/src/PhpDoc/Tag/ParamTag.php @@ -8,7 +8,10 @@ class ParamTag implements TypedTag { - public function __construct(private Type $type, private bool $isVariadic) + public function __construct( + private Type $type, + private bool $isVariadic, + ) { } diff --git a/src/PhpDoc/Tag/PropertyTag.php b/src/PhpDoc/Tag/PropertyTag.php index b204ce4bb3..f2e42c14b3 100644 --- a/src/PhpDoc/Tag/PropertyTag.php +++ b/src/PhpDoc/Tag/PropertyTag.php @@ -10,25 +10,44 @@ class PropertyTag public function __construct( private Type $type, - private bool $readable, - private bool $writable, + private ?Type $readableType, + private ?Type $writableType, ) { } + /** + * @deprecated Use getReadableType() / getWritableType() + */ public function getType(): Type { return $this->type; } + public function getReadableType(): ?Type + { + return $this->readableType; + } + + public function getWritableType(): ?Type + { + return $this->writableType; + } + + /** + * @phpstan-assert-if-true !null $this->getReadableType() + */ public function isReadable(): bool { - return $this->readable; + return $this->readableType !== null; } + /** + * @phpstan-assert-if-true !null $this->getWritableType() + */ public function isWritable(): bool { - return $this->writable; + return $this->writableType !== null; } } diff --git a/src/PhpDoc/Tag/RequireExtendsTag.php b/src/PhpDoc/Tag/RequireExtendsTag.php new file mode 100644 index 0000000000..a1e60d45a6 --- /dev/null +++ b/src/PhpDoc/Tag/RequireExtendsTag.php @@ -0,0 +1,20 @@ +type; + } + +} diff --git a/src/PhpDoc/Tag/RequireImplementsTag.php b/src/PhpDoc/Tag/RequireImplementsTag.php new file mode 100644 index 0000000000..2a0f42303f --- /dev/null +++ b/src/PhpDoc/Tag/RequireImplementsTag.php @@ -0,0 +1,20 @@ +type; + } + +} diff --git a/src/PhpDoc/Tag/SelfOutTypeTag.php b/src/PhpDoc/Tag/SelfOutTypeTag.php new file mode 100644 index 0000000000..bbd5a2ca09 --- /dev/null +++ b/src/PhpDoc/Tag/SelfOutTypeTag.php @@ -0,0 +1,27 @@ +type; + } + + /** + * @return self + */ + public function withType(Type $type): TypedTag + { + return new self($type); + } + +} diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index dc7a0a4cc5..e080c998ea 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -10,6 +10,7 @@ use PhpParser\Node\Name; use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\NameScope; +use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode; @@ -28,17 +29,22 @@ use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; +use PHPStan\PhpDocParser\Ast\Type\InvalidTypeNode; use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; +use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; +use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\PassedByReference; use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; @@ -62,20 +68,29 @@ use PHPStan\Type\FloatType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\TemplateType; +use PHPStan\Type\Generic\TemplateTypeFactory; +use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeScope; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Helper\GetTemplateTypeType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; use PHPStan\Type\IterableType; use PHPStan\Type\KeyOfType; use PHPStan\Type\MixedType; -use PHPStan\Type\NeverType; +use PHPStan\Type\NonAcceptingNeverType; use PHPStan\Type\NonexistentParentClassType; use PHPStan\Type\NullType; +use PHPStan\Type\ObjectShapeType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\OffsetAccessType; use PHPStan\Type\ResourceType; use PHPStan\Type\StaticType; +use PHPStan\Type\StaticTypeFactory; +use PHPStan\Type\StringAlwaysAcceptingObjectWithToStringType; use PHPStan\Type\StringType; use PHPStan\Type\ThisType; use PHPStan\Type\Type; @@ -83,7 +98,6 @@ use PHPStan\Type\TypeAliasResolverProvider; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; use PHPStan\Type\ValueOfType; use PHPStan\Type\VoidType; @@ -98,14 +112,18 @@ use function min; use function preg_match; use function preg_quote; +use function str_contains; use function str_replace; -use function strpos; +use function str_starts_with; use function strtolower; use function substr; class TypeNodeResolver { + /** @var array */ + private array $genericTypeResolvingStack = []; + public function __construct( private TypeNodeResolverExtensionRegistryProvider $extensionRegistryProvider, private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, @@ -158,10 +176,14 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): Type } elseif ($typeNode instanceof ArrayShapeNode) { return $this->resolveArrayShapeNode($typeNode, $nameScope); + } elseif ($typeNode instanceof ObjectShapeNode) { + return $this->resolveObjectShapeNode($typeNode, $nameScope); } elseif ($typeNode instanceof ConstTypeNode) { return $this->resolveConstTypeNode($typeNode, $nameScope); } elseif ($typeNode instanceof OffsetAccessTypeNode) { return $this->resolveOffsetAccessNode($typeNode, $nameScope); + } elseif ($typeNode instanceof InvalidTypeNode) { + return new MixedType(true); } return new ErrorType(); @@ -180,7 +202,20 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco case 'negative-int': return IntegerRangeType::fromInterval(null, -1); + case 'non-positive-int': + return IntegerRangeType::fromInterval(null, 0); + + case 'non-negative-int': + return IntegerRangeType::fromInterval(0, null); + + case 'non-zero-int': + return new UnionType([ + IntegerRangeType::fromInterval(null, -1), + IntegerRangeType::fromInterval(1, null), + ]); + case 'string': + case 'lowercase-string': return new StringType(); case 'literal-string': @@ -189,6 +224,7 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco case 'class-string': case 'interface-string': case 'trait-string': + case 'enum-string': return new ClassStringType(); case 'callable-string': @@ -206,6 +242,18 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco return new UnionType([new IntegerType(), new FloatType(), new StringType(), new BooleanType()]); + case 'empty-scalar': + return TypeCombinator::intersect( + new UnionType([new IntegerType(), new FloatType(), new StringType(), new BooleanType()]), + StaticTypeFactory::falsey(), + ); + + case 'non-empty-scalar': + return TypeCombinator::remove( + new UnionType([new IntegerType(), new FloatType(), new StringType(), new BooleanType()]), + StaticTypeFactory::falsey(), + ); + case 'number': $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); @@ -238,6 +286,7 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco ]); case 'non-empty-string': + case 'non-empty-lowercase-string': return new IntersectionType([ new StringType(), new AccessoryNonEmptyStringType(), @@ -250,6 +299,13 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco new AccessoryNonFalsyStringType(), ]); + case 'non-empty-literal-string': + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + new AccessoryLiteralStringType(), + ]); + case 'bool': return new BooleanType(); @@ -299,6 +355,12 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco case 'callable': return new CallableType(); + case 'pure-callable': + return new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()); + + case 'pure-closure': + return new ClosureType(); + case 'resource': $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); @@ -308,37 +370,65 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco return new ResourceType(); + case 'open-resource': + case 'closed-resource': + return new ResourceType(); + case 'mixed': return new MixedType(true); + case 'non-empty-mixed': + return new MixedType(true, StaticTypeFactory::falsey()); + case 'void': return new VoidType(); case 'object': return new ObjectWithoutClassType(); + case 'callable-object': + return new IntersectionType([new ObjectWithoutClassType(), new CallableType()]); + + case 'callable-array': + return new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]); + case 'never': + case 'noreturn': $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); if ($type !== null) { return $type; } - return new NeverType(true); + return new NonAcceptingNeverType(); case 'never-return': case 'never-returns': case 'no-return': - case 'noreturn': - return new NeverType(true); + return new NonAcceptingNeverType(); case 'list': - return new ArrayType(new IntegerType(), new MixedType()); + return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new MixedType())); case 'non-empty-list': - return TypeCombinator::intersect( + return AccessoryArrayListType::intersectWith(TypeCombinator::intersect( new ArrayType(new IntegerType(), new MixedType()), new NonEmptyArrayType(), + )); + case '__always-list': + return TypeCombinator::intersect( + new ArrayType(new IntegerType(), new MixedType()), + new AccessoryArrayListType(), ); + + case 'empty': + $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); + if ($type !== null) { + return $type; + } + + return StaticTypeFactory::falsey(); + case '__stringandstringable': + return new StringAlwaysAcceptingObjectWithToStringType(); } if ($nameScope->getClassName() !== null) { @@ -379,7 +469,7 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco } $stringName = $nameScope->resolveStringName($typeNode->name); - if (strpos($stringName, '-') !== false && strpos($stringName, 'OCI-') !== 0) { + if (str_contains($stringName, '-') && !str_starts_with($stringName, 'OCI-')) { return new ErrorType(); } @@ -530,8 +620,23 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na { $mainTypeName = strtolower($typeNode->type->name); $genericTypes = $this->resolveMultiple($typeNode->genericTypes, $nameScope); + $variances = array_map( + static function (string $variance): TemplateTypeVariance { + switch ($variance) { + case GenericTypeNode::VARIANCE_INVARIANT: + return TemplateTypeVariance::createInvariant(); + case GenericTypeNode::VARIANCE_COVARIANT: + return TemplateTypeVariance::createCovariant(); + case GenericTypeNode::VARIANCE_CONTRAVARIANT: + return TemplateTypeVariance::createContravariant(); + case GenericTypeNode::VARIANCE_BIVARIANT: + return TemplateTypeVariance::createBivariant(); + } + }, + $typeNode->variances, + ); - if ($mainTypeName === 'array' || $mainTypeName === 'non-empty-array') { + if (in_array($mainTypeName, ['array', 'non-empty-array'], true)) { if (count($genericTypes) === 1) { // array $arrayType = new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), $genericTypes[0]); } elseif (count($genericTypes) === 2) { // array @@ -539,7 +644,7 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na new IntegerType(), new StringType(), ])); - $arrayType = new ArrayType(!$keyType instanceof NeverType ? ArrayType::castToArrayKeyType($keyType) : $keyType, $genericTypes[1]); + $arrayType = new ArrayType($keyType->toArrayKey(), $genericTypes[1]); } else { return new ErrorType(); } @@ -549,9 +654,9 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na } return $arrayType; - } elseif ($mainTypeName === 'list' || $mainTypeName === 'non-empty-list') { + } elseif (in_array($mainTypeName, ['list', 'non-empty-list'], true)) { if (count($genericTypes) === 1) { // list - $listType = new ArrayType(new IntegerType(), $genericTypes[0]); + $listType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $genericTypes[0])); if ($mainTypeName === 'non-empty-list') { return TypeCombinator::intersect($listType, new NonEmptyArrayType()); } @@ -572,7 +677,7 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na } elseif (in_array($mainTypeName, ['class-string', 'interface-string'], true)) { if (count($genericTypes) === 1) { $genericType = $genericTypes[0]; - if ((new ObjectWithoutClassType())->isSuperTypeOf($genericType)->yes() || $genericType instanceof MixedType) { + if ($genericType->isObject()->yes() || $genericType instanceof MixedType) { return new GenericClassStringType($genericType); } } @@ -608,27 +713,8 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na return new ErrorType(); } elseif ($mainTypeName === 'value-of') { if (count($genericTypes) === 1) { // value-of - if ($genericTypes[0] instanceof TypeWithClassName) { - if ($this->getReflectionProvider()->hasClass($genericTypes[0]->getClassName())) { - $classReflection = $this->getReflectionProvider()->getClass($genericTypes[0]->getClassName()); - - if ($classReflection->isBackedEnum()) { - $cases = []; - foreach ($classReflection->getEnumCases() as $enumCaseReflection) { - $backingType = $enumCaseReflection->getBackingValueType(); - if ($backingType === null) { - continue; - } - - $cases[] = $backingType; - } - - return TypeCombinator::union(...$cases); - } - } - } - $type = new ValueOfType($genericTypes[0]); + return $type->isResolvable() ? $type->resolve() : $type; } @@ -655,90 +741,142 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na if (count($genericTypes) === 1) { return TypeUtils::toBenevolentUnion($genericTypes[0]); } + return new ErrorType(); + } elseif ($mainTypeName === 'template-type') { + if (count($genericTypes) === 3) { + $result = []; + /** @var class-string $ancestorClassName */ + foreach ($genericTypes[1]->getObjectClassNames() as $ancestorClassName) { + foreach ($genericTypes[2]->getConstantStrings() as $templateTypeName) { + $result[] = new GetTemplateTypeType($genericTypes[0], $ancestorClassName, $templateTypeName->getValue()); + } + } + + return TypeCombinator::union(...$result); + } + return new ErrorType(); } $mainType = $this->resolveIdentifierTypeNode($typeNode->type, $nameScope); + $mainTypeObjectClassNames = $mainType->getObjectClassNames(); + if (count($mainTypeObjectClassNames) > 1) { + if ($mainType instanceof TemplateType) { + return new ErrorType(); + } + throw new ShouldNotHappenException(); + } + $mainTypeClassName = $mainTypeObjectClassNames[0] ?? null; - if ($mainType instanceof TypeWithClassName) { - if (!$this->getReflectionProvider()->hasClass($mainType->getClassName())) { - return new GenericObjectType($mainType->getClassName(), $genericTypes); + if ($mainTypeClassName !== null) { + if (!$this->getReflectionProvider()->hasClass($mainTypeClassName)) { + return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances); } - $classReflection = $this->getReflectionProvider()->getClass($mainType->getClassName()); + $classReflection = $this->getReflectionProvider()->getClass($mainTypeClassName); if ($classReflection->isGeneric()) { - if (in_array($mainType->getClassName(), [ + if (in_array($mainTypeClassName, [ Traversable::class, IteratorAggregate::class, Iterator::class, ], true)) { if (count($genericTypes) === 1) { - return new GenericObjectType($mainType->getClassName(), [ + return new GenericObjectType($mainTypeClassName, [ new MixedType(true), $genericTypes[0], + ], null, null, [ + TemplateTypeVariance::createInvariant(), + $variances[0], ]); } if (count($genericTypes) === 2) { - return new GenericObjectType($mainType->getClassName(), [ + return new GenericObjectType($mainTypeClassName, [ $genericTypes[0], $genericTypes[1], + ], null, null, [ + $variances[0], + $variances[1], ]); } } - if ($mainType->getClassName() === Generator::class) { + if ($mainTypeClassName === Generator::class) { if (count($genericTypes) === 1) { $mixed = new MixedType(true); - return new GenericObjectType($mainType->getClassName(), [ + return new GenericObjectType($mainTypeClassName, [ $mixed, $genericTypes[0], $mixed, $mixed, + ], null, null, [ + TemplateTypeVariance::createInvariant(), + $variances[0], + TemplateTypeVariance::createInvariant(), + TemplateTypeVariance::createInvariant(), ]); } if (count($genericTypes) === 2) { $mixed = new MixedType(true); - return new GenericObjectType($mainType->getClassName(), [ + return new GenericObjectType($mainTypeClassName, [ $genericTypes[0], $genericTypes[1], $mixed, $mixed, + ], null, null, [ + $variances[0], + $variances[1], + TemplateTypeVariance::createInvariant(), + TemplateTypeVariance::createInvariant(), ]); } } if (!$mainType->isIterable()->yes()) { - return new GenericObjectType($mainType->getClassName(), $genericTypes); + return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances); } if ( count($genericTypes) !== 1 || $classReflection->getTemplateTypeMap()->count() === 1 ) { - return new GenericObjectType($mainType->getClassName(), $genericTypes); + return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances); } } } if ($mainType->isIterable()->yes()) { - if (count($genericTypes) === 1) { // Foo - return TypeCombinator::intersect( - $mainType, - new IterableType(new MixedType(true), $genericTypes[0]), - ); + if ($mainTypeClassName !== null) { + if (isset($this->genericTypeResolvingStack[$mainTypeClassName])) { + return new ErrorType(); + } + + $this->genericTypeResolvingStack[$mainTypeClassName] = true; } - if (count($genericTypes) === 2) { // Foo - return TypeCombinator::intersect( - $mainType, - new IterableType($genericTypes[0], $genericTypes[1]), - ); + try { + if (count($genericTypes) === 1) { // Foo + return TypeCombinator::intersect( + $mainType, + new IterableType(new MixedType(true), $genericTypes[0]), + ); + } + + if (count($genericTypes) === 2) { // Foo + return TypeCombinator::intersect( + $mainType, + new IterableType($genericTypes[0], $genericTypes[1]), + ); + } + } finally { + if ($mainTypeClassName !== null) { + unset($this->genericTypeResolvingStack[$mainTypeClassName]); + } } } - if ($mainType instanceof TypeWithClassName) { - return new GenericObjectType($mainType->getClassName(), $genericTypes); + if ($mainTypeClassName !== null) { + return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances); } return new ErrorType(); @@ -746,15 +884,41 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na private function resolveCallableTypeNode(CallableTypeNode $typeNode, NameScope $nameScope): Type { + $templateTags = []; + + if (count($typeNode->templateTypes ?? []) > 0) { + foreach ($typeNode->templateTypes as $templateType) { + $templateTags[$templateType->name] = new TemplateTag( + $templateType->name, + $templateType->bound !== null + ? $this->resolve($templateType->bound, $nameScope) + : new MixedType(), + TemplateTypeVariance::createInvariant(), + ); + } + $templateTypeScope = TemplateTypeScope::createWithAnonymousFunction(); + + $templateTypeMap = new TemplateTypeMap(array_map( + static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), + $templateTags, + )); + + $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap); + } else { + $templateTypeMap = TemplateTypeMap::createEmpty(); + } + $mainType = $this->resolve($typeNode->identifier, $nameScope); + $isVariadic = false; $parameters = array_map( function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadic): NativeParameterReflection { $isVariadic = $isVariadic || $parameterNode->isVariadic; $parameterName = $parameterNode->parameterName; - if (strpos($parameterName, '$') === 0) { + if (str_starts_with($parameterName, '$')) { $parameterName = substr($parameterName, 1); } + return new NativeParameterReflection( $parameterName, $parameterNode->isOptional || $parameterNode->isVariadic, @@ -766,16 +930,35 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi }, $typeNode->parameters, ); + $returnType = $this->resolve($typeNode->returnType, $nameScope); if ($mainType instanceof CallableType) { - return new CallableType($parameters, $returnType, $isVariadic); + $pure = $mainType->isPure(); + if ($pure->yes() && $returnType->isVoid()->yes()) { + return new ErrorType(); + } + + return new CallableType($parameters, $returnType, $isVariadic, $templateTypeMap, null, $templateTags, $pure); } elseif ( $mainType instanceof ObjectType && $mainType->getClassName() === Closure::class ) { - return new ClosureType($parameters, $returnType, $isVariadic); + return new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, null, null, $templateTags, [], [ + new SimpleImpurePoint( + 'functionCall', + 'call to a Closure', + false, + ), + ]); + } elseif ($mainType instanceof ClosureType) { + $closure = new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, null, null, $templateTags, [], $mainType->getImpurePoints()); + if ($closure->isPure()->yes() && $returnType->isVoid()->yes()) { + return new ErrorType(); + } + + return $closure; } return new ErrorType(); @@ -784,6 +967,9 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi private function resolveArrayShapeNode(ArrayShapeNode $typeNode, NameScope $nameScope): Type { $builder = ConstantArrayTypeBuilder::createEmpty(); + if (count($typeNode->items) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + $builder->degradeToGeneralArray(true); + } foreach ($typeNode->items as $itemNode) { $offsetType = null; @@ -799,7 +985,33 @@ private function resolveArrayShapeNode(ArrayShapeNode $typeNode, NameScope $name $builder->setOffsetValueType($offsetType, $this->resolve($itemNode->valueType, $nameScope), $itemNode->optional); } - return $builder->getArray(); + $arrayType = $builder->getArray(); + if ($typeNode->kind === ArrayShapeNode::KIND_LIST) { + $arrayType = AccessoryArrayListType::intersectWith($arrayType); + } + + return $arrayType; + } + + private function resolveObjectShapeNode(ObjectShapeNode $typeNode, NameScope $nameScope): Type + { + $properties = []; + $optionalProperties = []; + foreach ($typeNode->items as $itemNode) { + if ($itemNode->keyName instanceof IdentifierTypeNode) { + $propertyName = $itemNode->keyName->name; + } elseif ($itemNode->keyName instanceof ConstExprStringNode) { + $propertyName = $itemNode->keyName->value; + } + + if ($itemNode->optional) { + $optionalProperties[] = $propertyName; + } + + $properties[$propertyName] = $this->resolve($itemNode->valueType, $nameScope); + } + + return new ObjectShapeType($properties, $optionalProperties); } private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameScope): Type @@ -896,9 +1108,13 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc return new EnumCaseObjectType($classReflection->getName(), $constantName); } - $reflectionConstant = $classReflection->getConstant($constantName); + $reflectionConstant = $classReflection->getNativeReflection()->getReflectionConstant($constantName); + if ($reflectionConstant === false) { + return new ErrorType(); + } + $declaringClass = $reflectionConstant->getDeclaringClass(); - return $this->initializerExprTypeResolver->getType($reflectionConstant->getValueExpr(), InitializerExprContext::fromClassReflection($reflectionConstant->getDeclaringClass())); + return $this->initializerExprTypeResolver->getType($reflectionConstant->getValueExpression(), InitializerExprContext::fromClass($declaringClass->getName(), $declaringClass->getFileName() ?: null)); } if ($constExpr instanceof ConstExprFloatNode) { diff --git a/src/PhpDoc/TypeNodeResolverExtension.php b/src/PhpDoc/TypeNodeResolverExtension.php index 36508c7834..de004ecbba 100644 --- a/src/PhpDoc/TypeNodeResolverExtension.php +++ b/src/PhpDoc/TypeNodeResolverExtension.php @@ -6,7 +6,23 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Type\Type; -/** @api */ +/** + * This is the interface type node resolver extensions implement for custom PHPDoc types. + * + * To register it in the configuration file use the `phpstan.phpDoc.typeNodeResolverExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.phpDoc.typeNodeResolverExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/custom-phpdoc-types + * + * @api + */ interface TypeNodeResolverExtension { diff --git a/src/PhpDoc/TypeStringResolver.php b/src/PhpDoc/TypeStringResolver.php index 3933e1d25b..91333363ed 100644 --- a/src/PhpDoc/TypeStringResolver.php +++ b/src/PhpDoc/TypeStringResolver.php @@ -20,7 +20,7 @@ public function resolve(string $typeString, ?NameScope $nameScope = null): Type { $tokens = new TokenIterator($this->typeLexer->tokenize($typeString)); $typeNode = $this->typeParser->parse($tokens); - $tokens->consumeTokenType(Lexer::TOKEN_END); // @phpstan-ignore-line + $tokens->consumeTokenType(Lexer::TOKEN_END); // @phpstan-ignore missingType.checkedException return $this->typeNodeResolver->resolve($typeNode, $nameScope ?? new NameScope(null, [])); } diff --git a/src/Process/CpuCoreCounter.php b/src/Process/CpuCoreCounter.php index 609baad081..cff6084b87 100644 --- a/src/Process/CpuCoreCounter.php +++ b/src/Process/CpuCoreCounter.php @@ -2,16 +2,8 @@ namespace PHPStan\Process; -use function count; -use function fgets; -use function file_get_contents; -use function function_exists; -use function is_file; -use function is_resource; -use function pclose; -use function popen; -use function preg_match_all; -use const DIRECTORY_SEPARATOR; +use Fidry\CpuCoreCounter\CpuCoreCounter as FidryCpuCoreCounter; +use Fidry\CpuCoreCounter\NumberOfCpuCoreNotFound; class CpuCoreCounter { @@ -24,42 +16,13 @@ public function getNumberOfCpuCores(): int return $this->count; } - if (!function_exists('proc_open')) { - return $this->count = 1; + try { + $this->count = (new FidryCpuCoreCounter())->getCount(); + } catch (NumberOfCpuCoreNotFound) { + $this->count = 1; } - // from brianium/paratest - if (@is_file('/proc/cpuinfo')) { - // Linux (and potentially Windows with linux sub systems) - $cpuinfo = @file_get_contents('/proc/cpuinfo'); - if ($cpuinfo !== false) { - preg_match_all('/^processor/m', $cpuinfo, $matches); - return $this->count = count($matches[0]); - } - } - - if (DIRECTORY_SEPARATOR === '\\') { - // Windows - $process = @popen('wmic cpu get NumberOfLogicalProcessors', 'rb'); - if (is_resource($process)) { - fgets($process); - $cores = (int) fgets($process); - pclose($process); - - return $this->count = $cores; - } - } - - $process = @popen('sysctl -n hw.ncpu', 'rb'); - if (is_resource($process)) { - // *nix (Linux, BSD and Mac) - $cores = (int) fgets($process); - pclose($process); - - return $this->count = $cores; - } - - return $this->count = 2; + return $this->count; } } diff --git a/src/Process/ProcessPromise.php b/src/Process/ProcessPromise.php index 26b7fb256a..91362ace74 100644 --- a/src/Process/ProcessPromise.php +++ b/src/Process/ProcessPromise.php @@ -2,7 +2,6 @@ namespace PHPStan\Process; -use PHPStan\Process\Runnable\Runnable; use PHPStan\ShouldNotHappenException; use React\ChildProcess\Process; use React\EventLoop\LoopInterface; @@ -14,7 +13,7 @@ use function stream_get_contents; use function tmpfile; -class ProcessPromise implements Runnable +class ProcessPromise { private Deferred $deferred; diff --git a/src/Process/Runnable/Runnable.php b/src/Process/Runnable/Runnable.php deleted file mode 100644 index e25e9154cc..0000000000 --- a/src/Process/Runnable/Runnable.php +++ /dev/null @@ -1,16 +0,0 @@ - */ - private array $queue = []; - - /** @var SplObjectStorage */ - private SplObjectStorage $running; - - public function __construct(private RunnableQueueLogger $logger, private int $maxSize) - { - /** @var SplObjectStorage $running */ - $running = new SplObjectStorage(); - $this->running = $running; - } - - public function getQueueSize(): int - { - $allSize = 0; - foreach ($this->queue as [$runnable, $size, $deferred]) { - $allSize += $size; - } - - return $allSize; - } - - public function getRunningSize(): int - { - $allSize = 0; - foreach ($this->running as $running) { // phpcs:ignore - [$size] = $this->running->getInfo(); - $allSize += $size; - } - - return $allSize; - } - - public function queue(Runnable $runnable, int $size): CancellablePromiseInterface - { - if ($size > $this->maxSize) { - throw new ShouldNotHappenException('Runnable size exceeds queue maxSize.'); - } - - $deferred = new Deferred(static function () use ($runnable): void { - $runnable->cancel(); - }); - $this->queue[] = [$runnable, $size, $deferred]; - $this->drainQueue(); - - /** @var CancellablePromiseInterface */ - return $deferred->promise(); - } - - private function drainQueue(): void - { - if (count($this->queue) === 0) { - $this->logger->log('Queue empty'); - return; - } - - $currentQueueSize = $this->getRunningSize(); - if ($currentQueueSize > $this->maxSize) { - throw new ShouldNotHappenException('Running overflow'); - } - - if ($currentQueueSize === $this->maxSize) { - $this->logger->log('Queue is full'); - return; - } - - $this->logger->log('Queue not full - looking at first item in the queue'); - - [$runnable, $runnableSize, $deferred] = $this->queue[0]; - - $newSize = $currentQueueSize + $runnableSize; - if ($newSize > $this->maxSize) { - $this->logger->log( - sprintf( - 'Canot remote first item from the queue - it has size %d, current queue size is %d, new size would be %d', - $runnableSize, - $currentQueueSize, - $newSize, - ), - ); - return; - } - - $this->logger->log(sprintf('Removing top item from queue - new size is %d', $newSize)); - - /** @var array{Runnable, int, Deferred} $popped */ - $popped = array_shift($this->queue); - if ($popped[0] !== $runnable || $popped[1] !== $runnableSize || $popped[2] !== $deferred) { - throw new ShouldNotHappenException(); - } - - $this->running->attach($runnable, [$runnableSize, $deferred]); - $this->logger->log(sprintf('Running process %s', $runnable->getName())); - $runnable->run()->then(function ($value) use ($runnable, $deferred): void { - $this->logger->log(sprintf('Process %s finished successfully', $runnable->getName())); - $deferred->resolve($value); - $this->running->detach($runnable); - $this->drainQueue(); - }, function (Throwable $e) use ($runnable, $deferred): void { - $this->logger->log(sprintf('Process %s finished unsuccessfully: %s', $runnable->getName(), $e->getMessage())); - $deferred->reject($e); - $this->running->detach($runnable); - $this->drainQueue(); - }); - } - - public function cancelAll(): void - { - foreach ($this->queue as [$runnable, $size, $deferred]) { - $deferred->promise()->cancel(); // @phpstan-ignore-line - } - - $runningDeferreds = []; - foreach ($this->running as $running) { // phpcs:ignore - [,$deferred] = $this->running->getInfo(); - $runningDeferreds[] = $deferred; - } - - foreach ($runningDeferreds as $deferred) { - $deferred->promise()->cancel(); // @phpstan-ignore-line - } - } - -} diff --git a/src/Process/Runnable/RunnableQueueLogger.php b/src/Process/Runnable/RunnableQueueLogger.php deleted file mode 100644 index a29ada14b3..0000000000 --- a/src/Process/Runnable/RunnableQueueLogger.php +++ /dev/null @@ -1,10 +0,0 @@ - + */ + public function getAllowedSubTypes(ClassReflection $classReflection): array; + +} diff --git a/src/Reflection/Annotations/AnnotationMethodReflection.php b/src/Reflection/Annotations/AnnotationMethodReflection.php index 3c31511389..cfd5b74e03 100644 --- a/src/Reflection/Annotations/AnnotationMethodReflection.php +++ b/src/Reflection/Annotations/AnnotationMethodReflection.php @@ -2,19 +2,21 @@ namespace PHPStan\Reflection\Annotations; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\FunctionVariant; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\FunctionVariantWithPhpDocs; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\MixedType; +use PHPStan\Type\ThisType; use PHPStan\Type\Type; class AnnotationMethodReflection implements ExtendedMethodReflection { - /** @var FunctionVariant[]|null */ + /** @var FunctionVariantWithPhpDocs[]|null */ private ?array $variants = null; /** @@ -27,6 +29,8 @@ public function __construct( private array $parameters, private bool $isStatic, private bool $isVariadic, + private ?Type $throwType, + private TemplateTypeMap $templateTypeMap, ) { } @@ -61,25 +65,29 @@ public function getName(): string return $this->name; } - /** - * @return ParametersAcceptor[] - */ public function getVariants(): array { if ($this->variants === null) { $this->variants = [ - new FunctionVariant( - TemplateTypeMap::createEmpty(), + new FunctionVariantWithPhpDocs( + $this->templateTypeMap, null, $this->parameters, $this->isVariadic, $this->returnType, + $this->returnType, + new MixedType(), ), ]; } return $this->variants; } + public function getNamedArgumentsVariants(): ?array + { + return null; + } + public function isDeprecated(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -95,6 +103,11 @@ public function isFinal(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isInternal(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -102,11 +115,19 @@ public function isInternal(): TrinaryLogic public function getThrowType(): ?Type { - return null; + return $this->throwType; } public function hasSideEffects(): TrinaryLogic { + if ($this->returnType->isVoid()->yes()) { + return TrinaryLogic::createYes(); + } + + if ((new ThisType($this->declaringClass))->isSuperTypeOf($this->returnType)->yes()) { + return TrinaryLogic::createYes(); + } + return TrinaryLogic::createMaybe(); } @@ -115,4 +136,33 @@ public function getDocComment(): ?string return null; } + public function getAsserts(): Assertions + { + return Assertions::createEmpty(); + } + + public function getSelfOutType(): ?Type + { + return null; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isPure(): TrinaryLogic + { + if ($this->hasSideEffects()->yes()) { + return TrinaryLogic::createNo(); + } + + return TrinaryLogic::createMaybe(); + } + } diff --git a/src/Reflection/Annotations/AnnotationPropertyReflection.php b/src/Reflection/Annotations/AnnotationPropertyReflection.php index 3613e87707..6fe9b9d125 100644 --- a/src/Reflection/Annotations/AnnotationPropertyReflection.php +++ b/src/Reflection/Annotations/AnnotationPropertyReflection.php @@ -12,9 +12,10 @@ class AnnotationPropertyReflection implements PropertyReflection public function __construct( private ClassReflection $declaringClass, - private Type $type, - private bool $readable = true, - private bool $writable = true, + private Type $readableType, + private Type $writableType, + private bool $readable, + private bool $writable, ) { } @@ -41,17 +42,17 @@ public function isPublic(): bool public function getReadableType(): Type { - return $this->type; + return $this->readableType; } public function getWritableType(): Type { - return $this->type; + return $this->writableType; } public function canChangeTypeAfterAssignment(): bool { - return true; + return $this->readableType->equals($this->writableType); } public function isReadable(): bool diff --git a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php index 8b51b325cf..fac7e5a9bb 100644 --- a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php +++ b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php @@ -2,11 +2,13 @@ namespace PHPStan\Reflection\Annotations; -use PHPStan\Reflection\ParameterReflection; +use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\PassedByReference; +use PHPStan\TrinaryLogic; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; -class AnnotationsMethodParameterReflection implements ParameterReflection +class AnnotationsMethodParameterReflection implements ParameterReflectionWithPhpDocs { public function __construct(private string $name, private Type $type, private PassedByReference $passedByReference, private bool $isOptional, private bool $isVariadic, private ?Type $defaultValue) @@ -28,6 +30,31 @@ public function getType(): Type return $this->type; } + public function getPhpDocType(): Type + { + return $this->type; + } + + public function getNativeType(): Type + { + return new MixedType(); + } + + public function getOutType(): ?Type + { + return null; + } + + public function isImmediatelyInvokedCallable(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClosureThisType(): ?Type + { + return null; + } + public function passedByReference(): PassedByReference { return $this->passedByReference; diff --git a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php index d16431ef33..2860988c91 100644 --- a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php +++ b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php @@ -2,11 +2,18 @@ namespace PHPStan\Reflection\Annotations; +use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; +use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeHelper; +use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeScope; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Type; +use function array_map; use function count; class AnnotationsMethodsClassReflectionExtension implements MethodsClassReflectionExtension @@ -56,16 +63,32 @@ private function findClassReflectionWithMethod( ); } + $templateTypeScope = TemplateTypeScope::createWithClass($classReflection->getName()); + + $templateTypeMap = new TemplateTypeMap(array_map( + static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), + $methodTags[$methodName]->getTemplateTags(), + )); + + $isStatic = $methodTags[$methodName]->isStatic(); + $nativeCallMethodName = $isStatic ? '__callStatic' : '__call'; + return new AnnotationMethodReflection( $methodName, $declaringClass, TemplateTypeHelper::resolveTemplateTypes( $methodTags[$methodName]->getReturnType(), $classReflection->getActiveTemplateTypeMap(), + $classReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createCovariant(), ), $parameters, - $methodTags[$methodName]->isStatic(), + $isStatic, $this->detectMethodVariadic($parameters), + $classReflection->hasNativeMethod($nativeCallMethodName) + ? $classReflection->getNativeMethod($nativeCallMethodName)->getThrowType() + : null, + $templateTypeMap, ); } @@ -78,21 +101,23 @@ private function findClassReflectionWithMethod( return $methodWithDeclaringClass; } - foreach ($classReflection->getParents() as $parentClass) { + $parentClass = $classReflection->getParentClass(); + while ($parentClass !== null) { $methodWithDeclaringClass = $this->findClassReflectionWithMethod($parentClass, $parentClass, $methodName); - if ($methodWithDeclaringClass === null) { - foreach ($parentClass->getTraits() as $traitClass) { - $parentTraitMethodWithDeclaringClass = $this->findClassReflectionWithMethod($traitClass, $parentClass, $methodName); - if ($parentTraitMethodWithDeclaringClass === null) { - continue; - } + if ($methodWithDeclaringClass !== null) { + return $methodWithDeclaringClass; + } - return $parentTraitMethodWithDeclaringClass; + foreach ($parentClass->getTraits() as $traitClass) { + $parentTraitMethodWithDeclaringClass = $this->findClassReflectionWithMethod($traitClass, $parentClass, $methodName); + if ($parentTraitMethodWithDeclaringClass === null) { + continue; } - continue; + + return $parentTraitMethodWithDeclaringClass; } - return $methodWithDeclaringClass; + $parentClass = $parentClass->getParentClass(); } foreach ($classReflection->getInterfaces() as $interfaceClass) { diff --git a/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php b/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php index 324f14b2ab..3ef4959ab8 100644 --- a/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php +++ b/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php @@ -6,6 +6,8 @@ use PHPStan\Reflection\PropertiesClassReflectionExtension; use PHPStan\Reflection\PropertyReflection; use PHPStan\Type\Generic\TemplateTypeHelper; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\NeverType; class AnnotationsPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension { @@ -39,14 +41,32 @@ private function findClassReflectionWithProperty( { $propertyTags = $classReflection->getPropertyTags(); if (isset($propertyTags[$propertyName])) { + $propertyTag = $propertyTags[$propertyName]; + + $isReadable = $propertyTags[$propertyName]->isReadable(); + $isWritable = $propertyTags[$propertyName]->isWritable(); + if ($classReflection->hasNativeProperty($propertyName)) { + $nativeProperty = $classReflection->getNativeProperty($propertyName); + $isReadable = $isReadable || $nativeProperty->isReadable(); + $isWritable = $isWritable || $nativeProperty->isWritable(); + } + return new AnnotationPropertyReflection( $declaringClass, TemplateTypeHelper::resolveTemplateTypes( - $propertyTags[$propertyName]->getType(), + $propertyTag->getReadableType() ?? new NeverType(), + $classReflection->getActiveTemplateTypeMap(), + $classReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createCovariant(), + ), + TemplateTypeHelper::resolveTemplateTypes( + $propertyTag->getWritableType() ?? new NeverType(), $classReflection->getActiveTemplateTypeMap(), + $classReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createContravariant(), ), - $propertyTags[$propertyName]->isReadable(), - $propertyTags[$propertyName]->isWritable(), + $isReadable, + $isWritable, ); } @@ -59,21 +79,23 @@ private function findClassReflectionWithProperty( return $methodWithDeclaringClass; } - foreach ($classReflection->getParents() as $parentClass) { + $parentClass = $classReflection->getParentClass(); + while ($parentClass !== null) { $methodWithDeclaringClass = $this->findClassReflectionWithProperty($parentClass, $parentClass, $propertyName); - if ($methodWithDeclaringClass === null) { - foreach ($parentClass->getTraits() as $traitClass) { - $parentTraitMethodWithDeclaringClass = $this->findClassReflectionWithProperty($traitClass, $parentClass, $propertyName); - if ($parentTraitMethodWithDeclaringClass === null) { - continue; - } + if ($methodWithDeclaringClass !== null) { + return $methodWithDeclaringClass; + } - return $parentTraitMethodWithDeclaringClass; + foreach ($parentClass->getTraits() as $traitClass) { + $parentTraitMethodWithDeclaringClass = $this->findClassReflectionWithProperty($traitClass, $parentClass, $propertyName); + if ($parentTraitMethodWithDeclaringClass === null) { + continue; } - continue; + + return $parentTraitMethodWithDeclaringClass; } - return $methodWithDeclaringClass; + $parentClass = $parentClass->getParentClass(); } foreach ($classReflection->getInterfaces() as $interfaceClass) { diff --git a/src/Reflection/Assertions.php b/src/Reflection/Assertions.php new file mode 100644 index 0000000000..6adb97136c --- /dev/null +++ b/src/Reflection/Assertions.php @@ -0,0 +1,111 @@ +asserts; + } + + /** + * @return AssertTag[] + */ + public function getAsserts(): array + { + return array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::NULL); + } + + /** + * @return AssertTag[] + */ + public function getAssertsIfTrue(): array + { + return array_merge( + array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::IF_TRUE), + array_map( + static fn (AssertTag $assert) => $assert->negate(), + array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::IF_FALSE && !$assert->isEquality()), + ), + ); + } + + /** + * @return AssertTag[] + */ + public function getAssertsIfFalse(): array + { + return array_merge( + array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::IF_FALSE), + array_map( + static fn (AssertTag $assert) => $assert->negate(), + array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::IF_TRUE && !$assert->isEquality()), + ), + ); + } + + /** + * @param callable(Type): Type $callable + */ + public function mapTypes(callable $callable): self + { + $assertTagsCallback = static fn (AssertTag $tag): AssertTag => $tag->withType($callable($tag->getType())); + + return new self(array_map($assertTagsCallback, $this->asserts)); + } + + public function intersectWith(Assertions $other): self + { + return new self(array_merge($this->getAll(), $other->getAll())); + } + + public static function createEmpty(): self + { + $empty = self::$empty; + + if ($empty !== null) { + return $empty; + } + + $empty = new self([]); + self::$empty = $empty; + + return $empty; + } + + public static function createFromResolvedPhpDocBlock(ResolvedPhpDocBlock $phpDocBlock): self + { + $tags = $phpDocBlock->getAssertTags(); + if (count($tags) === 0) { + return self::createEmpty(); + } + + return new self($tags); + } + +} diff --git a/src/Reflection/BetterReflection/BetterReflectionProvider.php b/src/Reflection/BetterReflection/BetterReflectionProvider.php index 541c530705..67166c39d1 100644 --- a/src/Reflection/BetterReflection/BetterReflectionProvider.php +++ b/src/Reflection/BetterReflection/BetterReflectionProvider.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection\BetterReflection; use Closure; +use Nette\Utils\Strings; use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\BetterReflection\Identifier\Exception\InvalidIdentifierName; @@ -10,8 +11,6 @@ use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; -use PHPStan\BetterReflection\Reflection\Exception\NotAClassReflection; -use PHPStan\BetterReflection\Reflection\Exception\NotAnInterfaceReflection; use PHPStan\BetterReflection\Reflection\ReflectionEnum; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; use PHPStan\BetterReflection\Reflector\Reflector; @@ -28,7 +27,9 @@ use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\StubPhpDocProvider; -use PHPStan\PhpDoc\Tag\ParamTag; +use PHPStan\PhpDoc\Tag\ParamClosureThisTag; +use PHPStan\PhpDoc\Tag\ParamOutTag; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassNameHelper; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Constant\RuntimeConstantReflection; @@ -41,7 +42,9 @@ use PHPStan\Reflection\Php\PhpFunctionReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\SignatureMap\NativeFunctionReflectionProvider; +use PHPStan\Reflection\SignatureMap\SignatureMapProvider; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Type; @@ -67,6 +70,9 @@ class BetterReflectionProvider implements ReflectionProvider /** @var array */ private array $cachedConstants = []; + /** + * @param string[] $universalObjectCratesClasses + */ public function __construct( private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, private InitializerExprTypeResolver $initializerExprTypeResolver, @@ -82,6 +88,8 @@ public function __construct( private AnonymousClassNameHelper $anonymousClassNameHelper, private FileHelper $fileHelper, private PhpStormStubsSourceStubber $phpstormStubsSourceStubber, + private SignatureMapProvider $signatureMapProvider, + private array $universalObjectCratesClasses, ) { } @@ -133,13 +141,18 @@ public function getClass(string $className): ClassReflection $this->stubPhpDocProvider, $this->phpDocInheritanceResolver, $this->phpVersion, + $this->signatureMapProvider, $this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsPropertyClassReflectionExtension(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsMethodsClassReflectionExtension(), $reflectionClass->getName(), $reflectionClass instanceof ReflectionEnum && PHP_VERSION_ID >= 80000 ? new $enumAdapter($reflectionClass) : new ReflectionClass($reflectionClass), null, null, $this->stubPhpDocProvider->findClassPhpDoc($reflectionClass->getName()), + $this->universalObjectCratesClasses, ); $this->classReflections[$reflectionClassName] = $classReflection; @@ -208,13 +221,18 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $ $this->stubPhpDocProvider, $this->phpDocInheritanceResolver, $this->phpVersion, + $this->signatureMapProvider, $this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(), - sprintf('class@anonymous/%s:%s', $filename, $classNode->getLine()), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsPropertyClassReflectionExtension(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsMethodsClassReflectionExtension(), + sprintf('class@anonymous/%s:%s', $filename, $classNode->getStartLine()), new ReflectionClass($reflectionClass), $scopeFile, null, $this->stubPhpDocProvider->findClassPhpDoc($className), + $this->universalObjectCratesClasses, ); $this->classReflections[$className] = self::$anonymousClasses[$className]; @@ -253,7 +271,7 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection { $reflectionFunction = new ReflectionFunction($this->reflector->reflectFunction($functionName)); $templateTypeMap = TemplateTypeMap::createEmpty(); - $phpDocParameterTags = []; + $phpDocParameterTypes = []; $phpDocReturnTag = null; $phpDocThrowsTag = null; $deprecatedTag = null; @@ -261,6 +279,12 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection $isInternal = false; $isFinal = false; $isPure = null; + $asserts = Assertions::createEmpty(); + $phpDocComment = null; + $phpDocParameterOutTags = []; + $phpDocParameterImmediatelyInvokedCallable = []; + $phpDocParameterClosureThisTypeTags = []; + $resolvedPhpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($reflectionFunction->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $reflectionFunction->getParameters())); if ($resolvedPhpDoc === null && $reflectionFunction->getFileName() !== false && $reflectionFunction->getDocComment() !== false) { $docComment = $reflectionFunction->getDocComment(); @@ -269,7 +293,7 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection if ($resolvedPhpDoc !== null) { $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); - $phpDocParameterTags = $resolvedPhpDoc->getParamTags(); + $phpDocParameterTypes = array_map(static fn ($tag) => $tag->getType(), $resolvedPhpDoc->getParamTags()); $phpDocReturnTag = $resolvedPhpDoc->getReturnTag(); $phpDocThrowsTag = $resolvedPhpDoc->getThrowsTag(); $deprecatedTag = $resolvedPhpDoc->getDeprecatedTag(); @@ -277,12 +301,19 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection $isInternal = $resolvedPhpDoc->isInternal(); $isFinal = $resolvedPhpDoc->isFinal(); $isPure = $resolvedPhpDoc->isPure(); + $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); + if ($resolvedPhpDoc->hasPhpDocString()) { + $phpDocComment = $resolvedPhpDoc->getPhpDocString(); + } + $phpDocParameterOutTags = $resolvedPhpDoc->getParamOutTags(); + $phpDocParameterImmediatelyInvokedCallable = $resolvedPhpDoc->getParamsImmediatelyInvokedCallable(); + $phpDocParameterClosureThisTypeTags = $resolvedPhpDoc->getParamClosureThisTags(); } return $this->functionReflectionFactory->create( $reflectionFunction, $templateTypeMap, - array_map(static fn (ParamTag $paramTag): Type => $paramTag->getType(), $phpDocParameterTags), + $phpDocParameterTypes, $phpDocReturnTag !== null ? $phpDocReturnTag->getType() : null, $phpDocThrowsTag !== null ? $phpDocThrowsTag->getType() : null, $deprecatedTag !== null ? $deprecatedTag->getMessage() : null, @@ -291,6 +322,11 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection $isFinal, $reflectionFunction->getFileName() !== false ? $reflectionFunction->getFileName() : null, $isPure, + $asserts, + $phpDocComment, + array_map(static fn (ParamOutTag $paramOutTag): Type => $paramOutTag->getType(), $phpDocParameterOutTags), + $phpDocParameterImmediatelyInvokedCallable, + array_map(static fn (ParamClosureThisTag $tag): Type => $tag->getType(), $phpDocParameterClosureThisTypeTags), ); } @@ -332,11 +368,39 @@ public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAn $constantReflection = $this->reflector->reflectConstant($constantName); $fileName = $constantReflection->getFileName(); $constantValueType = $this->initializerExprTypeResolver->getType($constantReflection->getValueExpression(), InitializerExprContext::fromGlobalConstant($constantReflection)); + $docComment = $constantReflection->getDocComment(); + + $isDeprecated = TrinaryLogic::createNo(); + $deprecatedDescription = null; + if ($docComment !== null) { + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($fileName, null, null, null, $docComment); + $isDeprecated = TrinaryLogic::createFromBoolean($resolvedPhpDoc->isDeprecated()); + + if ($resolvedPhpDoc->isDeprecated() && $resolvedPhpDoc->getDeprecatedTag() !== null) { + $deprecatedMessage = $resolvedPhpDoc->getDeprecatedTag()->getMessage(); + + $matches = Strings::match($deprecatedMessage ?? '', '#^(\d+)\.(\d+)(?:\.(\d+))?$#'); + if ($matches !== null) { + $major = $matches[1]; + $minor = $matches[2]; + $patch = $matches[3] ?? 0; + $versionId = sprintf('%d%02d%02d', $major, $minor, $patch); + + $isDeprecated = TrinaryLogic::createFromBoolean($this->phpVersion->getVersionId() >= $versionId); + } else { + // filter raw version number messages like in + // https://github.com/JetBrains/phpstorm-stubs/blob/9608c953230b08f07b703ecfe459cc58d5421437/filter/filter.php#L478 + $deprecatedDescription = $deprecatedMessage; + } + } + } return $this->cachedConstants[$constantName] = new RuntimeConstantReflection( $constantName, $constantValueType, $fileName, + $isDeprecated, + $deprecatedDescription, ); } @@ -348,7 +412,7 @@ public function resolveConstantName(Node\Name $nameNode, ?NamespaceAnswerer $nam return true; } catch (IdentifierNotFound) { // pass - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection) { + } catch (UnableToCompileNode) { // pass } return false; diff --git a/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php b/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php index 2807869c0a..5a207d0476 100644 --- a/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php +++ b/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php @@ -55,7 +55,6 @@ public function __construct( private array $analysedPaths, private array $composerAutoloaderProjectPaths, private array $analysedPathsFromConfig, - private ?string $singleReflectionFile, ) { } @@ -64,10 +63,6 @@ public function create(): SourceLocator { $locators = []; - if ($this->singleReflectionFile !== null) { - $locators[] = $this->optimizedSingleFileSourceLocatorRepository->getOrCreate($this->singleReflectionFile); - } - $astLocator = new Locator($this->parser); $locators[] = new AutoloadFunctionsSourceLocator( new AutoloadSourceLocator($this->fileNodesFetcher, false), diff --git a/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php index fd213829f4..969d978333 100644 --- a/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php @@ -131,7 +131,13 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): $startLine = $this->startLineByClass[$loweredClassName]; } else { $reflection = $this->getReflectionClass($identifier->getName()); - if ($reflection !== null && $reflection->getStartLine() !== false) { + if ( + $reflection !== null + && $reflection->getStartLine() !== false + && is_string($reflection->getFileName()) + && is_file($reflection->getFileName()) + && $reflection->getFileName() === $this->presentSymbols['classes'][$loweredClassName] + ) { $startLine = $reflection->getStartLine(); } } @@ -283,18 +289,7 @@ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $id private function getReflectionClass(string $className): ?ReflectionClass { if (class_exists($className, false) || interface_exists($className, false) || trait_exists($className, false)) { - $reflection = new ReflectionClass($className); - $filename = $reflection->getFileName(); - - if (!is_string($filename)) { - return null; - } - - if (!is_file($filename)) { - return null; - } - - return $reflection; + return new ReflectionClass($className); } return null; @@ -323,6 +318,9 @@ private function locateClassByName(string $className): ?array if (!is_string($filename)) { return null; } + if (!is_file($filename)) { + return null; + } return [[$filename], $reflection->getName(), $reflection->getStartLine() !== false ? $reflection->getStartLine() : null]; } @@ -334,7 +332,6 @@ private function locateClassByName(string $className): ?array $this->silenceErrors(); try { - /** @var array{string[], string, null}|null */ $result = FileReadTrapStreamWrapper::withStreamWrapperOverride( static function () use ($className): ?array { $functions = spl_autoload_functions(); diff --git a/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php b/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php index 6d12e5d09e..7e8367df3c 100644 --- a/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php +++ b/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php @@ -34,6 +34,8 @@ public function enterNode(Node $node): ?int { if ($node instanceof Namespace_) { $this->currentNamespaceNode = $node; + + return null; } if ($node instanceof Node\Stmt\ClassLike) { @@ -45,7 +47,6 @@ public function enterNode(Node $node): ?int $this->classNodes[strtolower($fullClassName)][] = new FetchedNode( $node, $this->currentNamespaceNode, - $this->fileName, new LocatedSource($this->contents, $fullClassName, $this->fileName), ); } @@ -59,7 +60,6 @@ public function enterNode(Node $node): ?int $this->functionNodes[strtolower($functionName)][] = new FetchedNode( $node, $this->currentNamespaceNode, - $this->fileName, new LocatedSource($this->contents, $functionName, $this->fileName), ); } @@ -76,7 +76,6 @@ public function enterNode(Node $node): ?int $this->constantNodes[ConstantNameHelper::normalize($const->namespacedName->toString())][] = new FetchedNode( $node, $this->currentNamespaceNode, - $this->fileName, new LocatedSource($this->contents, null, $this->fileName), ); } @@ -98,7 +97,6 @@ public function enterNode(Node $node): ?int $constantNode = new FetchedNode( $node, $this->currentNamespaceNode, - $this->fileName, new LocatedSource($this->contents, $constantName, $this->fileName), ); $this->constantNodes[ConstantNameHelper::normalize($constantName)][] = $constantNode; diff --git a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php index c930d375b7..3d123ccfe2 100644 --- a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php +++ b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php @@ -11,15 +11,19 @@ use PHPStan\File\CouldNotReadFileException; use PHPStan\File\FileReader; use PHPStan\Internal\ComposerHelper; -use function array_filter; +use PHPStan\Php\PhpVersion; use function array_key_exists; use function array_map; use function array_merge; use function array_merge_recursive; +use function array_reverse; use function count; use function dirname; +use function glob; use function is_dir; use function is_file; +use function str_contains; +use const GLOB_ONLYDIR; class ComposerJsonAndInstalledJsonSourceLocatorMaker { @@ -28,6 +32,7 @@ public function __construct( private OptimizedDirectorySourceLocatorRepository $optimizedDirectorySourceLocatorRepository, private OptimizedPsrAutoloaderLocatorFactory $optimizedPsrAutoloaderLocatorFactory, private OptimizedDirectorySourceLocatorFactory $optimizedDirectorySourceLocatorFactory, + private PhpVersion $phpVersion, ) { } @@ -67,8 +72,6 @@ public function create(string $projectInstallationPath): ?SourceLocator $this->packagePrefixPath($installedJsonDirectoryPath, $package, $vendorDirectory), ), $installed), ); - $classMapFiles = array_filter($classMapPaths, 'is_file'); - $classMapDirectories = array_filter($classMapPaths, 'is_dir'); $filePaths = array_merge( $this->prefixPaths($this->packageToFilePaths($composer), $projectInstallationPath . '/'), $dev ? $this->prefixPaths($this->packageToFilePaths($composer, 'autoload-dev'), $projectInstallationPath . '/') : [], @@ -105,16 +108,18 @@ public function create(string $projectInstallationPath): ?SourceLocator )), ); - foreach ($classMapDirectories as $classMapDirectory) { - if (!is_dir($classMapDirectory)) { + $files = []; + foreach ($classMapPaths as $classMapPath) { + if (is_dir($classMapPath)) { + $locators[] = $this->optimizedDirectorySourceLocatorRepository->getOrCreate($classMapPath); + continue; + } + if (!is_file($classMapPath)) { continue; } - $locators[] = $this->optimizedDirectorySourceLocatorRepository->getOrCreate($classMapDirectory); + $files[] = $classMapPath; } - - $files = []; - - foreach (array_merge($classMapFiles, $filePaths) as $file) { + foreach ($filePaths as $file) { if (!is_file($file)) { continue; } @@ -125,6 +130,39 @@ public function create(string $projectInstallationPath): ?SourceLocator $locators[] = $this->optimizedDirectorySourceLocatorFactory->createByFiles($files); } + $binDir = ComposerHelper::getBinDirFromComposerConfig($projectInstallationPath, $composer); + $phpunitBridgeDir = $binDir . '/.phpunit'; + if (!is_dir($vendorDirectory . '/phpunit/phpunit') && is_dir($phpunitBridgeDir)) { + // from https://github.com/composer/composer/blob/8ff237afb61b8766efa576b8ae1cc8560c8aed96/phpstan/locate-phpunit-autoloader.php + $bestDirFound = null; + $phpunitBridgeDirectories = glob($phpunitBridgeDir . '/phpunit-*', GLOB_ONLYDIR); + if ($phpunitBridgeDirectories !== false) { + foreach (array_reverse($phpunitBridgeDirectories) as $dir) { + $bestDirFound = $dir; + if ($this->phpVersion->getVersionId() >= 80100 && str_contains($dir, 'phpunit-10')) { + break; + } + if ($this->phpVersion->getVersionId() >= 80000) { + if (str_contains($dir, 'phpunit-9')) { + break; + } + continue; + } + + if (str_contains($dir, 'phpunit-8') || str_contains($dir, 'phpunit-7')) { + break; + } + } + + if ($bestDirFound !== null) { + $phpunitBridgeLocator = $this->create($bestDirFound); + if ($phpunitBridgeLocator !== null) { + $locators[] = $phpunitBridgeLocator; + } + } + } + } + return new AggregateSourceLocator($locators); } diff --git a/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php b/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php index 5af9211c14..89e33adb3d 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php +++ b/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php @@ -17,7 +17,6 @@ class FetchedNode public function __construct( private Node $node, private ?Node\Stmt\Namespace_ $namespace, - private string $fileName, private LocatedSource $locatedSource, ) { @@ -36,11 +35,6 @@ public function getNamespace(): ?Node\Stmt\Namespace_ return $this->namespace; } - public function getFileName(): string - { - return $this->fileName; - } - public function getLocatedSource(): LocatedSource { return $this->locatedSource; diff --git a/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php b/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php index c43c0866fb..0fa3440e29 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php +++ b/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php @@ -2,7 +2,6 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use PhpParser\Node; use PhpParser\NodeTraverser; use PHPStan\File\FileReader; use PHPStan\Parser\Parser; @@ -26,7 +25,6 @@ public function fetchNodes(string $fileName): FetchedNodesResult $contents = FileReader::read($fileName); try { - /** @var Node[] $ast */ $ast = $this->parser->parseFile($fileName); } catch (ParserErrorsException) { return new FetchedNodesResult([], [], []); diff --git a/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php b/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php index 10e2f78e54..4a35d07ca2 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php +++ b/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; use PHPStan\ShouldNotHappenException; +use function is_dir; use function is_file; use function stat; use function stream_resolve_include_path; @@ -259,4 +260,14 @@ public function stream_set_option($option, $arg1, $arg2): bool return false; } + public function dir_opendir(string $path, int $options): bool + { + return is_dir($path); + } + + public function dir_readdir(): string + { + return ''; + } + } diff --git a/src/Reflection/BetterReflection/SourceLocator/NewOptimizedDirectorySourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/NewOptimizedDirectorySourceLocator.php new file mode 100644 index 0000000000..557ac4848c --- /dev/null +++ b/src/Reflection/BetterReflection/SourceLocator/NewOptimizedDirectorySourceLocator.php @@ -0,0 +1,217 @@ + $classToFile + * @param array> $functionToFiles + * @param array $constantToFile + */ + public function __construct( + private FileNodesFetcher $fileNodesFetcher, + private array $classToFile, + private array $functionToFiles, + private array $constantToFile, + ) + { + } + + public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection + { + if ($identifier->isClass()) { + $className = strtolower($identifier->getName()); + $file = $this->findFileByClass($className); + if ($file === null) { + return null; + } + + $fetchedClassNodes = $this->fileNodesFetcher->fetchNodes($file)->getClassNodes(); + + if (!array_key_exists($className, $fetchedClassNodes)) { + return null; + } + + /** @var FetchedNode $fetchedClassNode */ + $fetchedClassNode = current($fetchedClassNodes[$className]); + + return $this->nodeToReflection($reflector, $fetchedClassNode); + } + + if ($identifier->isFunction()) { + $functionName = strtolower($identifier->getName()); + $files = $this->findFilesByFunction($functionName); + + $fetchedFunctionNode = null; + foreach ($files as $file) { + $fetchedFunctionNodes = $this->fileNodesFetcher->fetchNodes($file)->getFunctionNodes(); + + if (!array_key_exists($functionName, $fetchedFunctionNodes)) { + continue; + } + + /** @var FetchedNode $fetchedFunctionNode */ + $fetchedFunctionNode = current($fetchedFunctionNodes[$functionName]); + } + + if ($fetchedFunctionNode === null) { + return null; + } + + return $this->nodeToReflection($reflector, $fetchedFunctionNode); + } + + if ($identifier->isConstant()) { + $constantName = ConstantNameHelper::normalize($identifier->getName()); + $file = $this->findFileByConstant($constantName); + + if ($file === null) { + return null; + } + + $fetchedConstantNodes = $this->fileNodesFetcher->fetchNodes($file)->getConstantNodes(); + + if (!array_key_exists($constantName, $fetchedConstantNodes)) { + return null; + } + + /** @var FetchedNode $fetchedConstantNode */ + $fetchedConstantNode = current($fetchedConstantNodes[$constantName]); + + return $this->nodeToReflection( + $reflector, + $fetchedConstantNode, + $this->findConstantPositionInConstNode($fetchedConstantNode->getNode(), $constantName), + ); + } + + return null; + } + + /** + * @param FetchedNode|FetchedNode|FetchedNode $fetchedNode + */ + private function nodeToReflection(Reflector $reflector, FetchedNode $fetchedNode, ?int $positionInNode = null): Reflection + { + $nodeToReflection = new NodeToReflection(); + return $nodeToReflection->__invoke( + $reflector, + $fetchedNode->getNode(), + $fetchedNode->getLocatedSource(), + $fetchedNode->getNamespace(), + $positionInNode, + ); + } + + private function findFileByClass(string $className): ?string + { + if (!array_key_exists($className, $this->classToFile)) { + return null; + } + + return $this->classToFile[$className]; + } + + private function findFileByConstant(string $constantName): ?string + { + if (!array_key_exists($constantName, $this->constantToFile)) { + return null; + } + + return $this->constantToFile[$constantName]; + } + + /** + * @return string[] + */ + private function findFilesByFunction(string $functionName): array + { + if (!array_key_exists($functionName, $this->functionToFiles)) { + return []; + } + + return $this->functionToFiles[$functionName]; + } + + /** + * @return list + */ + public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array + { + $reflections = []; + if ($identifierType->isClass()) { + foreach ($this->classToFile as $file) { + $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); + foreach ($fetchedNodesResult->getClassNodes() as $identifierName => $fetchedClassNodes) { + foreach ($fetchedClassNodes as $fetchedClassNode) { + $reflections[$identifierName] = $this->nodeToReflection($reflector, $fetchedClassNode); + } + } + } + } elseif ($identifierType->isFunction()) { + foreach ($this->functionToFiles as $files) { + foreach ($files as $file) { + $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); + foreach ($fetchedNodesResult->getFunctionNodes() as $identifierName => $fetchedFunctionNodes) { + foreach ($fetchedFunctionNodes as $fetchedFunctionNode) { + $reflections[$identifierName] = $this->nodeToReflection($reflector, $fetchedFunctionNode); + continue 2; + } + } + } + } + } elseif ($identifierType->isConstant()) { + foreach ($this->constantToFile as $file) { + $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); + foreach ($fetchedNodesResult->getConstantNodes() as $identifierName => $fetchedConstantNodes) { + foreach ($fetchedConstantNodes as $fetchedConstantNode) { + $reflections[$identifierName] = $this->nodeToReflection( + $reflector, + $fetchedConstantNode, + $this->findConstantPositionInConstNode($fetchedConstantNode->getNode(), $identifierName), + ); + } + } + } + } + + return array_values($reflections); + } + + private function findConstantPositionInConstNode(Node\Stmt\Const_|Node\Expr\FuncCall $constantNode, string $constantName): ?int + { + if ($constantNode instanceof Node\Expr\FuncCall) { + return null; + } + + /** @var int $position */ + foreach ($constantNode->consts as $position => $const) { + if ($const->namespacedName === null) { + throw new ShouldNotHappenException(); + } + + if (ConstantNameHelper::normalize($const->namespacedName->toString()) === $constantName) { + return $position; + } + } + + throw new ShouldNotHappenException(); + } + +} diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php index 7cb83c9039..b6fc6e471c 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php @@ -16,6 +16,7 @@ use function array_values; use function count; use function current; +use function in_array; use function ltrim; use function php_strip_whitespace; use function preg_match_all; @@ -23,6 +24,9 @@ use function sprintf; use function strtolower; +/** + * @deprecated Use NewOptimizedDirectorySourceLocator + */ class OptimizedDirectorySourceLocator implements SourceLocator { @@ -276,7 +280,7 @@ private function findSymbols(string $file): array $name = $matches['name'][$i]; // skip anon classes extending/implementing - if ($name === 'extends' || $name === 'implements') { + if (in_array($name, ['extends', 'implements'], true)) { continue; } @@ -291,7 +295,7 @@ private function findSymbols(string $file): array } /** - * @return array + * @return list */ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array { diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php index ec6d1e60db..a147d1872e 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php @@ -2,35 +2,215 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use PHPStan\Cache\Cache; use PHPStan\File\FileFinder; use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\ConstantNameHelper; +use function array_key_exists; +use function count; +use function in_array; +use function ltrim; +use function php_strip_whitespace; +use function preg_match_all; +use function preg_replace; +use function sha1_file; +use function sprintf; +use function strtolower; class OptimizedDirectorySourceLocatorFactory { - public function __construct(private FileNodesFetcher $fileNodesFetcher, private FileFinder $fileFinder, private PhpVersion $phpVersion) + private PhpFileCleaner $cleaner; + + private string $extraTypes; + + public function __construct( + private FileNodesFetcher $fileNodesFetcher, + private FileFinder $fileFinder, + private PhpVersion $phpVersion, + private Cache $cache, + ) { + $this->extraTypes = $this->phpVersion->supportsEnums() ? '|enum' : ''; + $this->cleaner = new PhpFileCleaner(); } - public function createByDirectory(string $directory): OptimizedDirectorySourceLocator + public function createByDirectory(string $directory): NewOptimizedDirectorySourceLocator { - return new OptimizedDirectorySourceLocator( + $files = $this->fileFinder->findFiles([$directory])->getFiles(); + $fileHashes = []; + foreach ($files as $file) { + $hash = sha1_file($file); + if ($hash === false) { + continue; + } + $fileHashes[$file] = $hash; + } + + $cacheKey = sprintf('odsl-%s', $directory); + $variableCacheKey = 'v1'; + + /** @var array|null $cached */ + $cached = $this->cache->load($cacheKey, $variableCacheKey); + if ($cached !== null) { + foreach ($cached as $file => [$hash, $classes, $functions, $constants]) { + if (!array_key_exists($file, $fileHashes)) { + unset($cached[$file]); + continue; + } + $newHash = $fileHashes[$file]; + unset($fileHashes[$file]); + if ($hash === $newHash) { + continue; + } + + [$newClasses, $newFunctions, $newConstants] = $this->findSymbols($file); + $cached[$file] = [$newHash, $newClasses, $newFunctions, $newConstants]; + } + } else { + $cached = []; + } + + foreach ($fileHashes as $file => $newHash) { + [$newClasses, $newFunctions, $newConstants] = $this->findSymbols($file); + $cached[$file] = [$newHash, $newClasses, $newFunctions, $newConstants]; + } + $this->cache->save($cacheKey, $variableCacheKey, $cached); + + [$classToFile, $functionToFiles, $constantToFile] = $this->changeStructure($cached); + + return new NewOptimizedDirectorySourceLocator( $this->fileNodesFetcher, - $this->phpVersion, - $this->fileFinder->findFiles([$directory])->getFiles(), + $classToFile, + $functionToFiles, + $constantToFile, ); } /** * @param string[] $files */ - public function createByFiles(array $files): OptimizedDirectorySourceLocator + public function createByFiles(array $files): NewOptimizedDirectorySourceLocator { - return new OptimizedDirectorySourceLocator( + $symbols = []; + foreach ($files as $file) { + [$newClasses, $newFunctions, $newConstants] = $this->findSymbols($file); + $symbols[$file] = ['', $newClasses, $newFunctions, $newConstants]; + } + + [$classToFile, $functionToFiles, $constantToFile] = $this->changeStructure($symbols); + + return new NewOptimizedDirectorySourceLocator( $this->fileNodesFetcher, - $this->phpVersion, - $files, + $classToFile, + $functionToFiles, + $constantToFile, ); } + /** + * @param array $symbols + * @return array{array, array>, array} + */ + private function changeStructure(array $symbols): array + { + $classToFile = []; + $constantToFile = []; + $functionToFiles = []; + foreach ($symbols as $file => [, $classes, $functions, $constants]) { + foreach ($classes as $classInFile) { + $classToFile[$classInFile] = $file; + } + foreach ($functions as $functionInFile) { + if (!array_key_exists($functionInFile, $functionToFiles)) { + $functionToFiles[$functionInFile] = []; + } + $functionToFiles[$functionInFile][] = $file; + } + foreach ($constants as $constantInFile) { + $constantToFile[$constantInFile] = $file; + } + } + + return [ + $classToFile, + $functionToFiles, + $constantToFile, + ]; + } + + /** + * Inspired by Composer\Autoload\ClassMapGenerator::findClasses() + * @link https://github.com/composer/composer/blob/45d3e133a4691eccb12e9cd6f9dfd76eddc1906d/src/Composer/Autoload/ClassMapGenerator.php#L216 + * + * @return array{string[], string[], string[]} + */ + private function findSymbols(string $file): array + { + $contents = @php_strip_whitespace($file); + if ($contents === '') { + return [[], [], []]; + } + + $matchResults = (bool) preg_match_all(sprintf('{\b(?:(?:class|interface|trait|const|function%s)\s)|(?:define\s*\()}i', $this->extraTypes), $contents, $matches); + if (!$matchResults) { + return [[], [], []]; + } + + $contents = $this->cleaner->clean($contents, count($matches[0])); + + preg_match_all(sprintf('{ + (?: + \b(?])(?: + (?: (?Pclass|interface|trait%s) \s++ (?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\-]*+) ) + | (?: (?Pfunction) \s++ (?:&\s*)? (?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\-]*+) \s*+ [&\(] ) + | (?: (?Pconst) \s++ (?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\-]*+) \s*+ [^;] ) + | (?: (?:\\\)? (?Pdefine) \s*+ \( \s*+ [\'"] (?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:[\\\\]{1,2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+) ) + | (?: (?Pnamespace) (?P\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;] ) + ) + ) + }ix', $this->extraTypes), $contents, $matches); + + $classes = []; + $functions = []; + $constants = []; + $namespace = ''; + + for ($i = 0, $len = count($matches['type']); $i < $len; $i++) { + if (isset($matches['ns'][$i]) && $matches['ns'][$i] !== '') { + $namespace = preg_replace('~\s+~', '', strtolower($matches['nsname'][$i])) . '\\'; + continue; + } + + if ($matches['function'][$i] !== '') { + $functions[] = strtolower(ltrim($namespace . $matches['fname'][$i], '\\')); + continue; + } + + if ($matches['constant'][$i] !== '') { + $constants[] = ConstantNameHelper::normalize(ltrim($namespace . $matches['cname'][$i], '\\')); + } + + if ($matches['define'][$i] !== '') { + $constants[] = ConstantNameHelper::normalize($matches['dname'][$i]); + continue; + } + + $name = $matches['name'][$i]; + + // skip anon classes extending/implementing + if (in_array($name, ['extends', 'implements'], true)) { + continue; + } + + $classes[] = strtolower(ltrim($namespace . $name, '\\')); + } + + return [ + $classes, + $functions, + $constants, + ]; + } + } diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php index 89c4ec0bcc..c636424c2f 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php @@ -7,14 +7,14 @@ class OptimizedDirectorySourceLocatorRepository { - /** @var array */ + /** @var array */ private array $locators = []; public function __construct(private OptimizedDirectorySourceLocatorFactory $factory) { } - public function getOrCreate(string $directory): OptimizedDirectorySourceLocator + public function getOrCreate(string $directory): NewOptimizedDirectorySourceLocator { if (array_key_exists($directory, $this->locators)) { return $this->locators[$directory]; diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php index 6e7d1d5d3a..7abfec64da 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php @@ -54,7 +54,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): } /** - * @return array + * @return list */ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array { diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php index b97433ae6f..982a372d4a 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php @@ -169,7 +169,92 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array { - return []; + $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($this->fileName); + $nodeToReflection = new NodeToReflection(); + $reflections = []; + if ($identifierType->isClass()) { + $classNodes = $fetchedNodesResult->getClassNodes(); + + foreach ($classNodes as $classNodesArray) { + foreach ($classNodesArray as $classNode) { + $classReflection = $nodeToReflection->__invoke( + $reflector, + $classNode->getNode(), + $classNode->getLocatedSource(), + $classNode->getNamespace(), + ); + + if (!$classReflection instanceof ReflectionClass) { + throw new ShouldNotHappenException(); + } + + $reflections[] = $classReflection; + } + } + } + + if ($identifierType->isFunction()) { + $functionNodes = $fetchedNodesResult->getFunctionNodes(); + + foreach ($functionNodes as $functionNodesArray) { + foreach ($functionNodesArray as $functionNode) { + $functionReflection = $nodeToReflection->__invoke( + $reflector, + $functionNode->getNode(), + $functionNode->getLocatedSource(), + $functionNode->getNamespace(), + ); + + $reflections[] = $functionReflection; + } + } + } + + if ($identifierType->isConstant()) { + $constantNodes = $fetchedNodesResult->getConstantNodes(); + foreach ($constantNodes as $constantNodesArray) { + foreach ($constantNodesArray as $fetchedConstantNode) { + $constantNode = $fetchedConstantNode->getNode(); + + if ($constantNode instanceof Const_) { + foreach ($constantNode->consts as $constPosition => $const) { + if ($const->namespacedName === null) { + throw new ShouldNotHappenException(); + } + + $constantReflection = $nodeToReflection->__invoke( + $reflector, + $constantNode, + $fetchedConstantNode->getLocatedSource(), + $fetchedConstantNode->getNamespace(), + $constPosition, + ); + if (!$constantReflection instanceof ReflectionConstant) { + throw new ShouldNotHappenException(); + } + + $reflections[] = $constantReflection; + } + + continue; + } + + $constantReflection = $nodeToReflection->__invoke( + $reflector, + $constantNode, + $fetchedConstantNode->getLocatedSource(), + $fetchedConstantNode->getNamespace(), + ); + if (!$constantReflection instanceof ReflectionConstant) { + throw new ShouldNotHappenException(); + } + + $reflections[] = $constantReflection; + } + } + } + + return $reflections; } } diff --git a/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php b/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php index d8c2d8a789..6e76066fee 100644 --- a/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php +++ b/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php @@ -4,6 +4,7 @@ use function array_keys; use function implode; +use function in_array; use function preg_match; use function preg_quote; use function strlen; @@ -64,7 +65,7 @@ public function clean(string $contents, int $maxMatches): string continue 2; } - if ($char === '"' || $char === "'") { + if (in_array($char, ['"', "'"], true)) { if ($inDefine) { $clean .= $char . $this->consumeString($char); $inDefine = false; @@ -232,7 +233,7 @@ private function skipComment(): void private function skipToNewline(): void { while ($this->index < $this->len) { - if ($this->contents[$this->index] === "\r" || $this->contents[$this->index] === "\n") { + if (in_array($this->contents[$this->index], ["\r", "\n"], true)) { return; } $this->index += 1; @@ -284,6 +285,7 @@ private function peek(string $char): bool /** * @param string[]|null $match + * @param-out string[] $match */ private function match(string $regex, ?array &$match = null, ?int $offset = null): bool { diff --git a/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php b/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php index 273dc62e3b..3d5615b24f 100644 --- a/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php +++ b/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php @@ -4,18 +4,19 @@ use PhpParser\Parser; use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; +use PHPStan\Node\Printer\Printer; use PHPStan\Php\PhpVersion; class PhpStormStubsSourceStubberFactory { - public function __construct(private Parser $phpParser, private PhpVersion $phpVersion) + public function __construct(private Parser $phpParser, private Printer $printer, private PhpVersion $phpVersion) { } public function create(): PhpStormStubsSourceStubber { - return new PhpStormStubsSourceStubber($this->phpParser, $this->phpVersion->getVersionId()); + return new PhpStormStubsSourceStubber($this->phpParser, $this->printer, $this->phpVersion->getVersionId()); } } diff --git a/src/Reflection/CallableFunctionVariantWithPhpDocs.php b/src/Reflection/CallableFunctionVariantWithPhpDocs.php new file mode 100644 index 0000000000..f9a8c002c6 --- /dev/null +++ b/src/Reflection/CallableFunctionVariantWithPhpDocs.php @@ -0,0 +1,77 @@ + $parameters + * @param SimpleThrowPoint[] $throwPoints + * @param SimpleImpurePoint[] $impurePoints + * @param InvalidateExprNode[] $invalidateExpressions + * @param string[] $usedVariables + */ + public function __construct( + TemplateTypeMap $templateTypeMap, + ?TemplateTypeMap $resolvedTemplateTypeMap, + array $parameters, + bool $isVariadic, + Type $returnType, + Type $phpDocReturnType, + Type $nativeReturnType, + ?TemplateTypeVarianceMap $callSiteVarianceMap, + private array $throwPoints, + private TrinaryLogic $isPure, + private array $impurePoints, + private array $invalidateExpressions, + private array $usedVariables, + ) + { + parent::__construct( + $templateTypeMap, + $resolvedTemplateTypeMap, + $parameters, + $isVariadic, + $returnType, + $phpDocReturnType, + $nativeReturnType, + $callSiteVarianceMap, + ); + } + + public function getThrowPoints(): array + { + return $this->throwPoints; + } + + public function isPure(): TrinaryLogic + { + return $this->isPure; + } + + public function getImpurePoints(): array + { + return $this->impurePoints; + } + + public function getInvalidateExpressions(): array + { + return $this->invalidateExpressions; + } + + public function getUsedVariables(): array + { + return $this->usedVariables; + } + +} diff --git a/src/Reflection/Callables/CallableParametersAcceptor.php b/src/Reflection/Callables/CallableParametersAcceptor.php new file mode 100644 index 0000000000..62371a0e85 --- /dev/null +++ b/src/Reflection/Callables/CallableParametersAcceptor.php @@ -0,0 +1,37 @@ + new self($function, $variant), $variants); + } + + public function getTemplateTypeMap(): TemplateTypeMap + { + return $this->variant->getTemplateTypeMap(); + } + + public function getResolvedTemplateTypeMap(): TemplateTypeMap + { + return $this->variant->getResolvedTemplateTypeMap(); + } + + /** + * @return array + */ + public function getParameters(): array + { + return $this->variant->getParameters(); + } + + public function isVariadic(): bool + { + return $this->variant->isVariadic(); + } + + public function getReturnType(): Type + { + return $this->variant->getReturnType(); + } + + public function getPhpDocReturnType(): Type + { + return $this->variant->getPhpDocReturnType(); + } + + public function getNativeReturnType(): Type + { + return $this->variant->getNativeReturnType(); + } + + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->variant->getCallSiteVarianceMap(); + } + + public function getThrowPoints(): array + { + if ($this->throwPoints !== null) { + return $this->throwPoints; + } + + if ($this->variant instanceof CallableParametersAcceptor) { + return $this->throwPoints = $this->variant->getThrowPoints(); + } + + $returnType = $this->variant->getReturnType(); + $throwType = $this->function->getThrowType(); + if ($throwType === null) { + if ($returnType instanceof NeverType && $returnType->isExplicit()) { + $throwType = new ObjectType(Throwable::class); + } + } + + $throwPoints = []; + if ($throwType !== null) { + if (!$throwType->isVoid()->yes()) { + $throwPoints[] = SimpleThrowPoint::createExplicit($throwType, true); + } + } else { + if (!(new ObjectType(Throwable::class))->isSuperTypeOf($returnType)->yes()) { + $throwPoints[] = SimpleThrowPoint::createImplicit(); + } + } + + return $this->throwPoints = $throwPoints; + } + + public function isPure(): TrinaryLogic + { + $impurePoints = $this->getImpurePoints(); + if (count($impurePoints) === 0) { + return TrinaryLogic::createYes(); + } + + $certainCount = 0; + foreach ($impurePoints as $impurePoint) { + if (!$impurePoint->isCertain()) { + continue; + } + + $certainCount++; + } + + return $certainCount > 0 ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe(); + } + + public function getImpurePoints(): array + { + if ($this->impurePoints !== null) { + return $this->impurePoints; + } + + if ($this->variant instanceof CallableParametersAcceptor) { + return $this->impurePoints = $this->variant->getImpurePoints(); + } + + $impurePoint = SimpleImpurePoint::createFromVariant($this->function, $this->variant); + if ($impurePoint === null) { + return $this->impurePoints = []; + } + + return $this->impurePoints = [$impurePoint]; + } + + public function getInvalidateExpressions(): array + { + return []; + } + + public function getUsedVariables(): array + { + return []; + } + +} diff --git a/src/Reflection/Callables/SimpleImpurePoint.php b/src/Reflection/Callables/SimpleImpurePoint.php new file mode 100644 index 0000000000..91b807b3ad --- /dev/null +++ b/src/Reflection/Callables/SimpleImpurePoint.php @@ -0,0 +1,72 @@ +hasSideEffects()->no()) { + $certain = $function->isPure()->no(); + if ($variant !== null) { + $certain = $certain || $variant->getReturnType()->isVoid()->yes(); + } + + if ($function instanceof FunctionReflection) { + return new SimpleImpurePoint( + 'functionCall', + sprintf('call to function %s()', $function->getName()), + $certain, + ); + } + + return new SimpleImpurePoint( + 'methodCall', + sprintf('call to method %s::%s()', $function->getDeclaringClass()->getDisplayName(), $function->getName()), + $certain, + ); + } + + return null; + } + + /** + * @return ImpurePointIdentifier + */ + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getDescription(): string + { + return $this->description; + } + + public function isCertain(): bool + { + return $this->certain; + } + +} diff --git a/src/Reflection/Callables/SimpleThrowPoint.php b/src/Reflection/Callables/SimpleThrowPoint.php new file mode 100644 index 0000000000..6a32d4eacd --- /dev/null +++ b/src/Reflection/Callables/SimpleThrowPoint.php @@ -0,0 +1,45 @@ +type; + } + + public function isExplicit(): bool + { + return $this->explicit; + } + + public function canContainAnyThrowable(): bool + { + return $this->canContainAnyThrowable; + } + +} diff --git a/src/Reflection/ClassConstantReflection.php b/src/Reflection/ClassConstantReflection.php index c03c33f3a5..fd69ce8129 100644 --- a/src/Reflection/ClassConstantReflection.php +++ b/src/Reflection/ClassConstantReflection.php @@ -7,8 +7,10 @@ use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClassConstant; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; +use PHPStan\Type\TypehintHelper; use const NAN; +/** @api */ class ClassConstantReflection implements ConstantReflection { @@ -18,6 +20,7 @@ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ClassReflection $declaringClass, private ReflectionClassConstant $reflection, + private ?Type $nativeType, private ?Type $phpDocType, private ?string $deprecatedDescription, private bool $isDeprecated, @@ -59,14 +62,38 @@ public function hasPhpDocType(): bool return $this->phpDocType !== null; } + public function getPhpDocType(): ?Type + { + return $this->phpDocType; + } + + public function hasNativeType(): bool + { + return $this->nativeType !== null; + } + + public function getNativeType(): ?Type + { + return $this->nativeType; + } + public function getValueType(): Type { if ($this->valueType === null) { - if ($this->phpDocType === null) { - $this->valueType = $this->initializerExprTypeResolver->getType($this->getValueExpr(), InitializerExprContext::fromClassReflection($this->declaringClass)); - } else { - $this->valueType = $this->phpDocType; + if ($this->phpDocType !== null) { + if ($this->nativeType !== null) { + return $this->valueType = TypehintHelper::decideType( + $this->nativeType, + $this->phpDocType, + ); + } + + return $this->phpDocType; + } elseif ($this->nativeType !== null) { + return $this->nativeType; } + + $this->valueType = $this->initializerExprTypeResolver->getType($this->getValueExpr(), InitializerExprContext::fromClassReflection($this->declaringClass)); } return $this->valueType; diff --git a/src/Reflection/ClassMemberAccessAnswerer.php b/src/Reflection/ClassMemberAccessAnswerer.php index b30294e758..ed1803e786 100644 --- a/src/Reflection/ClassMemberAccessAnswerer.php +++ b/src/Reflection/ClassMemberAccessAnswerer.php @@ -6,6 +6,9 @@ interface ClassMemberAccessAnswerer { + /** + * @phpstan-assert-if-true !null $this->getClassReflection() + */ public function isInClass(): bool; public function getClassReflection(): ?ClassReflection; diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index b37da57a1a..131804e9df 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -21,11 +21,17 @@ use PHPStan\PhpDoc\Tag\MethodTag; use PHPStan\PhpDoc\Tag\MixinTag; use PHPStan\PhpDoc\Tag\PropertyTag; +use PHPStan\PhpDoc\Tag\RequireExtendsTag; +use PHPStan\PhpDoc\Tag\RequireImplementsTag; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDoc\Tag\TypeAliasImportTag; use PHPStan\PhpDoc\Tag\TypeAliasTag; use PHPStan\Reflection\Php\PhpClassReflectionExtension; use PHPStan\Reflection\Php\PhpPropertyReflection; +use PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension; +use PHPStan\Reflection\RequireExtension\RequireExtendsMethodsClassReflectionExtension; +use PHPStan\Reflection\RequireExtension\RequireExtendsPropertiesClassReflectionExtension; +use PHPStan\Reflection\SignatureMap\SignatureMapProvider; use PHPStan\ShouldNotHappenException; use PHPStan\Type\CircularTypeAliasDefinitionException; use PHPStan\Type\Constant\ConstantIntegerType; @@ -36,12 +42,15 @@ use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeScope; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; +use PHPStan\Type\Generic\TypeProjectionHelper; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeAlias; use PHPStan\Type\TypehintHelper; use PHPStan\Type\VerbosityLevel; use ReflectionException; -use stdClass; use function array_diff; use function array_filter; use function array_key_exists; @@ -70,7 +79,7 @@ class ClassReflection /** @var PropertyReflection[] */ private array $properties = []; - /** @var ConstantReflection[] */ + /** @var ClassConstantReflection[] */ private array $constants = []; /** @var int[]|null */ @@ -94,6 +103,10 @@ class ClassReflection private ?TemplateTypeMap $activeTemplateTypeMap = null; + private ?TemplateTypeVarianceMap $defaultCallSiteVarianceMap = null; + + private ?TemplateTypeVarianceMap $callSiteVarianceMap = null; + /** @var array|null */ private ?array $ancestors = null; @@ -106,6 +119,8 @@ class ClassReflection private string|false|null $reflectionDocComment = false; + private false|ResolvedPhpDocBlock $resolvedPhpDocBlock = false; + /** @var ClassReflection[]|null */ private ?array $cachedInterfaces = null; @@ -120,6 +135,8 @@ class ClassReflection /** * @param PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions * @param MethodsClassReflectionExtension[] $methodsClassReflectionExtensions + * @param AllowedSubTypesClassReflectionExtension[] $allowedSubTypesClassReflectionExtensions + * @param string[] $universalObjectCratesClasses */ public function __construct( private ReflectionProvider $reflectionProvider, @@ -128,14 +145,20 @@ public function __construct( private StubPhpDocProvider $stubPhpDocProvider, private PhpDocInheritanceResolver $phpDocInheritanceResolver, private PhpVersion $phpVersion, + private SignatureMapProvider $signatureMapProvider, private array $propertiesClassReflectionExtensions, private array $methodsClassReflectionExtensions, + private array $allowedSubTypesClassReflectionExtensions, + private RequireExtendsPropertiesClassReflectionExtension $requireExtendsPropertiesClassReflectionExtension, + private RequireExtendsMethodsClassReflectionExtension $requireExtendsMethodsClassReflectionExtension, private string $displayName, private ReflectionClass|ReflectionEnum $reflection, private ?string $anonymousFilename, private ?TemplateTypeMap $resolvedTemplateTypeMap, private ?ResolvedPhpDocBlock $stubPhpDocBlock, + private array $universalObjectCratesClasses, private ?string $extraCacheKey = null, + private ?TemplateTypeVarianceMap $resolvedCallSiteVarianceMap = null, ) { } @@ -195,7 +218,8 @@ public function getParentClass(): ?ClassReflection $extendedType = TemplateTypeHelper::resolveTemplateTypes( $extendedType, $this->getPossiblyIncompleteActiveTemplateTypeMap(), - true, + $this->getCallSiteVarianceMap(), + TemplateTypeVariance::createStatic(), ); } @@ -228,17 +252,26 @@ public function getName(): string public function getDisplayName(bool $withTemplateTypes = true): string { - $name = $this->displayName; - if ( $withTemplateTypes === false || $this->resolvedTemplateTypeMap === null || count($this->resolvedTemplateTypeMap->getTypes()) === 0 ) { - return $name; + return $this->displayName; } - return $name . '<' . implode(',', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::typeOnly()), $this->getActiveTemplateTypeMap()->getTypes())) . '>'; + $templateTypes = []; + $variances = $this->getCallSiteVarianceMap()->getVariances(); + foreach ($this->getActiveTemplateTypeMap()->getTypes() as $name => $templateType) { + $variance = $variances[$name] ?? null; + if ($variance === null) { + continue; + } + + $templateTypes[] = TypeProjectionHelper::describe($templateType, $variance, VerbosityLevel::typeOnly()); + } + + return $this->displayName . '<' . implode(',', $templateTypes) . '>'; } public function getCacheKey(): string @@ -251,7 +284,18 @@ public function getCacheKey(): string $cacheKey = $this->displayName; if ($this->resolvedTemplateTypeMap !== null) { - $cacheKey .= '<' . implode(',', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::cache()), $this->resolvedTemplateTypeMap->getTypes())) . '>'; + $templateTypes = []; + $variances = $this->getCallSiteVarianceMap()->getVariances(); + foreach ($this->getActiveTemplateTypeMap()->getTypes() as $name => $templateType) { + $variance = $variances[$name] ?? null; + if ($variance === null) { + continue; + } + + $templateTypes[] = TypeProjectionHelper::describe($templateType, $variance, VerbosityLevel::cache()); + } + + $cacheKey .= '<' . implode(',', $templateTypes) . '>'; } if ($this->extraCacheKey !== null) { @@ -342,6 +386,10 @@ private function collectTraits(ReflectionClass|ReflectionEnum $class): array public function allowsDynamicProperties(): bool { + if ($this->isEnum()) { + return false; + } + if (!$this->phpVersion->deprecatesDynamicProperties()) { return true; } @@ -350,7 +398,11 @@ public function allowsDynamicProperties(): bool return false; } - if ($this->is(stdClass::class)) { + if (UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( + $this->reflectionProvider, + $this->universalObjectCratesClasses, + $this, + )) { return true; } @@ -370,7 +422,30 @@ private function allowsDynamicPropertiesExtensions(): bool return true; } - return $this->hasNativeMethod('__get') || $this->hasNativeMethod('__set') || $this->hasNativeMethod('__isset'); + $hasMagicMethod = $this->hasNativeMethod('__get') || $this->hasNativeMethod('__set') || $this->hasNativeMethod('__isset'); + if ($hasMagicMethod) { + return true; + } + + foreach ($this->getRequireExtendsTags() as $extendsTag) { + $type = $extendsTag->getType(); + if (!$type instanceof ObjectType) { + continue; + } + + $reflection = $type->getClassReflection(); + if ($reflection === null) { + continue; + } + + if (!$reflection->allowsDynamicPropertiesExtensions()) { + continue; + } + + return true; + } + + return false; } public function hasProperty(string $propertyName): bool @@ -381,13 +456,17 @@ public function hasProperty(string $propertyName): bool foreach ($this->propertiesClassReflectionExtensions as $i => $extension) { if ($i > 0 && !$this->allowsDynamicPropertiesExtensions()) { - continue; + break; } if ($extension->hasProperty($this, $propertyName)) { return true; } } + if ($this->requireExtendsPropertiesClassReflectionExtension->hasProperty($this, $propertyName)) { + return true; + } + return false; } @@ -399,6 +478,10 @@ public function hasMethod(string $methodName): bool } } + if ($this->requireExtendsMethodsClassReflectionExtension->hasMethod($this, $methodName)) { + return true; + } + return false; } @@ -408,6 +491,7 @@ public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): if ($scope->isInClass()) { $key = sprintf('%s-%s', $key, $scope->getClassReflection()->getCacheKey()); } + if (!isset($this->methods[$key])) { foreach ($this->methodsClassReflectionExtensions as $extension) { if (!$extension->hasMethod($this, $methodName)) { @@ -422,6 +506,13 @@ public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): } } + if (!isset($this->methods[$key])) { + if ($this->requireExtendsMethodsClassReflectionExtension->hasMethod($this, $methodName)) { + $method = $this->requireExtendsMethodsClassReflectionExtension->getMethod($this, $methodName); + $this->methods[$key] = $method; + } + } + if (!isset($this->methods[$key])) { throw new MissingMethodFromReflectionException($this->getName(), $methodName); } @@ -530,11 +621,13 @@ public function getProperty(string $propertyName, ClassMemberAccessAnswerer $sco if ($scope->isInClass()) { $key = sprintf('%s-%s', $key, $scope->getClassReflection()->getCacheKey()); } + if (!isset($this->properties[$key])) { foreach ($this->propertiesClassReflectionExtensions as $i => $extension) { if ($i > 0 && !$this->allowsDynamicPropertiesExtensions()) { - continue; + break; } + if (!$extension->hasProperty($this, $propertyName)) { continue; } @@ -547,6 +640,13 @@ public function getProperty(string $propertyName, ClassMemberAccessAnswerer $sco } } + if (!isset($this->properties[$key])) { + if ($this->requireExtendsPropertiesClassReflectionExtension->hasProperty($this, $propertyName)) { + $property = $this->requireExtendsPropertiesClassReflectionExtension->getProperty($this, $propertyName); + $this->properties[$key] = $property; + } + } + if (!isset($this->properties[$key])) { throw new MissingPropertyFromReflectionException($this->getName(), $propertyName); } @@ -583,9 +683,28 @@ public function isTrait(): bool return $this->reflection->isTrait(); } + /** + * @phpstan-assert-if-true ReflectionEnum $this->reflection + */ public function isEnum(): bool { - return $this->reflection->isEnum(); + return $this->reflection instanceof ReflectionEnum && $this->reflection->isEnum(); + } + + /** + * @return 'Interface'|'Trait'|'Enum'|'Class' + */ + public function getClassTypeDescription(): string + { + if ($this->isInterface()) { + return 'Interface'; + } elseif ($this->isTrait()) { + return 'Trait'; + } elseif ($this->isEnum()) { + return 'Enum'; + } + + return 'Class'; } public function isReadOnly(): bool @@ -612,12 +731,7 @@ public function getBackedEnumType(): ?Type return null; } - $reflectionType = $this->reflection->getBackingType(); - if ($reflectionType === null) { - return null; - } - - return TypehintHelper::decideTypeFromReflection($reflectionType); + return TypehintHelper::decideTypeFromReflection($this->reflection->getBackingType()); } public function hasEnumCase(string $name): bool @@ -626,10 +740,6 @@ public function hasEnumCase(string $name): bool return false; } - if (!$this->reflection instanceof ReflectionEnum) { - return false; - } - return $this->reflection->hasCase($name); } @@ -638,15 +748,16 @@ public function hasEnumCase(string $name): bool */ public function getEnumCases(): array { - if (!$this->reflection instanceof ReflectionEnum) { + if (!$this->isEnum()) { throw new ShouldNotHappenException(); } $cases = []; + $initializerExprContext = InitializerExprContext::fromClassReflection($this); foreach ($this->reflection->getCases() as $case) { $valueType = null; if ($case instanceof ReflectionEnumBackedCase) { - $valueType = $this->initializerExprTypeResolver->getType($case->getValueExpression(), InitializerExprContext::fromClassReflection($this)); + $valueType = $this->initializerExprTypeResolver->getType($case->getValueExpression(), $initializerExprContext); } /** @var string $caseName */ $caseName = $case->getName(); @@ -823,6 +934,8 @@ public function getImmediateInterfaces(): array $implementedType = TemplateTypeHelper::resolveTemplateTypes( $implementedType, $this->getPossiblyIncompleteActiveTemplateTypeMap(), + $this->getCallSiteVarianceMap(), + TemplateTypeVariance::createStatic(), true, ); } @@ -881,15 +994,15 @@ public function getTraits(bool $recursive = false): array } /** - * @return string[] + * @return list */ public function getParentClassesNames(): array { $parentNames = []; - $currentClassReflection = $this; - while ($currentClassReflection->getParentClass() !== null) { - $parentNames[] = $currentClassReflection->getParentClass()->getName(); - $currentClassReflection = $currentClassReflection->getParentClass(); + $parentClass = $this->getParentClass(); + while ($parentClass !== null) { + $parentNames[] = $parentClass->getName(); + $parentClass = $parentClass->getParentClass(); } return $parentNames; @@ -909,7 +1022,7 @@ public function hasConstant(string $name): bool return $this->reflectionProvider->hasClass($reflectionConstant->getDeclaringClass()->getName()); } - public function getConstant(string $name): ConstantReflection + public function getConstant(string $name): ClassConstantReflection { if (!isset($this->constants[$name])) { $reflectionConstant = $this->getNativeReflection()->getReflectionConstant($name); @@ -945,10 +1058,18 @@ public function getConstant(string $name): ConstantReflection $phpDocType = $varTags[0]->getType(); } + $nativeType = null; + if ($reflectionConstant->getType() !== null) { + $nativeType = TypehintHelper::decideTypeFromReflection($reflectionConstant->getType(), null, $declaringClass); + } elseif ($this->signatureMapProvider->hasClassConstantMetadata($declaringClass->getName(), $name)) { + $nativeType = $this->signatureMapProvider->getClassConstantMetadata($declaringClass->getName(), $name)['nativeType']; + } + $this->constants[$name] = new ClassConstantReflection( $this->initializerExprTypeResolver, $declaringClass, $reflectionConstant, + $nativeType, $phpDocType, $deprecatedDescription, $isDeprecated, @@ -969,7 +1090,7 @@ public function hasTraitUse(string $traitName): bool private function getTraitNames(): array { $class = $this->reflection; - $traitNames = $class->getTraitNames(); + $traitNames = array_map(static fn (ReflectionClass $class) => $class->getName(), $this->collectTraits($class)); while ($class->getParentClass() !== false) { $traitNames = array_values(array_unique(array_merge($traitNames, $class->getParentClass()->getTraitNames()))); $class = $class->getParentClass(); @@ -1143,6 +1264,9 @@ private function findAttributeFlags(): ?int $arguments[] = new Arg($expression, false, false, [], is_int($i) ? null : new Identifier($i)); } + if (!$attributeClass->hasConstructor()) { + return null; + } $attributeConstructor = $attributeClass->getConstructor(); $attributeConstructorVariant = ParametersAcceptorSelector::selectSingle($attributeConstructor->getVariants()); @@ -1229,6 +1353,32 @@ public function getPossiblyIncompleteActiveTemplateTypeMap(): TemplateTypeMap return $this->resolvedTemplateTypeMap ?? $this->getTemplateTypeMap(); } + private function getDefaultCallSiteVarianceMap(): TemplateTypeVarianceMap + { + if ($this->defaultCallSiteVarianceMap !== null) { + return $this->defaultCallSiteVarianceMap; + } + + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + $this->defaultCallSiteVarianceMap = TemplateTypeVarianceMap::createEmpty(); + return $this->defaultCallSiteVarianceMap; + } + + $map = []; + foreach ($this->getTemplateTags() as $templateTag) { + $map[$templateTag->getName()] = TemplateTypeVariance::createInvariant(); + } + + $this->defaultCallSiteVarianceMap = new TemplateTypeVarianceMap($map); + return $this->defaultCallSiteVarianceMap; + } + + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->callSiteVarianceMap ??= $this->resolvedCallSiteVarianceMap ?? $this->getDefaultCallSiteVarianceMap(); + } + public function isGeneric(): bool { if ($this->isGeneric === null) { @@ -1255,13 +1405,33 @@ public function typeMapFromList(array $types): TemplateTypeMap $map = []; $i = 0; foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { - $map[$tag->getName()] = $types[$i] ?? new ErrorType(); + $map[$tag->getName()] = $types[$i] ?? $tag->getBound(); $i++; } return new TemplateTypeMap($map); } + /** + * @param array $variances + */ + public function varianceMapFromList(array $variances): TemplateTypeVarianceMap + { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + return new TemplateTypeVarianceMap([]); + } + + $map = []; + $i = 0; + foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { + $map[$tag->getName()] = $variances[$i] ?? TemplateTypeVariance::createInvariant(); + $i++; + } + + return new TemplateTypeVarianceMap($map); + } + /** @return array */ public function typeMapToList(TemplateTypeMap $typeMap): array { @@ -1278,6 +1448,22 @@ public function typeMapToList(TemplateTypeMap $typeMap): array return $list; } + /** @return array */ + public function varianceMapToList(TemplateTypeVarianceMap $varianceMap): array + { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + return []; + } + + $list = []; + foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { + $list[] = $varianceMap->getVariance($tag->getName()) ?? TemplateTypeVariance::createInvariant(); + } + + return $list; + } + /** * @param array $types */ @@ -1290,13 +1476,49 @@ public function withTypes(array $types): self $this->stubPhpDocProvider, $this->phpDocInheritanceResolver, $this->phpVersion, + $this->signatureMapProvider, $this->propertiesClassReflectionExtensions, $this->methodsClassReflectionExtensions, + $this->allowedSubTypesClassReflectionExtensions, + $this->requireExtendsPropertiesClassReflectionExtension, + $this->requireExtendsMethodsClassReflectionExtension, $this->displayName, $this->reflection, $this->anonymousFilename, $this->typeMapFromList($types), $this->stubPhpDocBlock, + $this->universalObjectCratesClasses, + null, + $this->resolvedCallSiteVarianceMap, + ); + } + + /** + * @param array $variances + */ + public function withVariances(array $variances): self + { + return new self( + $this->reflectionProvider, + $this->initializerExprTypeResolver, + $this->fileTypeMapper, + $this->stubPhpDocProvider, + $this->phpDocInheritanceResolver, + $this->phpVersion, + $this->signatureMapProvider, + $this->propertiesClassReflectionExtensions, + $this->methodsClassReflectionExtensions, + $this->allowedSubTypesClassReflectionExtensions, + $this->requireExtendsPropertiesClassReflectionExtension, + $this->requireExtendsMethodsClassReflectionExtension, + $this->displayName, + $this->reflection, + $this->anonymousFilename, + $this->resolvedTemplateTypeMap, + $this->stubPhpDocBlock, + $this->universalObjectCratesClasses, + null, + $this->varianceMapFromList($variances), ); } @@ -1316,7 +1538,11 @@ public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock return null; } - return $this->fileTypeMapper->getResolvedPhpDoc($fileName, $this->getName(), null, null, $this->reflectionDocComment); + if ($this->resolvedPhpDocBlock !== false) { + return $this->resolvedPhpDocBlock; + } + + return $this->resolvedPhpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc($fileName, $this->getName(), null, null, $this->reflectionDocComment); } private function getFirstExtendsTag(): ?ExtendsTag @@ -1444,6 +1670,32 @@ public function getMixinTags(): array return $resolvedPhpDoc->getMixinTags(); } + /** + * @return array + */ + public function getRequireExtendsTags(): array + { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + return []; + } + + return $resolvedPhpDoc->getRequireExtendsTags(); + } + + /** + * @return array + */ + public function getRequireImplementsTags(): array + { + $resolvedPhpDoc = $this->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + return []; + } + + return $resolvedPhpDoc->getRequireImplementsTags(); + } + /** * @return array */ @@ -1485,10 +1737,26 @@ public function getResolvedMixinTypes(): array $types[] = TemplateTypeHelper::resolveTemplateTypes( $mixinTag->getType(), $this->getActiveTemplateTypeMap(), + $this->getCallSiteVarianceMap(), + TemplateTypeVariance::createStatic(), ); } return $types; } + /** + * @return array|null + */ + public function getAllowedSubTypes(): ?array + { + foreach ($this->allowedSubTypesClassReflectionExtensions as $allowedSubTypesClassReflectionExtension) { + if ($allowedSubTypesClassReflectionExtension->supports($this)) { + return $allowedSubTypesClassReflectionExtension->getAllowedSubTypes($this); + } + } + + return null; + } + } diff --git a/src/Reflection/ClassReflectionExtensionRegistry.php b/src/Reflection/ClassReflectionExtensionRegistry.php index e18880e0be..6ba9c4d28a 100644 --- a/src/Reflection/ClassReflectionExtensionRegistry.php +++ b/src/Reflection/ClassReflectionExtensionRegistry.php @@ -3,6 +3,8 @@ namespace PHPStan\Reflection; use PHPStan\Broker\Broker; +use PHPStan\Reflection\RequireExtension\RequireExtendsMethodsClassReflectionExtension; +use PHPStan\Reflection\RequireExtension\RequireExtendsPropertiesClassReflectionExtension; use function array_merge; class ClassReflectionExtensionRegistry @@ -11,14 +13,18 @@ class ClassReflectionExtensionRegistry /** * @param PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions * @param MethodsClassReflectionExtension[] $methodsClassReflectionExtensions + * @param AllowedSubTypesClassReflectionExtension[] $allowedSubTypesClassReflectionExtensions */ public function __construct( Broker $broker, private array $propertiesClassReflectionExtensions, private array $methodsClassReflectionExtensions, + private array $allowedSubTypesClassReflectionExtensions, + private RequireExtendsPropertiesClassReflectionExtension $requireExtendsPropertiesClassReflectionExtension, + private RequireExtendsMethodsClassReflectionExtension $requireExtendsMethodsClassReflectionExtension, ) { - foreach (array_merge($propertiesClassReflectionExtensions, $methodsClassReflectionExtensions) as $extension) { + foreach (array_merge($propertiesClassReflectionExtensions, $methodsClassReflectionExtensions, $allowedSubTypesClassReflectionExtensions) as $extension) { if (!($extension instanceof BrokerAwareExtension)) { continue; } @@ -43,4 +49,22 @@ public function getMethodsClassReflectionExtensions(): array return $this->methodsClassReflectionExtensions; } + /** + * @return AllowedSubTypesClassReflectionExtension[] + */ + public function getAllowedSubTypesClassReflectionExtensions(): array + { + return $this->allowedSubTypesClassReflectionExtensions; + } + + public function getRequireExtendsPropertyClassReflectionExtension(): RequireExtendsPropertiesClassReflectionExtension + { + return $this->requireExtendsPropertiesClassReflectionExtension; + } + + public function getRequireExtendsMethodsClassReflectionExtension(): RequireExtendsMethodsClassReflectionExtension + { + return $this->requireExtendsMethodsClassReflectionExtension; + } + } diff --git a/src/Reflection/Constant/RuntimeConstantReflection.php b/src/Reflection/Constant/RuntimeConstantReflection.php index 341ae68cc4..7bc10879f4 100644 --- a/src/Reflection/Constant/RuntimeConstantReflection.php +++ b/src/Reflection/Constant/RuntimeConstantReflection.php @@ -13,6 +13,8 @@ public function __construct( private string $name, private Type $valueType, private ?string $fileName, + private TrinaryLogic $isDeprecated, + private ?string $deprecatedDescription, ) { } @@ -34,12 +36,12 @@ public function getFileName(): ?string public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::createNo(); + return $this->isDeprecated; } public function getDeprecatedDescription(): ?string { - return null; + return $this->deprecatedDescription; } public function isInternal(): TrinaryLogic diff --git a/src/Reflection/ConstantNameHelper.php b/src/Reflection/ConstantNameHelper.php index bc3d9e7990..237c29c166 100644 --- a/src/Reflection/ConstantNameHelper.php +++ b/src/Reflection/ConstantNameHelper.php @@ -7,7 +7,7 @@ use function end; use function explode; use function implode; -use function strpos; +use function str_contains; use function strtolower; class ConstantNameHelper @@ -15,7 +15,7 @@ class ConstantNameHelper public static function normalize(string $name): string { - if (strpos($name, '\\') === false) { + if (!str_contains($name, '\\')) { return $name; } diff --git a/src/Reflection/ConstructorsHelper.php b/src/Reflection/ConstructorsHelper.php index eb71f71074..6a721f2dd7 100644 --- a/src/Reflection/ConstructorsHelper.php +++ b/src/Reflection/ConstructorsHelper.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection; +use PHPStan\DependencyInjection\Container; use ReflectionException; use function array_key_exists; use function explode; @@ -9,13 +10,14 @@ final class ConstructorsHelper { - /** @var array */ + /** @var array> */ private array $additionalConstructorsCache = []; /** * @param list $additionalConstructors */ public function __construct( + private Container $container, private array $additionalConstructors, ) { @@ -34,6 +36,15 @@ public function getConstructors(ClassReflection $classReflection): array $constructors[] = $classReflection->getConstructor()->getName(); } + /** @var AdditionalConstructorsExtension[] $extensions */ + $extensions = $this->container->getServicesByTag(AdditionalConstructorsExtension::EXTENSION_TAG); + foreach ($extensions as $extension) { + $extensionConstructors = $extension->getAdditionalConstructors($classReflection); + foreach ($extensionConstructors as $extensionConstructor) { + $constructors[] = $extensionConstructor; + } + } + $nativeReflection = $classReflection->getNativeReflection(); foreach ($this->additionalConstructors as $additionalConstructor) { [$className, $methodName] = explode('::', $additionalConstructor); diff --git a/src/Reflection/Dummy/ChangedTypeMethodReflection.php b/src/Reflection/Dummy/ChangedTypeMethodReflection.php index 4ccba18790..4fc7daca3d 100644 --- a/src/Reflection/Dummy/ChangedTypeMethodReflection.php +++ b/src/Reflection/Dummy/ChangedTypeMethodReflection.php @@ -2,20 +2,23 @@ namespace PHPStan\Reflection\Dummy; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; +use function is_bool; -class ChangedTypeMethodReflection implements MethodReflection +class ChangedTypeMethodReflection implements ExtendedMethodReflection { /** - * @param ParametersAcceptor[] $variants + * @param ParametersAcceptorWithPhpDocs[] $variants + * @param ParametersAcceptorWithPhpDocs[]|null $namedArgumentsVariants */ - public function __construct(private ClassReflection $declaringClass, private MethodReflection $reflection, private array $variants) + public function __construct(private ClassReflection $declaringClass, private ExtendedMethodReflection $reflection, private array $variants, private ?array $namedArgumentsVariants) { } @@ -59,6 +62,11 @@ public function getVariants(): array return $this->variants; } + public function getNamedArgumentsVariants(): ?array + { + return $this->namedArgumentsVariants; + } + public function isDeprecated(): TrinaryLogic { return $this->reflection->isDeprecated(); @@ -74,6 +82,11 @@ public function isFinal(): TrinaryLogic return $this->reflection->isFinal(); } + public function isFinalByKeyword(): TrinaryLogic + { + return $this->reflection->isFinalByKeyword(); + } + public function isInternal(): TrinaryLogic { return $this->reflection->isInternal(); @@ -89,4 +102,34 @@ public function hasSideEffects(): TrinaryLogic return $this->reflection->hasSideEffects(); } + public function getAsserts(): Assertions + { + return $this->reflection->getAsserts(); + } + + public function getSelfOutType(): ?Type + { + return $this->reflection->getSelfOutType(); + } + + public function returnsByReference(): TrinaryLogic + { + return $this->reflection->returnsByReference(); + } + + public function isAbstract(): TrinaryLogic + { + $abstract = $this->reflection->isAbstract(); + if (is_bool($abstract)) { + return TrinaryLogic::createFromBoolean($abstract); + } + + return $abstract; + } + + public function isPure(): TrinaryLogic + { + return $this->reflection->isPure(); + } + } diff --git a/src/Reflection/Dummy/DummyConstructorReflection.php b/src/Reflection/Dummy/DummyConstructorReflection.php index 1bcd01f6af..b3cdde365f 100644 --- a/src/Reflection/Dummy/DummyConstructorReflection.php +++ b/src/Reflection/Dummy/DummyConstructorReflection.php @@ -2,16 +2,18 @@ namespace PHPStan\Reflection\Dummy; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\FunctionVariant; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\FunctionVariantWithPhpDocs; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\VoidType; -class DummyConstructorReflection implements MethodReflection +class DummyConstructorReflection implements ExtendedMethodReflection { public function __construct(private ClassReflection $declaringClass) @@ -51,16 +53,24 @@ public function getPrototype(): ClassMemberReflection public function getVariants(): array { return [ - new FunctionVariant( + new FunctionVariantWithPhpDocs( TemplateTypeMap::createEmpty(), null, [], false, new VoidType(), + new MixedType(), + new MixedType(), + null, ), ]; } + public function getNamedArgumentsVariants(): ?array + { + return null; + } + public function isDeprecated(): TrinaryLogic { return TrinaryLogic::createMaybe(); @@ -96,4 +106,34 @@ public function getDocComment(): ?string return null; } + public function getAsserts(): Assertions + { + return Assertions::createEmpty(); + } + + public function getSelfOutType(): ?Type + { + return null; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + } diff --git a/src/Reflection/Dummy/DummyMethodReflection.php b/src/Reflection/Dummy/DummyMethodReflection.php index 77b06eb5ae..806c24c815 100644 --- a/src/Reflection/Dummy/DummyMethodReflection.php +++ b/src/Reflection/Dummy/DummyMethodReflection.php @@ -2,17 +2,17 @@ namespace PHPStan\Reflection\Dummy; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use stdClass; -class DummyMethodReflection implements MethodReflection +class DummyMethodReflection implements ExtendedMethodReflection { public function __construct(private string $name) @@ -51,9 +51,6 @@ public function getPrototype(): ClassMemberReflection return $this; } - /** - * @return ParametersAcceptor[] - */ public function getVariants(): array { return [ @@ -61,6 +58,11 @@ public function getVariants(): array ]; } + public function getNamedArgumentsVariants(): ?array + { + return null; + } + public function isDeprecated(): TrinaryLogic { return TrinaryLogic::createMaybe(); @@ -76,6 +78,11 @@ public function isFinal(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isInternal(): TrinaryLogic { return TrinaryLogic::createMaybe(); @@ -96,4 +103,29 @@ public function getDocComment(): ?string return null; } + public function getAsserts(): Assertions + { + return Assertions::createEmpty(); + } + + public function getSelfOutType(): ?Type + { + return null; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + } diff --git a/src/Reflection/ExtendedMethodReflection.php b/src/Reflection/ExtendedMethodReflection.php index a3c2667f20..0ff0f8f2de 100644 --- a/src/Reflection/ExtendedMethodReflection.php +++ b/src/Reflection/ExtendedMethodReflection.php @@ -2,18 +2,53 @@ namespace PHPStan\Reflection; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Type; + /** * The purpose of this interface is to be able to * answer more questions about methods * without breaking backward compatibility * with existing MethodsClassReflectionExtension. * - * Developers are meant to only use the MethodReflection + * Developers are meant to only implement MethodReflection * and its methods in their code. * - * Methods on ExtendedMethodReflection are subject to change. + * New methods on ExtendedMethodReflection will be added + * in minor versions. + * + * @api */ interface ExtendedMethodReflection extends MethodReflection { + /** + * @return ParametersAcceptorWithPhpDocs[] + */ + public function getVariants(): array; + + /** + * @return ParametersAcceptorWithPhpDocs[]|null + */ + public function getNamedArgumentsVariants(): ?array; + + public function getAsserts(): Assertions; + + public function getSelfOutType(): ?Type; + + public function returnsByReference(): TrinaryLogic; + + public function isFinalByKeyword(): TrinaryLogic; + + public function isAbstract(): TrinaryLogic|bool; + + /** + * This indicates whether the method has phpstan-pure + * or phpstan-impure annotation above it. + * + * In most cases asking hasSideEffects() is much more practical + * as it also accounts for void return type (method being always impure). + */ + public function isPure(): TrinaryLogic; + } diff --git a/src/Reflection/FunctionReflection.php b/src/Reflection/FunctionReflection.php index 8ef8a806b2..91afcbaadb 100644 --- a/src/Reflection/FunctionReflection.php +++ b/src/Reflection/FunctionReflection.php @@ -14,10 +14,15 @@ public function getName(): string; public function getFileName(): ?string; /** - * @return ParametersAcceptor[] + * @return ParametersAcceptorWithPhpDocs[] */ public function getVariants(): array; + /** + * @return ParametersAcceptorWithPhpDocs[]|null + */ + public function getNamedArgumentsVariants(): ?array; + public function isDeprecated(): TrinaryLogic; public function getDeprecatedDescription(): ?string; @@ -32,4 +37,19 @@ public function hasSideEffects(): TrinaryLogic; public function isBuiltin(): bool; + public function getAsserts(): Assertions; + + public function getDocComment(): ?string; + + public function returnsByReference(): TrinaryLogic; + + /** + * This indicates whether the function has phpstan-pure + * or phpstan-impure annotation above it. + * + * In most cases asking hasSideEffects() is much more practical + * as it also accounts for void return type (method being always impure). + */ + public function isPure(): TrinaryLogic; + } diff --git a/src/Reflection/FunctionReflectionFactory.php b/src/Reflection/FunctionReflectionFactory.php index 21403ecc7b..67684abdab 100644 --- a/src/Reflection/FunctionReflectionFactory.php +++ b/src/Reflection/FunctionReflectionFactory.php @@ -11,7 +11,10 @@ interface FunctionReflectionFactory { /** - * @param Type[] $phpDocParameterTypes + * @param array $phpDocParameterTypes + * @param array $phpDocParameterOutTypes + * @param array $phpDocParameterImmediatelyInvokedCallable + * @param array $phpDocParameterClosureThisTypes */ public function create( ReflectionFunction $reflection, @@ -24,7 +27,12 @@ public function create( bool $isInternal, bool $isFinal, ?string $filename, - ?bool $isPure = null, + ?bool $isPure, + Assertions $asserts, + ?string $phpDocComment, + array $phpDocParameterOutTypes, + array $phpDocParameterImmediatelyInvokedCallable, + array $phpDocParameterClosureThisTypes, ): PhpFunctionReflection; } diff --git a/src/Reflection/FunctionVariant.php b/src/Reflection/FunctionVariant.php index 9936ae9244..3c5947ac5c 100644 --- a/src/Reflection/FunctionVariant.php +++ b/src/Reflection/FunctionVariant.php @@ -3,12 +3,15 @@ namespace PHPStan\Reflection; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; /** @api */ class FunctionVariant implements ParametersAcceptor { + private TemplateTypeVarianceMap $callSiteVarianceMap; + /** * @api * @param array $parameters @@ -19,8 +22,10 @@ public function __construct( private array $parameters, private bool $isVariadic, private Type $returnType, + ?TemplateTypeVarianceMap $callSiteVarianceMap = null, ) { + $this->callSiteVarianceMap = $callSiteVarianceMap ?? TemplateTypeVarianceMap::createEmpty(); } public function getTemplateTypeMap(): TemplateTypeMap @@ -33,6 +38,11 @@ public function getResolvedTemplateTypeMap(): TemplateTypeMap return $this->resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty(); } + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->callSiteVarianceMap; + } + /** * @return array */ diff --git a/src/Reflection/FunctionVariantWithPhpDocs.php b/src/Reflection/FunctionVariantWithPhpDocs.php index aae15cb864..2efd1c1a97 100644 --- a/src/Reflection/FunctionVariantWithPhpDocs.php +++ b/src/Reflection/FunctionVariantWithPhpDocs.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; /** @api */ @@ -21,6 +22,7 @@ public function __construct( Type $returnType, private Type $phpDocReturnType, private Type $nativeReturnType, + ?TemplateTypeVarianceMap $callSiteVarianceMap = null, ) { parent::__construct( @@ -29,6 +31,7 @@ public function __construct( $parameters, $isVariadic, $returnType, + $callSiteVarianceMap, ); } @@ -37,7 +40,7 @@ public function __construct( */ public function getParameters(): array { - /** @var ParameterReflectionWithPhpDocs[] $parameters */ + /** @var array $parameters */ $parameters = parent::getParameters(); return $parameters; diff --git a/src/Reflection/GenericParametersAcceptorResolver.php b/src/Reflection/GenericParametersAcceptorResolver.php index 5a62594231..edbb692931 100644 --- a/src/Reflection/GenericParametersAcceptorResolver.php +++ b/src/Reflection/GenericParametersAcceptorResolver.php @@ -2,12 +2,18 @@ namespace PHPStan\Reflection; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Php\DummyParameterWithPhpDocs; +use PHPStan\TrinaryLogic; use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function array_key_exists; +use function array_map; use function array_merge; use function count; use function is_int; @@ -19,7 +25,7 @@ class GenericParametersAcceptorResolver * @api * @param array $argTypes */ - public static function resolve(array $argTypes, ParametersAcceptor $parametersAcceptor): ParametersAcceptor + public static function resolve(array $argTypes, ParametersAcceptor $parametersAcceptor): ParametersAcceptorWithPhpDocs { $typeMap = TemplateTypeMap::createEmpty(); $passedArgs = []; @@ -79,11 +85,51 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc $typeMap->getTypes(), )); - return new ResolvedFunctionVariant( + $originalParametersAcceptor = $parametersAcceptor; + + if (!$parametersAcceptor instanceof ParametersAcceptorWithPhpDocs) { + $parametersAcceptor = new FunctionVariantWithPhpDocs( + $parametersAcceptor->getTemplateTypeMap(), + $parametersAcceptor->getResolvedTemplateTypeMap(), + array_map(static fn (ParameterReflection $parameter): ParameterReflectionWithPhpDocs => new DummyParameterWithPhpDocs( + $parameter->getName(), + $parameter->getType(), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + new MixedType(), + $parameter->getType(), + null, + TrinaryLogic::createMaybe(), + null, + ), $parametersAcceptor->getParameters()), + $parametersAcceptor->isVariadic(), + $parametersAcceptor->getReturnType(), + $parametersAcceptor->getReturnType(), + new MixedType(), + TemplateTypeVarianceMap::createEmpty(), + ); + } + + $result = new ResolvedFunctionVariantWithOriginal( $parametersAcceptor, $resolvedTemplateTypeMap, + $parametersAcceptor->getCallSiteVarianceMap(), $passedArgs, ); + if ($originalParametersAcceptor instanceof CallableParametersAcceptor) { + return new ResolvedFunctionVariantWithCallable( + $result, + $originalParametersAcceptor->getThrowPoints(), + $originalParametersAcceptor->isPure(), + $originalParametersAcceptor->getImpurePoints(), + $originalParametersAcceptor->getInvalidateExpressions(), + $originalParametersAcceptor->getUsedVariables(), + ); + } + + return $result; } } diff --git a/src/Reflection/InaccessibleMethod.php b/src/Reflection/InaccessibleMethod.php index f089c132ad..5b8504a7d6 100644 --- a/src/Reflection/InaccessibleMethod.php +++ b/src/Reflection/InaccessibleMethod.php @@ -2,11 +2,15 @@ namespace PHPStan\Reflection; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\MixedType; use PHPStan\Type\Type; -class InaccessibleMethod implements ParametersAcceptor +class InaccessibleMethod implements CallableParametersAcceptor { public function __construct(private MethodReflection $methodReflection) @@ -28,6 +32,11 @@ public function getResolvedTemplateTypeMap(): TemplateTypeMap return TemplateTypeMap::createEmpty(); } + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return TemplateTypeVarianceMap::createEmpty(); + } + /** * @return array */ @@ -46,4 +55,35 @@ public function getReturnType(): Type return new MixedType(); } + public function getThrowPoints(): array + { + return []; + } + + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getImpurePoints(): array + { + return [ + new SimpleImpurePoint( + 'methodCall', + 'call to unknown method', + false, + ), + ]; + } + + public function getInvalidateExpressions(): array + { + return []; + } + + public function getUsedVariables(): array + { + return []; + } + } diff --git a/src/Reflection/InitializerExprContext.php b/src/Reflection/InitializerExprContext.php index 1818d15e92..c71a75c7d0 100644 --- a/src/Reflection/InitializerExprContext.php +++ b/src/Reflection/InitializerExprContext.php @@ -55,10 +55,13 @@ private static function parseNamespace(string $name): ?string public static function fromClassReflection(ClassReflection $classReflection): self { - $className = $classReflection->getName(); + return self::fromClass($classReflection->getName(), $classReflection->getFileName()); + } + public static function fromClass(string $className, ?string $fileName): self + { return new self( - $classReflection->getFileName(), + $fileName, self::parseNamespace($className), $className, null, @@ -134,6 +137,11 @@ public static function fromGlobalConstant(ReflectionConstant $constant): self ); } + public static function createEmpty(): self + { + return new self(null, null, null, null, null, null); + } + public function getFile(): ?string { return $this->file; diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index eb14e3e166..b8615874f2 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -5,6 +5,9 @@ use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp; +use PhpParser\Node\Expr\ClassConstFetch; +use PhpParser\Node\Expr\ConstFetch; +use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\Scalar\DNumber; @@ -15,11 +18,13 @@ use PhpParser\Node\Scalar\MagicConst\Line; use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\ConstantResolver; +use PHPStan\Analyser\OutOfClassScope; use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; @@ -34,6 +39,7 @@ use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Constant\OversizedArrayBuilder; use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\Enum\EnumCaseObjectType; @@ -51,35 +57,48 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StaticType; -use PHPStan\Type\StaticTypeFactory; use PHPStan\Type\StringType; use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypehintHelper; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; +use function array_key_exists; use function array_keys; +use function array_merge; +use function assert; +use function ceil; use function count; use function dirname; +use function floor; use function in_array; +use function is_finite; use function is_float; use function is_int; use function max; +use function min; use function sprintf; use function strtolower; +use const INF; class InitializerExprTypeResolver { public const CALCULATE_SCALARS_LIMIT = 128; + /** @var array */ + private array $currentlyResolvingClassConstant = []; + public function __construct( private ConstantResolver $constantResolver, private ReflectionProviderProvider $reflectionProviderProvider, private PhpVersion $phpVersion, private OperatorTypeSpecifyingExtensionRegistryProvider $operatorTypeSpecifyingExtensionRegistryProvider, + private OversizedArrayBuilder $oversizedArrayBuilder, + private bool $usePathConstantsAsConstantString = false, ) { } @@ -99,7 +118,7 @@ public function getType(Expr $expr, InitializerExprContext $context): Type if ($expr instanceof String_) { return new ConstantStringType($expr->value); } - if ($expr instanceof Expr\ConstFetch) { + if ($expr instanceof ConstFetch) { $constName = (string) $expr->name; $loweredConstName = strtolower($constName); if ($loweredConstName === 'true') { @@ -119,14 +138,22 @@ public function getType(Expr $expr, InitializerExprContext $context): Type } if ($expr instanceof File) { $file = $context->getFile(); - return $file !== null ? (new ConstantStringType($file))->generalize(GeneralizePrecision::moreSpecific()) : new StringType(); + if ($file === null) { + return new StringType(); + } + $stringType = new ConstantStringType($file); + return $this->usePathConstantsAsConstantString ? $stringType : $stringType->generalize(GeneralizePrecision::moreSpecific()); } if ($expr instanceof Dir) { $file = $context->getFile(); - return $file !== null ? (new ConstantStringType(dirname($file)))->generalize(GeneralizePrecision::moreSpecific()) : new StringType(); + if ($file === null) { + return new StringType(); + } + $stringType = new ConstantStringType(dirname($file)); + return $this->usePathConstantsAsConstantString ? $stringType : $stringType->generalize(GeneralizePrecision::moreSpecific()); } if ($expr instanceof Line) { - return new ConstantIntegerType($expr->getLine()); + return new ConstantIntegerType($expr->getStartLine()); } if ($expr instanceof Expr\New_) { if ($expr->class instanceof Name) { @@ -143,7 +170,7 @@ public function getType(Expr $expr, InitializerExprContext $context): Type $dim = $this->getType($expr->dim, $context); return $var->getOffsetValueType($dim); } - if ($expr instanceof Expr\ClassConstFetch && $expr->name instanceof Identifier) { + if ($expr instanceof ClassConstFetch && $expr->name instanceof Identifier) { return $this->getClassConstFetchType($expr->class, $expr->name->toString(), $context->getClassName(), fn (Expr $expr): Type => $this->getType($expr, $context)); } if ($expr instanceof Expr\UnaryPlus) { @@ -164,7 +191,7 @@ public function getType(Expr $expr, InitializerExprContext $context): Type $elseType = $this->getType($expr->else, $context); if ($expr->if === null) { return TypeCombinator::union( - TypeCombinator::remove($condType, StaticTypeFactory::falsey()), + TypeCombinator::removeFalsey($condType), $elseType, ); } @@ -172,7 +199,7 @@ public function getType(Expr $expr, InitializerExprContext $context): Type $ifType = $this->getType($expr->if, $context); return TypeCombinator::union( - TypeCombinator::remove($ifType, StaticTypeFactory::falsey()), + TypeCombinator::removeFalsey($ifType), $elseType, ); } @@ -317,6 +344,13 @@ public function getType(Expr $expr, InitializerExprContext $context): Type } if ($expr instanceof MagicConst\Class_) { + if ($context->getTraitName() !== null) { + return TypeCombinator::intersect( + new ClassStringType(), + new AccessoryLiteralStringType(), + ); + } + if ($context->getClassName() === null) { return new ConstantStringType(''); } @@ -325,6 +359,13 @@ public function getType(Expr $expr, InitializerExprContext $context): Type } if ($expr instanceof MagicConst\Namespace_) { + if ($context->getTraitName() !== null) { + return TypeCombinator::intersect( + new StringType(), + new AccessoryLiteralStringType(), + ); + } + return new ConstantStringType($context->getNamespace() ?? ''); } @@ -344,6 +385,15 @@ public function getType(Expr $expr, InitializerExprContext $context): Type return new ConstantStringType($context->getTraitName(), true); } + if ($expr instanceof PropertyFetch && $expr->name instanceof Identifier) { + $fetchedOnType = $this->getType($expr->var, $context); + if (!$fetchedOnType->hasProperty($expr->name->name)->yes()) { + return new ErrorType(); + } + + return $fetchedOnType->getProperty($expr->name->name, new OutOfClassScope())->getReadableType(); + } + return new MixedType(); } @@ -352,8 +402,16 @@ public function getType(Expr $expr, InitializerExprContext $context): Type */ public function getConcatType(Expr $left, Expr $right, callable $getTypeCallback): Type { - $leftStringType = $getTypeCallback($left)->toString(); - $rightStringType = $getTypeCallback($right)->toString(); + $leftType = $getTypeCallback($left); + $rightType = $getTypeCallback($right); + + return $this->resolveConcatType($leftType, $rightType); + } + + public function resolveConcatType(Type $left, Type $right): Type + { + $leftStringType = $left->toString(); + $rightStringType = $right->toString(); if (TypeCombinator::union( $leftStringType, $rightStringType, @@ -373,34 +431,33 @@ public function getConcatType(Expr $left, Expr $right, callable $getTypeCallback return $leftStringType->append($rightStringType); } + $leftConstantStrings = $leftStringType->getConstantStrings(); + $rightConstantStrings = $rightStringType->getConstantStrings(); + $combinedConstantStringsCount = count($leftConstantStrings) * count($rightConstantStrings); + // we limit the number of union-types for performance reasons - if ($leftStringType instanceof UnionType && count($leftStringType->getTypes()) <= 16 && $rightStringType instanceof ConstantStringType) { - $constantStrings = TypeUtils::getConstantStrings($leftStringType); - if (count($constantStrings) > 0) { - $strings = []; - foreach ($constantStrings as $constantString) { - if ($constantString->getValue() === '') { - $strings[] = $rightStringType; + if ($combinedConstantStringsCount > 0 && $combinedConstantStringsCount <= 16) { + $strings = []; - continue; - } - $strings[] = $constantString->append($rightStringType); + foreach ($leftConstantStrings as $leftConstantString) { + if ($leftConstantString->getValue() === '') { + $strings = array_merge($strings, $rightConstantStrings); + + continue; } - return TypeCombinator::union(...$strings); - } - } - if ($rightStringType instanceof UnionType && count($rightStringType->getTypes()) <= 16 && $leftStringType instanceof ConstantStringType) { - $constantStrings = TypeUtils::getConstantStrings($rightStringType); - if (count($constantStrings) > 0) { - $strings = []; - foreach ($constantStrings as $constantString) { - if ($constantString->getValue() === '') { - $strings[] = $leftStringType; + + foreach ($rightConstantStrings as $rightConstantString) { + if ($rightConstantString->getValue() === '') { + $strings[] = $leftConstantString; continue; } - $strings[] = $leftStringType->append($constantString); + + $strings[] = $leftConstantString->append($rightConstantString); } + } + + if (count($strings) > 0) { return TypeCombinator::union(...$strings); } } @@ -431,10 +488,12 @@ public function getConcatType(Expr $left, Expr $right, callable $getTypeCallback */ public function getArrayType(Expr\Array_ $expr, callable $getTypeCallback): Type { - $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); if (count($expr->items) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { - $arrayBuilder->degradeToGeneralArray(); + return $this->oversizedArrayBuilder->build($expr, $getTypeCallback); } + + $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + $isList = null; foreach ($expr->items as $arrayItem) { if ($arrayItem === null) { continue; @@ -442,28 +501,35 @@ public function getArrayType(Expr\Array_ $expr, callable $getTypeCallback): Type $valueType = $getTypeCallback($arrayItem->value); if ($arrayItem->unpack) { - if ($valueType instanceof ConstantArrayType) { + $constantArrays = $valueType->getConstantArrays(); + if (count($constantArrays) === 1) { + $constantArrayType = $constantArrays[0]; $hasStringKey = false; - foreach ($valueType->getKeyTypes() as $keyType) { - if ($keyType instanceof ConstantStringType) { + foreach ($constantArrayType->getKeyTypes() as $keyType) { + if ($keyType->isString()->yes()) { $hasStringKey = true; break; } } - foreach ($valueType->getValueTypes() as $i => $innerValueType) { + foreach ($constantArrayType->getValueTypes() as $i => $innerValueType) { if ($hasStringKey && $this->phpVersion->supportsArrayUnpackingWithStringKeys()) { - $arrayBuilder->setOffsetValueType($valueType->getKeyTypes()[$i], $innerValueType, $valueType->isOptionalKey($i)); + $arrayBuilder->setOffsetValueType($constantArrayType->getKeyTypes()[$i], $innerValueType, $constantArrayType->isOptionalKey($i)); } else { - $arrayBuilder->setOffsetValueType(null, $innerValueType, $valueType->isOptionalKey($i)); + $arrayBuilder->setOffsetValueType(null, $innerValueType, $constantArrayType->isOptionalKey($i)); } } } else { $arrayBuilder->degradeToGeneralArray(); - $offsetType = $this->phpVersion->supportsArrayUnpackingWithStringKeys() && !$valueType->getIterableKeyType()->isString()->no() - ? $valueType->getIterableKeyType() - : new IntegerType(); + if ($this->phpVersion->supportsArrayUnpackingWithStringKeys() && !$valueType->getIterableKeyType()->isString()->no()) { + $isList = false; + $offsetType = $valueType->getIterableKeyType(); + } else { + $isList ??= $arrayBuilder->isList(); + $offsetType = new IntegerType(); + } + $arrayBuilder->setOffsetValueType($offsetType, $valueType->getIterableValueType(), !$valueType->isIterableAtLeastOnce()->yes()); } } else { @@ -473,7 +539,13 @@ public function getArrayType(Expr\Array_ $expr, callable $getTypeCallback): Type ); } } - return $arrayBuilder->getArray(); + + $arrayType = $arrayBuilder->getArray(); + if ($isList === true) { + return AccessoryArrayListType::intersectWith($arrayType); + } + + return $arrayType; } /** @@ -485,11 +557,11 @@ public function getBitwiseAndType(Expr $left, Expr $right, callable $getTypeCall $rightType = $getTypeCallback($right); if ($leftType instanceof NeverType || $rightType instanceof NeverType) { - return new NeverType(); + return $this->getNeverType($leftType, $rightType); } - $leftTypes = TypeUtils::getConstantScalars($leftType); - $rightTypes = TypeUtils::getConstantScalars($rightType); + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); $leftTypesCount = count($leftTypes); $rightTypesCount = count($rightTypes); if ($leftTypesCount > 0 && $rightTypesCount > 0) { @@ -552,11 +624,11 @@ public function getBitwiseOrType(Expr $left, Expr $right, callable $getTypeCallb $rightType = $getTypeCallback($right); if ($leftType instanceof NeverType || $rightType instanceof NeverType) { - return new NeverType(); + return $this->getNeverType($leftType, $rightType); } - $leftTypes = TypeUtils::getConstantScalars($leftType); - $rightTypes = TypeUtils::getConstantScalars($rightType); + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); $leftTypesCount = count($leftTypes); $rightTypesCount = count($rightTypes); if ($leftTypesCount > 0 && $rightTypesCount > 0) { @@ -609,11 +681,11 @@ public function getBitwiseXorType(Expr $left, Expr $right, callable $getTypeCall $rightType = $getTypeCallback($right); if ($leftType instanceof NeverType || $rightType instanceof NeverType) { - return new NeverType(); + return $this->getNeverType($leftType, $rightType); } - $leftTypes = TypeUtils::getConstantScalars($leftType); - $rightTypes = TypeUtils::getConstantScalars($rightType); + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); $leftTypesCount = count($leftTypes); $rightTypesCount = count($rightTypes); if ($leftTypesCount > 0 && $rightTypesCount > 0) { @@ -666,25 +738,21 @@ public function getSpaceshipType(Expr $left, Expr $right, callable $getTypeCallb $callbackRightType = $getTypeCallback($right); if ($callbackLeftType instanceof NeverType || $callbackRightType instanceof NeverType) { - return new NeverType(); + return $this->getNeverType($callbackLeftType, $callbackRightType); } - $leftTypes = TypeUtils::getConstantScalars($callbackLeftType); - $rightTypes = TypeUtils::getConstantScalars($callbackRightType); + $leftTypes = $callbackLeftType->getConstantScalarTypes(); + $rightTypes = $callbackRightType->getConstantScalarTypes(); $leftTypesCount = count($leftTypes); $rightTypesCount = count($rightTypes); - if ($leftTypesCount > 0 && $rightTypesCount > 0) { + if ($leftTypesCount > 0 && $rightTypesCount > 0 && $leftTypesCount * $rightTypesCount <= self::CALCULATE_SCALARS_LIMIT) { $resultTypes = []; - $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; foreach ($leftTypes as $leftType) { foreach ($rightTypes as $rightType) { $leftValue = $leftType->getValue(); $rightValue = $rightType->getValue(); $resultType = $this->getTypeFromValue($leftValue <=> $rightValue); - if ($generalize) { - $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); - } $resultTypes[] = $resultType; } } @@ -702,8 +770,8 @@ public function getDivType(Expr $left, Expr $right, callable $getTypeCallback): $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); - $leftTypes = TypeUtils::getConstantScalars($leftType); - $rightTypes = TypeUtils::getConstantScalars($rightType); + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); $leftTypesCount = count($leftTypes); $rightTypesCount = count($rightTypes); if ($leftTypesCount > 0 && $rightTypesCount > 0) { @@ -722,11 +790,11 @@ public function getDivType(Expr $left, Expr $right, callable $getTypeCallback): throw new ShouldNotHappenException(); } - if ($rightNumberType->getValue() === 0 || $rightNumberType->getValue() === 0.0) { + if (in_array($rightNumberType->getValue(), [0, 0.0], true)) { return new ErrorType(); } - $resultType = $this->getTypeFromValue($leftNumberType->getValue() / $rightNumberType->getValue()); // @phpstan-ignore-line + $resultType = $this->getTypeFromValue($leftNumberType->getValue() / $rightNumberType->getValue()); // @phpstan-ignore binaryOp.invalid if ($generalize) { $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); } @@ -736,13 +804,9 @@ public function getDivType(Expr $left, Expr $right, callable $getTypeCallback): return TypeCombinator::union(...$resultTypes); } - $rightScalarTypes = TypeUtils::getConstantScalars($rightType->toNumber()); - foreach ($rightScalarTypes as $scalarType) { - - if ( - $scalarType->getValue() === 0 - || $scalarType->getValue() === 0.0 - ) { + $rightScalarValues = $rightType->toNumber()->getConstantScalarValues(); + foreach ($rightScalarValues as $scalarValue) { + if ($scalarValue === 0 || $scalarValue === 0.0) { return new ErrorType(); } } @@ -759,11 +823,11 @@ public function getModType(Expr $left, Expr $right, callable $getTypeCallback): $rightType = $getTypeCallback($right); if ($leftType instanceof NeverType || $rightType instanceof NeverType) { - return new NeverType(); + return $this->getNeverType($leftType, $rightType); } - $leftTypes = TypeUtils::getConstantScalars($leftType); - $rightTypes = TypeUtils::getConstantScalars($rightType); + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); $leftTypesCount = count($leftTypes); $rightTypesCount = count($rightTypes); if ($leftTypesCount > 0 && $rightTypesCount > 0) { @@ -802,20 +866,16 @@ public function getModType(Expr $left, Expr $right, callable $getTypeCallback): return new ConstantIntegerType(0); } - $rightScalarTypes = TypeUtils::getConstantScalars($rightType->toNumber()); - foreach ($rightScalarTypes as $scalarType) { + $rightScalarValues = $rightType->toNumber()->getConstantScalarValues(); + foreach ($rightScalarValues as $scalarValue) { - if ( - $scalarType->getValue() === 0 - || $scalarType->getValue() === 0.0 - ) { + if ($scalarValue === 0 || $scalarValue === 0.0) { return new ErrorType(); } } - $integer = new IntegerType(); $positiveInt = IntegerRangeType::fromInterval(0, null); - if ($integer->isSuperTypeOf($rightType)->yes()) { + if ($rightType->isInteger()->yes()) { $rangeMin = null; $rangeMax = null; @@ -860,11 +920,11 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): $rightType = $getTypeCallback($right); if ($leftType instanceof NeverType || $rightType instanceof NeverType) { - return new NeverType(); + return $this->getNeverType($leftType, $rightType); } - $leftTypes = TypeUtils::getConstantScalars($leftType); - $rightTypes = TypeUtils::getConstantScalars($rightType); + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); $leftTypesCount = count($leftTypes); $rightTypesCount = count($rightTypes); if ($leftTypesCount > 0 && $rightTypesCount > 0) { @@ -894,8 +954,8 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): return TypeCombinator::union(...$resultTypes); } - $leftConstantArrays = TypeUtils::getOldConstantArrays($leftType); - $rightConstantArrays = TypeUtils::getOldConstantArrays($rightType); + $leftConstantArrays = $leftType->getConstantArrays(); + $rightConstantArrays = $rightType->getConstantArrays(); $leftCount = count($leftConstantArrays); $rightCount = count($rightConstantArrays); @@ -948,12 +1008,24 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): ); if ($leftType->isIterableAtLeastOnce()->yes() || $rightType->isIterableAtLeastOnce()->yes()) { - return TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); } + if ($leftType->isList()->yes() && $rightType->isList()->yes()) { + $arrayType = AccessoryArrayListType::intersectWith($arrayType); + } + return $arrayType; } if ($leftType instanceof MixedType && $rightType instanceof MixedType) { + if ( + ($leftIsArray->no() && $rightIsArray->no()) + ) { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } return new BenevolentUnionType([ new FloatType(), new IntegerType(), @@ -1005,8 +1077,8 @@ public function getMinusType(Expr $left, Expr $right, callable $getTypeCallback) $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); - $leftTypes = TypeUtils::getConstantScalars($leftType); - $rightTypes = TypeUtils::getConstantScalars($rightType); + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); $leftTypesCount = count($leftTypes); $rightTypesCount = count($rightTypes); if ($leftTypesCount > 0 && $rightTypesCount > 0) { @@ -1047,8 +1119,8 @@ public function getMulType(Expr $left, Expr $right, callable $getTypeCallback): $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); - $leftTypes = TypeUtils::getConstantScalars($leftType); - $rightTypes = TypeUtils::getConstantScalars($rightType); + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); $leftTypesCount = count($leftTypes); $rightTypesCount = count($rightTypes); if ($leftTypesCount > 0 && $rightTypesCount > 0) { @@ -1105,38 +1177,17 @@ public function getPowType(Expr $left, Expr $right, callable $getTypeCallback): $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); - $leftTypes = TypeUtils::getConstantScalars($leftType); - $rightTypes = TypeUtils::getConstantScalars($rightType); - $leftTypesCount = count($leftTypes); - $rightTypesCount = count($rightTypes); - if ($leftTypesCount > 0 && $rightTypesCount > 0) { - $resultTypes = []; - $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; - foreach ($leftTypes as $leftTypeInner) { - foreach ($rightTypes as $rightTypeInner) { - $leftNumberType = $leftTypeInner->toNumber(); - $rightNumberType = $rightTypeInner->toNumber(); - - if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { - return new ErrorType(); - } - - if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { - throw new ShouldNotHappenException(); - } - - $resultType = $this->getTypeFromValue($leftNumberType->getValue() ** $rightNumberType->getValue()); - if ($generalize) { - $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); - } - $resultTypes[] = $resultType; - } - } + $exponentiatedTyped = $leftType->exponentiate($rightType); + if (!$exponentiatedTyped instanceof ErrorType) { + return $exponentiatedTyped; + } - return TypeCombinator::union(...$resultTypes); + $extensionSpecified = $this->callOperatorTypeSpecifyingExtensions(new BinaryOp\Pow($left, $right), $leftType, $rightType); + if ($extensionSpecified !== null) { + return $extensionSpecified; } - return $this->resolveCommonMath(new BinaryOp\Pow($left, $right), $leftType, $rightType); + return new ErrorType(); } /** @@ -1148,11 +1199,11 @@ public function getShiftLeftType(Expr $left, Expr $right, callable $getTypeCallb $rightType = $getTypeCallback($right); if ($leftType instanceof NeverType || $rightType instanceof NeverType) { - return new NeverType(); + return $this->getNeverType($leftType, $rightType); } - $leftTypes = TypeUtils::getConstantScalars($leftType); - $rightTypes = TypeUtils::getConstantScalars($rightType); + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); $leftTypesCount = count($leftTypes); $rightTypesCount = count($rightTypes); if ($leftTypesCount > 0 && $rightTypesCount > 0) { @@ -1205,11 +1256,11 @@ public function getShiftRightType(Expr $left, Expr $right, callable $getTypeCall $rightType = $getTypeCallback($right); if ($leftType instanceof NeverType || $rightType instanceof NeverType) { - return new NeverType(); + return $this->getNeverType($leftType, $rightType); } - $leftTypes = TypeUtils::getConstantScalars($leftType); - $rightTypes = TypeUtils::getConstantScalars($rightType); + $leftTypes = $leftType->getConstantScalarTypes(); + $rightTypes = $rightType->getConstantScalarTypes(); $leftTypesCount = count($leftTypes); $rightTypesCount = count($rightTypes); if ($leftTypesCount > 0 && $rightTypesCount > 0) { @@ -1259,6 +1310,12 @@ public function resolveIdenticalType(Type $leftType, Type $rightType): BooleanTy return new ConstantBooleanType($leftType->getValue() === $rightType->getValue()); } + $leftTypeFiniteTypes = $leftType->getFiniteTypes(); + $rightTypeFiniteType = $rightType->getFiniteTypes(); + if (count($leftTypeFiniteTypes) === 1 && count($rightTypeFiniteType) === 1) { + return new ConstantBooleanType($leftTypeFiniteTypes[0]->equals($rightTypeFiniteType[0])); + } + $isSuperset = $leftType->isSuperTypeOf($rightType); if ($isSuperset->no()) { return new ConstantBooleanType(false); @@ -1273,35 +1330,21 @@ public function resolveIdenticalType(Type $leftType, Type $rightType): BooleanTy public function resolveEqualType(Type $leftType, Type $rightType): BooleanType { - $integerType = new IntegerType(); - $floatType = new FloatType(); if ( - ($leftType->isString()->yes() && $rightType->isString()->yes()) - || ($integerType->isSuperTypeOf($leftType)->yes() && $integerType->isSuperTypeOf($rightType)->yes()) - || ($floatType->isSuperTypeOf($leftType)->yes() && $floatType->isSuperTypeOf($rightType)->yes()) + ($leftType->isEnum()->yes() && $rightType->isTrue()->no()) + || ($rightType->isEnum()->yes() && $leftType->isTrue()->no()) + || ($leftType->isString()->yes() && $rightType->isString()->yes()) + || ($leftType->isInteger()->yes() && $rightType->isInteger()->yes()) + || ($leftType->isFloat()->yes() && $rightType->isFloat()->yes()) ) { return $this->resolveIdenticalType($leftType, $rightType); } - if ($leftType instanceof ConstantArrayType && $leftType->isEmpty() && $rightType instanceof ConstantScalarType) { - // @phpstan-ignore-next-line - return new ConstantBooleanType($rightType->getValue() == []); // phpcs:ignore - } - if ($rightType instanceof ConstantArrayType && $rightType->isEmpty() && $leftType instanceof ConstantScalarType) { - // @phpstan-ignore-next-line - return new ConstantBooleanType($leftType->getValue() == []); // phpcs:ignore - } - - if ($leftType instanceof ConstantScalarType && $rightType instanceof ConstantScalarType) { - // @phpstan-ignore-next-line - return new ConstantBooleanType($leftType->getValue() == $rightType->getValue()); // phpcs:ignore - } - if ($leftType instanceof ConstantArrayType && $rightType instanceof ConstantArrayType) { return $this->resolveConstantArrayTypeComparison($leftType, $rightType, fn ($leftValueType, $rightValueType): BooleanType => $this->resolveEqualType($leftValueType, $rightValueType)); } - return new BooleanType(); + return $leftType->looseCompare($rightType, $this->phpVersion); } /** @@ -1361,7 +1404,7 @@ private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType, } $leftIdenticalToRight = $valueComparisonCallback($leftValueTypes[$i], $rightValueTypes[$j]); - if ($leftIdenticalToRight instanceof ConstantBooleanType && !$leftIdenticalToRight->getValue()) { + if ($leftIdenticalToRight->isFalse()->yes()) { return new ConstantBooleanType(false); } $resultType = TypeCombinator::union($resultType, $leftIdenticalToRight); @@ -1377,93 +1420,99 @@ private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType, return $resultType->toBoolean(); } + private function callOperatorTypeSpecifyingExtensions(Expr\BinaryOp $expr, Type $leftType, Type $rightType): ?Type + { + $operatorSigil = $expr->getOperatorSigil(); + $operatorTypeSpecifyingExtensions = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry()->getOperatorTypeSpecifyingExtensions($operatorSigil, $leftType, $rightType); + + /** @var Type[] $extensionTypes */ + $extensionTypes = []; + + foreach ($operatorTypeSpecifyingExtensions as $extension) { + $extensionTypes[] = $extension->specifyType($operatorSigil, $leftType, $rightType); + } + + if (count($extensionTypes) > 0) { + return TypeCombinator::union(...$extensionTypes); + } + + return null; + } + /** - * @param BinaryOp\Plus|BinaryOp\Minus|BinaryOp\Mul|BinaryOp\Pow|BinaryOp\Div $expr + * @param BinaryOp\Plus|BinaryOp\Minus|BinaryOp\Mul|BinaryOp\Div $expr */ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $rightType): Type { - if (($leftType instanceof IntegerRangeType || $leftType instanceof ConstantIntegerType || $leftType instanceof UnionType) && - ($rightType instanceof IntegerRangeType || $rightType instanceof ConstantIntegerType || $rightType instanceof UnionType) && - !$expr instanceof BinaryOp\Pow) { + $types = TypeCombinator::union($leftType, $rightType); + $leftNumberType = $leftType->toNumber(); + $rightNumberType = $rightType->toNumber(); - if ($leftType instanceof ConstantIntegerType) { + if ( + !$types instanceof MixedType + && ( + $rightNumberType instanceof IntegerRangeType + || $rightNumberType instanceof ConstantIntegerType + || $rightNumberType instanceof UnionType + ) + ) { + if ($leftNumberType instanceof IntegerRangeType || $leftNumberType instanceof ConstantIntegerType) { return $this->integerRangeMath( - $leftType, + $leftNumberType, $expr, - $rightType, + $rightNumberType, ); - } elseif ($leftType instanceof UnionType) { - + } elseif ($leftNumberType instanceof UnionType) { $unionParts = []; - foreach ($leftType->getTypes() as $type) { - if ($type instanceof IntegerRangeType || $type instanceof ConstantIntegerType) { - $unionParts[] = $this->integerRangeMath($type, $expr, $rightType); + foreach ($leftNumberType->getTypes() as $type) { + $numberType = $type->toNumber(); + if ($numberType instanceof IntegerRangeType || $numberType instanceof ConstantIntegerType) { + $unionParts[] = $this->integerRangeMath($numberType, $expr, $rightNumberType); } else { - $unionParts[] = $type; + $unionParts[] = $numberType; } } $union = TypeCombinator::union(...$unionParts); - if ($leftType instanceof BenevolentUnionType) { + if ($leftNumberType instanceof BenevolentUnionType) { return TypeUtils::toBenevolentUnion($union)->toNumber(); } return $union->toNumber(); } - - return $this->integerRangeMath($leftType, $expr, $rightType); } - $operatorSigil = $expr->getOperatorSigil(); - $operatorTypeSpecifyingExtensions = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry()->getOperatorTypeSpecifyingExtensions($operatorSigil, $leftType, $rightType); - - /** @var Type[] $extensionTypes */ - $extensionTypes = []; - - foreach ($operatorTypeSpecifyingExtensions as $extension) { - $extensionTypes[] = $extension->specifyType($operatorSigil, $leftType, $rightType); - } - - if (count($extensionTypes) > 0) { - return TypeCombinator::union(...$extensionTypes); + $specifiedTypes = $this->callOperatorTypeSpecifyingExtensions($expr, $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; } - $types = TypeCombinator::union($leftType, $rightType); if ( - $leftType instanceof ArrayType - || $rightType instanceof ArrayType - || $types instanceof ArrayType + $leftType->isArray()->yes() + || $rightType->isArray()->yes() + || $types->isArray()->yes() ) { return new ErrorType(); } - $leftNumberType = $leftType->toNumber(); - $rightNumberType = $rightType->toNumber(); if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { return new ErrorType(); } if ($leftNumberType instanceof NeverType || $rightNumberType instanceof NeverType) { - return new NeverType(); + return $this->getNeverType($leftNumberType, $rightNumberType); } if ( - (new FloatType())->isSuperTypeOf($leftNumberType)->yes() - || (new FloatType())->isSuperTypeOf($rightNumberType)->yes() + $leftNumberType->isFloat()->yes() + || $rightNumberType->isFloat()->yes() ) { return new FloatType(); } - if ($expr instanceof Expr\BinaryOp\Pow) { - return new BenevolentUnionType([ - new FloatType(), - new IntegerType(), - ]); - } - $resultType = TypeCombinator::union($leftNumberType, $rightNumberType); if ($expr instanceof Expr\BinaryOp\Div) { - if ($types instanceof MixedType || $resultType instanceof IntegerType) { + if ($types instanceof MixedType || $resultType->isInteger()->yes()) { return new BenevolentUnionType([new IntegerType(), new FloatType()]); } @@ -1483,7 +1532,6 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri /** * @param ConstantIntegerType|IntegerRangeType $range * @param BinaryOp\Div|BinaryOp\Minus|BinaryOp\Mul|BinaryOp\Plus $node - * @param IntegerRangeType|ConstantIntegerType|UnionType $operand */ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): Type { @@ -1500,8 +1548,9 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T $unionParts = []; foreach ($operand->getTypes() as $type) { - if ($type instanceof IntegerRangeType || $type instanceof ConstantIntegerType) { - $unionParts[] = $this->integerRangeMath($range, $node, $type); + $numberType = $type->toNumber(); + if ($numberType instanceof IntegerRangeType || $numberType instanceof ConstantIntegerType) { + $unionParts[] = $this->integerRangeMath($range, $node, $numberType); } else { $unionParts[] = $type->toNumber(); } @@ -1515,6 +1564,17 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T return $union->toNumber(); } + $operand = $operand->toNumber(); + if ($operand instanceof IntegerRangeType) { + $operandMin = $operand->getMin(); + $operandMax = $operand->getMax(); + } elseif ($operand instanceof ConstantIntegerType) { + $operandMin = $operand->getValue(); + $operandMax = $operand->getValue(); + } else { + return $operand; + } + if ($node instanceof BinaryOp\Plus) { if ($operand instanceof ConstantIntegerType) { /** @var int|float|null $min */ @@ -1580,63 +1640,103 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T } } } elseif ($node instanceof Expr\BinaryOp\Mul) { - if ($operand instanceof ConstantIntegerType) { - /** @var int|float|null $min */ - $min = $rangeMin !== null ? $rangeMin * $operand->getValue() : null; + $min1 = $rangeMin === 0 || $operandMin === 0 ? 0 : ($rangeMin ?? -INF) * ($operandMin ?? -INF); + $min2 = $rangeMin === 0 || $operandMax === 0 ? 0 : ($rangeMin ?? -INF) * ($operandMax ?? INF); + $max1 = $rangeMax === 0 || $operandMin === 0 ? 0 : ($rangeMax ?? INF) * ($operandMin ?? -INF); + $max2 = $rangeMax === 0 || $operandMax === 0 ? 0 : ($rangeMax ?? INF) * ($operandMax ?? INF); - /** @var int|float|null $max */ - $max = $rangeMax !== null ? $rangeMax * $operand->getValue() : null; - } else { - /** @var int|float|null $min */ - $min = $rangeMin !== null && $operand->getMin() !== null ? $rangeMin * $operand->getMin() : null; - - /** @var int|float|null $max */ - $max = $rangeMax !== null && $operand->getMax() !== null ? $rangeMax * $operand->getMax() : null; - } + $min = min($min1, $min2, $max1, $max2); + $max = max($min1, $min2, $max1, $max2); - if ($min !== null && $max !== null && $min > $max) { - [$min, $max] = [$max, $min]; + if (!is_finite($min)) { + $min = null; } - - // invert maximas on multiplication with negative constants - if ((($range instanceof ConstantIntegerType && $range->getValue() < 0) - || ($operand instanceof ConstantIntegerType && $operand->getValue() < 0)) - && ($min === null || $max === null)) { - [$min, $max] = [$max, $min]; + if (!is_finite($max)) { + $max = null; } - } else { if ($operand instanceof ConstantIntegerType) { $min = $rangeMin !== null && $operand->getValue() !== 0 ? $rangeMin / $operand->getValue() : null; $max = $rangeMax !== null && $operand->getValue() !== 0 ? $rangeMax / $operand->getValue() : null; } else { - $min = $rangeMin !== null && $operand->getMin() !== null && $operand->getMin() !== 0 ? $rangeMin / $operand->getMin() : null; - $max = $rangeMax !== null && $operand->getMax() !== null && $operand->getMax() !== 0 ? $rangeMax / $operand->getMax() : null; - } + // Avoid division by zero when looking for the min and the max by using the closest int + $operandMin = $operandMin !== 0 ? $operandMin : 1; + $operandMax = $operandMax !== 0 ? $operandMax : -1; + + if ( + ($operandMin < 0 || $operandMin === null) + && ($operandMax > 0 || $operandMax === null) + ) { + $negativeOperand = IntegerRangeType::fromInterval($operandMin, 0); + assert($negativeOperand instanceof IntegerRangeType); + $positiveOperand = IntegerRangeType::fromInterval(0, $operandMax); + assert($positiveOperand instanceof IntegerRangeType); + + $result = TypeCombinator::union( + $this->integerRangeMath($range, $node, $negativeOperand), + $this->integerRangeMath($range, $node, $positiveOperand), + )->toNumber(); + + if ($result->equals(new UnionType([new IntegerType(), new FloatType()]))) { + return new BenevolentUnionType([new IntegerType(), new FloatType()]); + } + + return $result; + } + if ( + ($rangeMin < 0 || $rangeMin === null) + && ($rangeMax > 0 || $rangeMax === null) + ) { + $negativeRange = IntegerRangeType::fromInterval($rangeMin, 0); + assert($negativeRange instanceof IntegerRangeType); + $positiveRange = IntegerRangeType::fromInterval(0, $rangeMax); + assert($positiveRange instanceof IntegerRangeType); + + $result = TypeCombinator::union( + $this->integerRangeMath($negativeRange, $node, $operand), + $this->integerRangeMath($positiveRange, $node, $operand), + )->toNumber(); + + if ($result->equals(new UnionType([new IntegerType(), new FloatType()]))) { + return new BenevolentUnionType([new IntegerType(), new FloatType()]); + } + + return $result; + } + + $rangeMinSign = ($rangeMin ?? -INF) <=> 0; + $rangeMaxSign = ($rangeMax ?? INF) <=> 0; + + $min1 = $operandMin !== null ? ($rangeMin ?? -INF) / $operandMin : $rangeMinSign * -0.1; + $min2 = $operandMax !== null ? ($rangeMin ?? -INF) / $operandMax : $rangeMinSign * 0.1; + $max1 = $operandMin !== null ? ($rangeMax ?? INF) / $operandMin : $rangeMaxSign * -0.1; + $max2 = $operandMax !== null ? ($rangeMax ?? INF) / $operandMax : $rangeMaxSign * 0.1; - if ($range instanceof IntegerRangeType && $operand instanceof IntegerRangeType) { - if ($rangeMax === null && $operand->getMax() === null) { - $min = 0; - } elseif ($rangeMin === null && $operand->getMin() === null) { + $min = min($min1, $min2, $max1, $max2); + $max = max($min1, $min2, $max1, $max2); + + if ($min === -INF) { $min = null; + } + if ($max === INF) { $max = null; } } + if ($min !== null && $max !== null && $min > $max) { + [$min, $max] = [$max, $min]; + } + if ($operand instanceof IntegerRangeType - && ($operand->getMin() === null || $operand->getMax() === null) || ($rangeMin === null || $rangeMax === null) - || is_float($min) || is_float($max) + || is_float($min) + || is_float($max) ) { if (is_float($min)) { - $min = (int) $min; + $min = (int) ceil($min); } if (is_float($max)) { - $max = (int) $max; - } - - if ($min !== null && $max !== null && $min > $max) { - [$min, $max] = [$max, $min]; + $max = (int) floor($max); } // invert maximas on division with negative constants @@ -1710,7 +1810,7 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con if (strtolower($constantName) === 'class') { return TypeTraverser::map( $constantClassType, - static function (Type $type, callable $traverse): Type { + function (Type $type, callable $traverse): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } @@ -1726,14 +1826,19 @@ static function (Type $type, callable $traverse): Type { ); } - if ($type instanceof TemplateType && !$type instanceof TypeWithClassName) { + $objectClassNames = $type->getObjectClassNames(); + if (count($objectClassNames) > 1) { + throw new ShouldNotHappenException(); + } + + if ($type instanceof TemplateType && $objectClassNames === []) { return TypeCombinator::intersect( new GenericClassStringType($type), new AccessoryLiteralStringType(), ); - } elseif ($type instanceof TypeWithClassName) { - $reflection = $type->getClassReflection(); - if ($reflection !== null && $reflection->isFinalByKeyword()) { + } elseif ($objectClassNames !== [] && $this->getReflectionProvider()->hasClass($objectClassNames[0])) { + $reflection = $this->getReflectionProvider()->getClass($objectClassNames[0]); + if ($reflection->isFinalByKeyword()) { return new ConstantStringType($reflection->getName(), true); } @@ -1741,7 +1846,7 @@ static function (Type $type, callable $traverse): Type { new GenericClassStringType($type), new AccessoryLiteralStringType(), ); - } elseif ((new ObjectWithoutClassType())->isSuperTypeOf($type)->yes()) { + } elseif ($type->isObject()->yes()) { return TypeCombinator::intersect( new ClassStringType(), new AccessoryLiteralStringType(), @@ -1753,9 +1858,8 @@ static function (Type $type, callable $traverse): Type { ); } - $referencedClasses = TypeUtils::getDirectClassNames($constantClassType); $types = []; - foreach ($referencedClasses as $referencedClass) { + foreach ($constantClassType->getObjectClassNames() as $referencedClass) { if (!$this->getReflectionProvider()->hasClass($referencedClass)) { continue; } @@ -1770,32 +1874,60 @@ static function (Type $type, callable $traverse): Type { continue; } + $resolvingName = sprintf('%s::%s', $constantClassReflection->getName(), $constantName); + if (array_key_exists($resolvingName, $this->currentlyResolvingClassConstant)) { + $types[] = new MixedType(); + continue; + } + + $this->currentlyResolvingClassConstant[$resolvingName] = true; + + if (!$isObject) { + $reflectionConstant = $constantClassReflection->getNativeReflection()->getReflectionConstant($constantName); + if ($reflectionConstant === false) { + unset($this->currentlyResolvingClassConstant[$resolvingName]); + continue; + } + $reflectionConstantDeclaringClass = $reflectionConstant->getDeclaringClass(); + $constantType = $this->getType($reflectionConstant->getValueExpression(), InitializerExprContext::fromClass($reflectionConstantDeclaringClass->getName(), $reflectionConstantDeclaringClass->getFileName() ?: null)); + $nativeType = null; + if ($reflectionConstant->getType() !== null) { + $nativeType = TypehintHelper::decideTypeFromReflection($reflectionConstant->getType(), null, $constantClassReflection); + } + $types[] = $this->constantResolver->resolveClassConstantType( + $constantClassReflection->getName(), + $constantName, + $constantType, + $nativeType, + ); + unset($this->currentlyResolvingClassConstant[$resolvingName]); + continue; + } + $constantReflection = $constantClassReflection->getConstant($constantName); if ( - $constantReflection instanceof ClassConstantReflection - && $isObject - && !$constantClassReflection->isFinal() + !$constantClassReflection->isFinal() && !$constantReflection->hasPhpDocType() + && !$constantReflection->hasNativeType() ) { + unset($this->currentlyResolvingClassConstant[$resolvingName]); return new MixedType(); } - if ( - $isObject - && ( - !$constantReflection instanceof ClassConstantReflection - || !$constantClassReflection->isFinal() - ) - ) { + if (!$constantClassReflection->isFinal()) { $constantType = $constantReflection->getValueType(); } else { $constantType = $this->getType($constantReflection->getValueExpr(), InitializerExprContext::fromClassReflection($constantReflection->getDeclaringClass())); } - $constantType = $this->constantResolver->resolveConstantType( - sprintf('%s::%s', $constantClassReflection->getName(), $constantName), + $nativeType = $constantReflection->getNativeType(); + $constantType = $this->constantResolver->resolveClassConstantType( + $constantClassReflection->getName(), + $constantName, $constantType, + $nativeType, ); + unset($this->currentlyResolvingClassConstant[$resolvingName]); $types[] = $constantType; } @@ -1829,20 +1961,20 @@ public function getClassConstFetchType(Name|Expr $class, string $constantName, ? public function getUnaryMinusType(Expr $expr, callable $getTypeCallback): Type { $type = $getTypeCallback($expr)->toNumber(); - $scalarValues = TypeUtils::getConstantScalars($type); + $scalarValues = $type->getConstantScalarValues(); if (count($scalarValues) > 0) { $newTypes = []; foreach ($scalarValues as $scalarValue) { - if ($scalarValue instanceof ConstantIntegerType) { + if (is_int($scalarValue)) { /** @var int|float $newValue */ - $newValue = -$scalarValue->getValue(); + $newValue = -$scalarValue; if (!is_int($newValue)) { return $type; } $newTypes[] = new ConstantIntegerType($newValue); - } elseif ($scalarValue instanceof ConstantFloatType) { - $newTypes[] = new ConstantFloatType(-$scalarValue->getValue()); + } elseif (is_float($scalarValue)) { + $newTypes[] = new ConstantFloatType(-$scalarValue); } } @@ -1881,7 +2013,7 @@ public function getBitwiseNotType(Expr $expr, callable $getTypeCallback): Type return TypeCombinator::intersect(...$accessories); } - if ($type instanceof IntegerType || $type instanceof FloatType) { + if ($type->isInteger()->yes() || $type->isFloat()->yes()) { return new IntegerType(); //no const types here, result depends on PHP_INT_SIZE } return new ErrorType(); @@ -1938,4 +2070,16 @@ private function getReflectionProvider(): ReflectionProvider return $this->reflectionProviderProvider->getReflectionProvider(); } + private function getNeverType(Type $leftType, Type $rightType): Type + { + // make sure we don't lose the explicit flag in the process + if ($leftType instanceof NeverType && $leftType->isExplicit()) { + return $leftType; + } + if ($rightType instanceof NeverType && $rightType->isExplicit()) { + return $rightType; + } + return new NeverType(); + } + } diff --git a/src/Reflection/MethodPrototypeReflection.php b/src/Reflection/MethodPrototypeReflection.php index 648f161df0..cf1a6ff76b 100644 --- a/src/Reflection/MethodPrototypeReflection.php +++ b/src/Reflection/MethodPrototypeReflection.php @@ -18,6 +18,7 @@ public function __construct( private bool $isPublic, private bool $isAbstract, private bool $isFinal, + private bool $isInternal, private array $variants, private ?Type $tentativeReturnType, ) @@ -59,6 +60,11 @@ public function isFinal(): bool return $this->isFinal; } + public function isInternal(): bool + { + return $this->isInternal; + } + public function getDocComment(): ?string { return null; diff --git a/src/Reflection/MethodsClassReflectionExtension.php b/src/Reflection/MethodsClassReflectionExtension.php index 6b12291b0b..5817ac7657 100644 --- a/src/Reflection/MethodsClassReflectionExtension.php +++ b/src/Reflection/MethodsClassReflectionExtension.php @@ -2,7 +2,23 @@ namespace PHPStan\Reflection; -/** @api */ +/** + * This is the interface custom methods class reflection extensions implement. + * + * To register it in the configuration file use the `phpstan.broker.methodsClassReflectionExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyMethodsClassReflectionExtension + * tags: + * - phpstan.broker.methodsClassReflectionExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/class-reflection-extensions + * + * @api + */ interface MethodsClassReflectionExtension { diff --git a/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php b/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php index be29968120..484690a8b7 100644 --- a/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php +++ b/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php @@ -7,7 +7,6 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; use function array_intersect; use function count; @@ -44,7 +43,7 @@ private function findMethod(ClassReflection $classReflection, string $methodName { $mixinTypes = $classReflection->getResolvedMixinTypes(); foreach ($mixinTypes as $type) { - if (count(array_intersect(TypeUtils::getDirectClassNames($type), $this->mixinExcludeClasses)) > 0) { + if (count(array_intersect($type->getObjectClassNames(), $this->mixinExcludeClasses)) > 0) { continue; } @@ -75,13 +74,14 @@ private function findMethod(ClassReflection $classReflection, string $methodName return new MixinMethodReflection($method, $static); } - foreach ($classReflection->getParents() as $parentClass) { + $parentClass = $classReflection->getParentClass(); + while ($parentClass !== null) { $method = $this->findMethod($parentClass, $methodName); - if ($method === null) { - continue; + if ($method !== null) { + return $method; } - return $method; + $parentClass = $parentClass->getParentClass(); } return null; diff --git a/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php b/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php index 7e60150737..b891da3894 100644 --- a/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php +++ b/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php @@ -7,7 +7,6 @@ use PHPStan\Reflection\PropertiesClassReflectionExtension; use PHPStan\Reflection\PropertyReflection; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; use function array_intersect; use function count; @@ -44,7 +43,7 @@ private function findProperty(ClassReflection $classReflection, string $property { $mixinTypes = $classReflection->getResolvedMixinTypes(); foreach ($mixinTypes as $type) { - if (count(array_intersect(TypeUtils::getDirectClassNames($type), $this->mixinExcludeClasses)) > 0) { + if (count(array_intersect($type->getObjectClassNames(), $this->mixinExcludeClasses)) > 0) { continue; } @@ -66,13 +65,14 @@ private function findProperty(ClassReflection $classReflection, string $property return $property; } - foreach ($classReflection->getParents() as $parentClass) { + $parentClass = $classReflection->getParentClass(); + while ($parentClass !== null) { $property = $this->findProperty($parentClass, $propertyName); - if ($property === null) { - continue; + if ($property !== null) { + return $property; } - return $property; + $parentClass = $parentClass->getParentClass(); } return null; diff --git a/src/Reflection/Native/NativeFunctionReflection.php b/src/Reflection/Native/NativeFunctionReflection.php index 2142675d94..ff52194908 100644 --- a/src/Reflection/Native/NativeFunctionReflection.php +++ b/src/Reflection/Native/NativeFunctionReflection.php @@ -2,26 +2,37 @@ namespace PHPStan\Reflection\Native; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -use PHPStan\Type\VoidType; class NativeFunctionReflection implements FunctionReflection { + private Assertions $assertions; + + private TrinaryLogic $returnsByReference; + /** - * @param ParametersAcceptor[] $variants + * @param ParametersAcceptorWithPhpDocs[] $variants + * @param ParametersAcceptorWithPhpDocs[]|null $namedArgumentsVariants */ public function __construct( private string $name, private array $variants, + private ?array $namedArgumentsVariants, private ?Type $throwType, private TrinaryLogic $hasSideEffects, private bool $isDeprecated, + ?Assertions $assertions = null, + private ?string $phpDocComment = null, + ?TrinaryLogic $returnsByReference = null, ) { + $this->assertions = $assertions ?? Assertions::createEmpty(); + $this->returnsByReference = $returnsByReference ?? TrinaryLogic::createMaybe(); } public function getName(): string @@ -35,13 +46,18 @@ public function getFileName(): ?string } /** - * @return ParametersAcceptor[] + * @return ParametersAcceptorWithPhpDocs[] */ public function getVariants(): array { return $this->variants; } + public function getNamedArgumentsVariants(): ?array + { + return $this->namedArgumentsVariants; + } + public function getThrowType(): ?Type { return $this->throwType; @@ -76,10 +92,19 @@ public function hasSideEffects(): TrinaryLogic return $this->hasSideEffects; } + public function isPure(): TrinaryLogic + { + if ($this->hasSideEffects()->yes()) { + return TrinaryLogic::createNo(); + } + + return $this->hasSideEffects->negate(); + } + private function isVoid(): bool { foreach ($this->variants as $variant) { - if (!$variant->getReturnType() instanceof VoidType) { + if (!$variant->getReturnType()->isVoid()->yes()) { return false; } } @@ -92,4 +117,19 @@ public function isBuiltin(): bool return true; } + public function getAsserts(): Assertions + { + return $this->assertions; + } + + public function getDocComment(): ?string + { + return $this->phpDocComment; + } + + public function returnsByReference(): TrinaryLogic + { + return $this->returnsByReference; + } + } diff --git a/src/Reflection/Native/NativeMethodReflection.php b/src/Reflection/Native/NativeMethodReflection.php index 2f96c22da0..9c6434286b 100644 --- a/src/Reflection/Native/NativeMethodReflection.php +++ b/src/Reflection/Native/NativeMethodReflection.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\Native; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; @@ -12,7 +13,6 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; -use PHPStan\Type\VoidType; use ReflectionException; use function strtolower; @@ -21,14 +21,19 @@ class NativeMethodReflection implements ExtendedMethodReflection /** * @param ParametersAcceptorWithPhpDocs[] $variants + * @param ParametersAcceptorWithPhpDocs[]|null $namedArgumentsVariants */ public function __construct( private ReflectionProvider $reflectionProvider, private ClassReflection $declaringClass, private BuiltinMethodReflection $reflection, private array $variants, + private ?array $namedArgumentsVariants, private TrinaryLogic $hasSideEffects, private ?Type $throwType, + private Assertions $assertions, + private ?Type $selfOutType, + private ?string $phpDocComment, ) { } @@ -53,16 +58,19 @@ public function isPublic(): bool return $this->reflection->isPublic(); } - public function isAbstract(): bool + public function isAbstract(): TrinaryLogic { - return $this->reflection->isAbstract(); + return TrinaryLogic::createFromBoolean($this->reflection->isAbstract()); } public function getPrototype(): ClassMemberReflection { try { $prototypeMethod = $this->reflection->getPrototype(); - $prototypeDeclaringClass = $this->reflectionProvider->getClass($prototypeMethod->getDeclaringClass()->getName()); + $prototypeDeclaringClass = $this->declaringClass->getAncestorWithClassName($prototypeMethod->getDeclaringClass()->getName()); + if ($prototypeDeclaringClass === null) { + $prototypeDeclaringClass = $this->reflectionProvider->getClass($prototypeMethod->getDeclaringClass()->getName()); + } $tentativeReturnType = null; if ($prototypeMethod->getTentativeReturnType() !== null) { @@ -77,6 +85,7 @@ public function getPrototype(): ClassMemberReflection $prototypeMethod->isPublic(), $prototypeMethod->isAbstract(), $prototypeMethod->isFinal(), + $prototypeMethod->isInternal(), $prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants(), $tentativeReturnType, ); @@ -90,14 +99,16 @@ public function getName(): string return $this->reflection->getName(); } - /** - * @return ParametersAcceptorWithPhpDocs[] - */ public function getVariants(): array { return $this->variants; } + public function getNamedArgumentsVariants(): ?array + { + return $this->namedArgumentsVariants; + } + public function getDeprecatedDescription(): ?string { return null; @@ -110,7 +121,7 @@ public function isDeprecated(): TrinaryLogic public function isInternal(): TrinaryLogic { - return TrinaryLogic::createNo(); + return TrinaryLogic::createFromBoolean($this->reflection->isInternal()); } public function isFinal(): TrinaryLogic @@ -118,6 +129,11 @@ public function isFinal(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->reflection->isFinal()); } + public function isFinalByKeyword(): TrinaryLogic + { + return $this->isFinal(); + } + public function getThrowType(): ?Type { return $this->throwType; @@ -137,10 +153,19 @@ public function hasSideEffects(): TrinaryLogic return $this->hasSideEffects; } + public function isPure(): TrinaryLogic + { + if ($this->hasSideEffects()->yes()) { + return TrinaryLogic::createNo(); + } + + return $this->hasSideEffects->negate(); + } + private function isVoid(): bool { foreach ($this->variants as $variant) { - if (!$variant->getReturnType() instanceof VoidType) { + if (!$variant->getReturnType()->isVoid()->yes()) { return false; } } @@ -150,7 +175,22 @@ private function isVoid(): bool public function getDocComment(): ?string { - return $this->reflection->getDocComment(); + return $this->phpDocComment; + } + + public function getAsserts(): Assertions + { + return $this->assertions; + } + + public function getSelfOutType(): ?Type + { + return $this->selfOutType; + } + + public function returnsByReference(): TrinaryLogic + { + return $this->reflection->returnsByReference(); } } diff --git a/src/Reflection/Native/NativeParameterReflection.php b/src/Reflection/Native/NativeParameterReflection.php index 219f48069d..7f92bfdb5e 100644 --- a/src/Reflection/Native/NativeParameterReflection.php +++ b/src/Reflection/Native/NativeParameterReflection.php @@ -5,6 +5,7 @@ use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\PassedByReference; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; class NativeParameterReflection implements ParameterReflection { @@ -50,6 +51,18 @@ public function getDefaultValue(): ?Type return $this->defaultValue; } + public function union(self $other): self + { + return new self( + $this->name, + $this->optional && $other->optional, + TypeCombinator::union($this->type, $other->type), + $this->passedByReference->combine($other->passedByReference), + $this->variadic && $other->variadic, + $this->optional && $other->optional ? $this->defaultValue : null, + ); + } + /** * @param mixed[] $properties */ diff --git a/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php b/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php index b5c01f9b90..3a81fbb4da 100644 --- a/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php +++ b/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php @@ -4,6 +4,7 @@ use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\PassedByReference; +use PHPStan\TrinaryLogic; use PHPStan\Type\Type; class NativeParameterWithPhpDocsReflection implements ParameterReflectionWithPhpDocs @@ -18,6 +19,9 @@ public function __construct( private PassedByReference $passedByReference, private bool $variadic, private ?Type $defaultValue, + private ?Type $outType, + private TrinaryLogic $immediatelyInvokedCallable, + private ?Type $closureThisType, ) { } @@ -62,6 +66,21 @@ public function getDefaultValue(): ?Type return $this->defaultValue; } + public function getOutType(): ?Type + { + return $this->outType; + } + + public function isImmediatelyInvokedCallable(): TrinaryLogic + { + return $this->immediatelyInvokedCallable; + } + + public function getClosureThisType(): ?Type + { + return $this->closureThisType; + } + /** * @param mixed[] $properties */ @@ -76,6 +95,9 @@ public static function __set_state(array $properties): self $properties['passedByReference'], $properties['variadic'], $properties['defaultValue'], + $properties['outType'], + $properties['immediatelyInvokedCallable'], + $properties['closureThisType'], ); } diff --git a/src/Reflection/PHPStan/NativeReflectionEnumReturnDynamicReturnTypeExtension.php b/src/Reflection/PHPStan/NativeReflectionEnumReturnDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..d0b71b91a7 --- /dev/null +++ b/src/Reflection/PHPStan/NativeReflectionEnumReturnDynamicReturnTypeExtension.php @@ -0,0 +1,40 @@ +className; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === $this->methodName; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if ($this->phpVersion->getVersionId() >= 80000) { + return null; + } + + return new ObjectType(ReflectionClass::class); + } + +} diff --git a/src/Reflection/ParameterReflectionWithPhpDocs.php b/src/Reflection/ParameterReflectionWithPhpDocs.php index e0ede3fd51..943338a493 100644 --- a/src/Reflection/ParameterReflectionWithPhpDocs.php +++ b/src/Reflection/ParameterReflectionWithPhpDocs.php @@ -2,8 +2,10 @@ namespace PHPStan\Reflection; +use PHPStan\TrinaryLogic; use PHPStan\Type\Type; +/** @api */ interface ParameterReflectionWithPhpDocs extends ParameterReflection { @@ -11,4 +13,10 @@ public function getPhpDocType(): Type; public function getNativeType(): Type; + public function getOutType(): ?Type; + + public function isImmediatelyInvokedCallable(): TrinaryLogic; + + public function getClosureThisType(): ?Type; + } diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 895eeab003..c6f597313a 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -2,14 +2,20 @@ namespace PHPStan\Reflection; +use Closure; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; use PHPStan\Parser\ArrayFilterArgVisitor; use PHPStan\Parser\ArrayMapArgVisitor; use PHPStan\Parser\ArrayWalkArgVisitor; +use PHPStan\Parser\ClosureBindArgVisitor; +use PHPStan\Parser\ClosureBindToVarVisitor; use PHPStan\Parser\CurlSetOptArgVisitor; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\Php\DummyParameter; +use PHPStan\Reflection\Php\DummyParameterWithPhpDocs; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -17,24 +23,29 @@ use PHPStan\Type\BooleanType; use PHPStan\Type\CallableType; use PHPStan\Type\Constant\ConstantIntegerType; -use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\IntegerType; use PHPStan\Type\LateResolvableType; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; +use PHPStan\Type\ObjectType; use PHPStan\Type\ResourceType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; use PHPStan\Type\UnionType; +use function array_key_exists; use function array_key_last; +use function array_map; +use function array_merge; use function array_slice; use function constant; use function count; use function defined; +use function is_string; use function sprintf; use const ARRAY_FILTER_USE_BOTH; use const ARRAY_FILTER_USE_KEY; @@ -69,11 +80,13 @@ public static function selectSingle( /** * @param Node\Arg[] $args * @param ParametersAcceptor[] $parametersAcceptors + * @param ParametersAcceptor[]|null $namedArgumentsVariants */ public static function selectFromArgs( Scope $scope, array $args, array $parametersAcceptors, + ?array $namedArgumentsVariants = null, ): ParametersAcceptor { $types = []; @@ -88,7 +101,7 @@ public static function selectFromArgs( $parameters = $acceptor->getParameters(); $callbackParameters = []; foreach ($arrayMapArgs as $arg) { - $callbackParameters[] = new DummyParameter('item', self::getIterableValueType($scope->getType($arg->value)), false, PassedByReference::createNo(), false, null); + $callbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($scope->getType($arg->value)), false, PassedByReference::createNo(), false, null); } $parameters[0] = new NativeParameterReflection( $parameters[0]->getName(), @@ -108,6 +121,7 @@ public static function selectFromArgs( $parameters, $acceptor->isVariadic(), $acceptor->getReturnType(), + $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), ), ]; } @@ -137,6 +151,7 @@ public static function selectFromArgs( $parameters, $acceptor->isVariadic(), $acceptor->getReturnType(), + $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), ), ]; } @@ -149,12 +164,12 @@ public static function selectFromArgs( if ($mode instanceof ConstantIntegerType) { if ($mode->getValue() === ARRAY_FILTER_USE_KEY) { $arrayFilterParameters = [ - new DummyParameter('key', self::getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), + new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), ]; } elseif ($mode->getValue() === ARRAY_FILTER_USE_BOTH) { $arrayFilterParameters = [ - new DummyParameter('item', self::getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), - new DummyParameter('key', self::getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), + new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), + new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), ]; } } @@ -165,13 +180,16 @@ public static function selectFromArgs( $parameters[1] = new NativeParameterReflection( $parameters[1]->getName(), $parameters[1]->isOptional(), - new CallableType( - $arrayFilterParameters ?? [ - new DummyParameter('item', self::getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), - ], - new MixedType(), - false, - ), + new UnionType([ + new CallableType( + $arrayFilterParameters ?? [ + new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), + ], + new BooleanType(), + false, + ), + new NullType(), + ]), $parameters[1]->passedByReference(), $parameters[1]->isVariadic(), $parameters[1]->getDefaultValue(), @@ -183,14 +201,15 @@ public static function selectFromArgs( $parameters, $acceptor->isVariadic(), $acceptor->getReturnType(), + $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), ), ]; } if (isset($args[0]) && (bool) $args[0]->getAttribute(ArrayWalkArgVisitor::ATTRIBUTE_NAME)) { $arrayWalkParameters = [ - new DummyParameter('item', self::getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createReadsArgument(), false, null), - new DummyParameter('key', self::getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), + new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createReadsArgument(), false, null), + new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), ]; if (isset($args[2])) { $arrayWalkParameters[] = new DummyParameter('arg', $scope->getType($args[2]->value), false, PassedByReference::createNo(), false, null); @@ -213,9 +232,101 @@ public static function selectFromArgs( $parameters, $acceptor->isVariadic(), $acceptor->getReturnType(), + $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), ), ]; } + + if (isset($args[0])) { + $closureBindToVar = $args[0]->getAttribute(ClosureBindToVarVisitor::ATTRIBUTE_NAME); + if ( + $closureBindToVar !== null + && $closureBindToVar instanceof Node\Expr\Variable + && is_string($closureBindToVar->name) + ) { + $varType = $scope->getType($closureBindToVar); + if ((new ObjectType(Closure::class))->isSuperTypeOf($varType)->yes()) { + $inFunction = $scope->getFunction(); + if ($inFunction !== null) { + $inFunctionVariant = self::selectSingle($inFunction->getVariants()); + $closureThisParameters = []; + foreach ($inFunctionVariant->getParameters() as $parameter) { + if ($parameter->getClosureThisType() === null) { + continue; + } + $closureThisParameters[$parameter->getName()] = $parameter->getClosureThisType(); + } + if (array_key_exists($closureBindToVar->name, $closureThisParameters)) { + if ($scope->hasExpressionType(new ParameterVariableOriginalValueExpr($closureBindToVar->name))->yes()) { + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + $parameters[0] = new NativeParameterReflection( + $parameters[0]->getName(), + $parameters[0]->isOptional(), + $closureThisParameters[$closureBindToVar->name], + $parameters[0]->passedByReference(), + $parameters[0]->isVariadic(), + $parameters[0]->getDefaultValue(), + ); + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + $parameters, + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + } + } + } + } + + if ( + $args[0]->getAttribute(ClosureBindArgVisitor::ATTRIBUTE_NAME) !== null + && $args[0]->value instanceof Node\Expr\Variable + && is_string($args[0]->value->name) + ) { + $closureVarName = $args[0]->value->name; + $inFunction = $scope->getFunction(); + if ($inFunction !== null) { + $inFunctionVariant = self::selectSingle($inFunction->getVariants()); + $closureThisParameters = []; + foreach ($inFunctionVariant->getParameters() as $parameter) { + if ($parameter->getClosureThisType() === null) { + continue; + } + $closureThisParameters[$parameter->getName()] = $parameter->getClosureThisType(); + } + if (array_key_exists($closureVarName, $closureThisParameters)) { + if ($scope->hasExpressionType(new ParameterVariableOriginalValueExpr($closureVarName))->yes()) { + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + $parameters[1] = new NativeParameterReflection( + $parameters[1]->getName(), + $parameters[1]->isOptional(), + $closureThisParameters[$closureVarName], + $parameters[1]->passedByReference(), + $parameters[1]->isVariadic(), + $parameters[1]->getDefaultValue(), + ); + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + $parameters, + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + } + } + } + } } if (count($parametersAcceptors) === 1) { @@ -225,9 +336,15 @@ public static function selectFromArgs( } } + $hasName = false; foreach ($args as $i => $arg) { $type = $scope->getType($arg->value); - $index = $arg->name !== null ? $arg->name->toString() : $i; + if ($arg->name !== null) { + $index = $arg->name->toString(); + $hasName = true; + } else { + $index = $i; + } if ($arg->unpack) { $unpack = true; $types[$index] = $type->getIterableValueType(); @@ -236,6 +353,10 @@ public static function selectFromArgs( } } + if ($hasName && $namedArgumentsVariants !== null) { + return self::selectFromTypes($types, $namedArgumentsVariants, $unpack); + } + return self::selectFromTypes($types, $parametersAcceptors, $unpack); } @@ -246,6 +367,22 @@ private static function hasAcceptorTemplateOrLateResolvableType(ParametersAccept } foreach ($acceptor->getParameters() as $parameter) { + if ( + $parameter instanceof ParameterReflectionWithPhpDocs + && $parameter->getOutType() !== null + && self::hasTemplateOrLateResolvableType($parameter->getOutType()) + ) { + return true; + } + + if ( + $parameter instanceof ParameterReflectionWithPhpDocs + && $parameter->getClosureThisType() !== null + && self::hasTemplateOrLateResolvableType($parameter->getClosureThisType()) + ) { + return true; + } + if (!self::hasTemplateOrLateResolvableType($parameter->getType())) { continue; } @@ -383,7 +520,7 @@ public static function selectFromTypes( /** * @param ParametersAcceptor[] $acceptors */ - public static function combineAcceptors(array $acceptors): ParametersAcceptor + public static function combineAcceptors(array $acceptors): ParametersAcceptorWithPhpDocs { if (count($acceptors) === 0) { throw new ShouldNotHappenException( @@ -391,7 +528,7 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptor ); } if (count($acceptors) === 1) { - return $acceptors[0]; + return self::wrapAcceptor($acceptors[0]); } $minimumNumberOfParameters = null; @@ -414,25 +551,47 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptor $parameters = []; $isVariadic = false; - $returnType = null; + $returnTypes = []; + $phpDocReturnTypes = []; + $nativeReturnTypes = []; + $callableOccurred = false; + $throwPoints = []; + $isPure = TrinaryLogic::createNo(); + $impurePoints = []; + $invalidateExpressions = []; + $usedVariables = []; foreach ($acceptors as $acceptor) { - if ($returnType === null) { - $returnType = $acceptor->getReturnType(); - } else { - $returnType = TypeCombinator::union($returnType, $acceptor->getReturnType()); + $returnTypes[] = $acceptor->getReturnType(); + + if ($acceptor instanceof ParametersAcceptorWithPhpDocs) { + $phpDocReturnTypes[] = $acceptor->getPhpDocReturnType(); + $nativeReturnTypes[] = $acceptor->getNativeReturnType(); + } + if ($acceptor instanceof CallableParametersAcceptor) { + $callableOccurred = true; + $throwPoints = array_merge($throwPoints, $acceptor->getThrowPoints()); + $isPure = $isPure->or($acceptor->isPure()); + $impurePoints = array_merge($impurePoints, $acceptor->getImpurePoints()); + $invalidateExpressions = array_merge($invalidateExpressions, $acceptor->getInvalidateExpressions()); + $usedVariables = array_merge($usedVariables, $acceptor->getUsedVariables()); } $isVariadic = $isVariadic || $acceptor->isVariadic(); foreach ($acceptor->getParameters() as $i => $parameter) { if (!isset($parameters[$i])) { - $parameters[$i] = new NativeParameterReflection( + $parameters[$i] = new DummyParameterWithPhpDocs( $parameter->getName(), - $i + 1 > $minimumNumberOfParameters, $parameter->getType(), + $i + 1 > $minimumNumberOfParameters, $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue(), + $parameter instanceof ParameterReflectionWithPhpDocs ? $parameter->getNativeType() : new MixedType(), + $parameter instanceof ParameterReflectionWithPhpDocs ? $parameter->getPhpDocType() : new MixedType(), + $parameter instanceof ParameterReflectionWithPhpDocs ? $parameter->getOutType() : null, + $parameter instanceof ParameterReflectionWithPhpDocs ? $parameter->isImmediatelyInvokedCallable() : TrinaryLogic::createMaybe(), + $parameter instanceof ParameterReflectionWithPhpDocs ? $parameter->getClosureThisType() : null, ); continue; } @@ -446,13 +605,49 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptor $defaultValue = null; } - $parameters[$i] = new NativeParameterReflection( + $type = TypeCombinator::union($parameters[$i]->getType(), $parameter->getType()); + $nativeType = $parameters[$i]->getNativeType(); + $phpDocType = $parameters[$i]->getPhpDocType(); + $outType = $parameters[$i]->getOutType(); + $immediatelyInvokedCallable = $parameters[$i]->isImmediatelyInvokedCallable(); + $closureThisType = $parameters[$i]->getClosureThisType(); + if ($parameter instanceof ParameterReflectionWithPhpDocs) { + $nativeType = TypeCombinator::union($nativeType, $parameter->getNativeType()); + $phpDocType = TypeCombinator::union($phpDocType, $parameter->getPhpDocType()); + + if ($parameter->getOutType() !== null) { + $outType = $outType === null ? null : TypeCombinator::union($outType, $parameter->getOutType()); + } else { + $outType = null; + } + + if ($parameter->getClosureThisType() !== null && $closureThisType !== null) { + $closureThisType = TypeCombinator::union($closureThisType, $parameter->getClosureThisType()); + } else { + $closureThisType = null; + } + + $immediatelyInvokedCallable = $parameter->isImmediatelyInvokedCallable()->or($immediatelyInvokedCallable); + } else { + $nativeType = new MixedType(); + $phpDocType = $type; + $outType = null; + $immediatelyInvokedCallable = TrinaryLogic::createMaybe(); + $closureThisType = null; + } + + $parameters[$i] = new DummyParameterWithPhpDocs( $parameters[$i]->getName() !== $parameter->getName() ? sprintf('%s|%s', $parameters[$i]->getName(), $parameter->getName()) : $parameter->getName(), + $type, $i + 1 > $minimumNumberOfParameters, - TypeCombinator::union($parameters[$i]->getType(), $parameter->getType()), $parameters[$i]->passedByReference()->combine($parameter->passedByReference()), $isVariadic, $defaultValue, + $nativeType, + $phpDocType, + $outType, + $immediatelyInvokedCallable, + $closureThisType, ); if ($isVariadic) { @@ -462,51 +657,90 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptor } } - return new FunctionVariant( + $returnType = TypeCombinator::union(...$returnTypes); + $phpDocReturnType = $phpDocReturnTypes === [] ? null : TypeCombinator::union(...$phpDocReturnTypes); + $nativeReturnType = $nativeReturnTypes === [] ? null : TypeCombinator::union(...$nativeReturnTypes); + + if ($callableOccurred) { + return new CallableFunctionVariantWithPhpDocs( + TemplateTypeMap::createEmpty(), + null, + $parameters, + $isVariadic, + $returnType, + $phpDocReturnType ?? $returnType, + $nativeReturnType ?? new MixedType(), + null, + $throwPoints, + $isPure, + $impurePoints, + $invalidateExpressions, + $usedVariables, + ); + } + + return new FunctionVariantWithPhpDocs( TemplateTypeMap::createEmpty(), null, $parameters, $isVariadic, $returnType, + $phpDocReturnType ?? $returnType, + $nativeReturnType ?? new MixedType(), ); } - private static function getIterableValueType(Type $type): Type + private static function wrapAcceptor(ParametersAcceptor $acceptor): ParametersAcceptorWithPhpDocs { - if ($type instanceof UnionType) { - $types = []; - foreach ($type->getTypes() as $innerType) { - $iterableValueType = $innerType->getIterableValueType(); - if ($iterableValueType instanceof ErrorType) { - continue; - } - - $types[] = $iterableValueType; - } + if ($acceptor instanceof ParametersAcceptorWithPhpDocs) { + return $acceptor; + } - return TypeCombinator::union(...$types); + if ($acceptor instanceof CallableParametersAcceptor) { + return new CallableFunctionVariantWithPhpDocs( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + array_map(static fn (ParameterReflection $parameter): ParameterReflectionWithPhpDocs => self::wrapParameter($parameter), $acceptor->getParameters()), + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor->getReturnType(), + new MixedType(), + TemplateTypeVarianceMap::createEmpty(), + $acceptor->getThrowPoints(), + $acceptor->isPure(), + $acceptor->getImpurePoints(), + $acceptor->getInvalidateExpressions(), + $acceptor->getUsedVariables(), + ); } - return $type->getIterableValueType(); + return new FunctionVariantWithPhpDocs( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + array_map(static fn (ParameterReflection $parameter): ParameterReflectionWithPhpDocs => self::wrapParameter($parameter), $acceptor->getParameters()), + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor->getReturnType(), + new MixedType(), + TemplateTypeVarianceMap::createEmpty(), + ); } - private static function getIterableKeyType(Type $type): Type + private static function wrapParameter(ParameterReflection $parameter): ParameterReflectionWithPhpDocs { - if ($type instanceof UnionType) { - $types = []; - foreach ($type->getTypes() as $innerType) { - $iterableKeyType = $innerType->getIterableKeyType(); - if ($iterableKeyType instanceof ErrorType) { - continue; - } - - $types[] = $iterableKeyType; - } - - return TypeCombinator::union(...$types); - } - - return $type->getIterableKeyType(); + return $parameter instanceof ParameterReflectionWithPhpDocs ? $parameter : new DummyParameterWithPhpDocs( + $parameter->getName(), + $parameter->getType(), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + new MixedType(), + $parameter->getType(), + null, + TrinaryLogic::createMaybe(), + null, + ); } private static function getCurlOptValueType(int $curlOpt): ?Type @@ -708,10 +942,18 @@ private static function getCurlOptValueType(int $curlOpt): ?Type } } + $intArrayStringKeysConstants = [ + 'CURLOPT_HTTPHEADER', + ]; + foreach ($intArrayStringKeysConstants as $constName) { + if (defined($constName) && constant($constName) === $curlOpt) { + return new ArrayType(new IntegerType(), new StringType()); + } + } + $arrayConstants = [ 'CURLOPT_CONNECT_TO', 'CURLOPT_HTTP200ALIASES', - 'CURLOPT_HTTPHEADER', 'CURLOPT_POSTQUOTE', 'CURLOPT_PROXYHEADER', 'CURLOPT_QUOTE', diff --git a/src/Reflection/ParametersAcceptorWithPhpDocs.php b/src/Reflection/ParametersAcceptorWithPhpDocs.php index 11200bc1f5..f8ae03e477 100644 --- a/src/Reflection/ParametersAcceptorWithPhpDocs.php +++ b/src/Reflection/ParametersAcceptorWithPhpDocs.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; /** @api */ @@ -17,4 +18,6 @@ public function getPhpDocReturnType(): Type; public function getNativeReturnType(): Type; + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap; + } diff --git a/src/Reflection/Php/BuiltinMethodReflection.php b/src/Reflection/Php/BuiltinMethodReflection.php index 51f90c87b9..d2d55336aa 100644 --- a/src/Reflection/Php/BuiltinMethodReflection.php +++ b/src/Reflection/Php/BuiltinMethodReflection.php @@ -55,4 +55,6 @@ public function isInternal(): bool; public function isAbstract(): bool; + public function returnsByReference(): TrinaryLogic; + } diff --git a/src/Reflection/Php/ClosureCallMethodReflection.php b/src/Reflection/Php/ClosureCallMethodReflection.php index ac25944bdd..7e2b402bf1 100644 --- a/src/Reflection/Php/ClosureCallMethodReflection.php +++ b/src/Reflection/Php/ClosureCallMethodReflection.php @@ -2,24 +2,29 @@ namespace PHPStan\Reflection\Php; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\FunctionVariant; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\FunctionVariantWithPhpDocs; use PHPStan\Reflection\Native\NativeParameterReflection; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ParameterReflection; +use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\ClosureType; +use PHPStan\Type\MixedType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; +use function array_map; use function array_unshift; +use function is_bool; -final class ClosureCallMethodReflection implements MethodReflection +final class ClosureCallMethodReflection implements ExtendedMethodReflection { public function __construct( - private MethodReflection $nativeMethodReflection, + private ExtendedMethodReflection $nativeMethodReflection, private ClosureType $closureType, ) { @@ -60,9 +65,6 @@ public function getPrototype(): ClassMemberReflection return $this->nativeMethodReflection->getPrototype(); } - /** - * @return ParametersAcceptor[] - */ public function getVariants(): array { $parameters = $this->closureType->getParameters(); @@ -78,16 +80,36 @@ public function getVariants(): array array_unshift($parameters, $newThis); return [ - new FunctionVariant( + new FunctionVariantWithPhpDocs( $this->closureType->getTemplateTypeMap(), $this->closureType->getResolvedTemplateTypeMap(), - $parameters, + array_map(static fn (ParameterReflection $parameter): ParameterReflectionWithPhpDocs => new DummyParameterWithPhpDocs( + $parameter->getName(), + $parameter->getType(), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + new MixedType(), + $parameter->getType(), + null, + TrinaryLogic::createMaybe(), + null, + ), $parameters), $this->closureType->isVariadic(), $this->closureType->getReturnType(), + $this->closureType->getReturnType(), + new MixedType(), + $this->closureType->getCallSiteVarianceMap(), ), ]; } + public function getNamedArgumentsVariants(): ?array + { + return null; + } + public function isDeprecated(): TrinaryLogic { return $this->nativeMethodReflection->isDeprecated(); @@ -103,6 +125,11 @@ public function isFinal(): TrinaryLogic return $this->nativeMethodReflection->isFinal(); } + public function isFinalByKeyword(): TrinaryLogic + { + return $this->nativeMethodReflection->isFinalByKeyword(); + } + public function isInternal(): TrinaryLogic { return $this->nativeMethodReflection->isInternal(); @@ -118,4 +145,34 @@ public function hasSideEffects(): TrinaryLogic return $this->nativeMethodReflection->hasSideEffects(); } + public function getAsserts(): Assertions + { + return $this->nativeMethodReflection->getAsserts(); + } + + public function getSelfOutType(): ?Type + { + return $this->nativeMethodReflection->getSelfOutType(); + } + + public function returnsByReference(): TrinaryLogic + { + return $this->nativeMethodReflection->returnsByReference(); + } + + public function isAbstract(): TrinaryLogic + { + $abstract = $this->nativeMethodReflection->isAbstract(); + if (is_bool($abstract)) { + return TrinaryLogic::createFromBoolean($abstract); + } + + return $abstract; + } + + public function isPure(): TrinaryLogic + { + return $this->nativeMethodReflection->isPure(); + } + } diff --git a/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php b/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php index a67891dd95..bd3c9b54ed 100644 --- a/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php @@ -2,7 +2,7 @@ namespace PHPStan\Reflection\Php; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Type\ClosureType; use PHPStan\Type\Type; @@ -19,12 +19,12 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototype return new self($this->prototype->doNotResolveTemplateTypeMapToBounds(), $this->closure); } - public function getNakedMethod(): MethodReflection + public function getNakedMethod(): ExtendedMethodReflection { return $this->getTransformedMethod(); } - public function getTransformedMethod(): MethodReflection + public function getTransformedMethod(): ExtendedMethodReflection { return new ClosureCallMethodReflection($this->prototype->getTransformedMethod(), $this->closure); } diff --git a/src/Reflection/Php/DummyParameterWithPhpDocs.php b/src/Reflection/Php/DummyParameterWithPhpDocs.php new file mode 100644 index 0000000000..262b601bea --- /dev/null +++ b/src/Reflection/Php/DummyParameterWithPhpDocs.php @@ -0,0 +1,55 @@ +phpDocType; + } + + public function getNativeType(): Type + { + return $this->nativeType; + } + + public function getOutType(): ?Type + { + return $this->outType; + } + + public function isImmediatelyInvokedCallable(): TrinaryLogic + { + return $this->immediatelyInvokedCallable; + } + + public function getClosureThisType(): ?Type + { + return $this->closureThisType; + } + +} diff --git a/src/Reflection/Php/EnumAllowedSubTypesClassReflectionExtension.php b/src/Reflection/Php/EnumAllowedSubTypesClassReflectionExtension.php new file mode 100644 index 0000000000..3ba005da82 --- /dev/null +++ b/src/Reflection/Php/EnumAllowedSubTypesClassReflectionExtension.php @@ -0,0 +1,28 @@ +isEnum(); + } + + public function getAllowedSubTypes(ClassReflection $classReflection): array + { + $cases = []; + foreach (array_keys($classReflection->getEnumCases()) as $name) { + $cases[] = new EnumCaseObjectType($classReflection->getName(), $name); + } + + return $cases; + } + +} diff --git a/src/Reflection/Php/EnumCasesMethodReflection.php b/src/Reflection/Php/EnumCasesMethodReflection.php index 502e73d436..0665eee656 100644 --- a/src/Reflection/Php/EnumCasesMethodReflection.php +++ b/src/Reflection/Php/EnumCasesMethodReflection.php @@ -2,14 +2,15 @@ namespace PHPStan\Reflection\Php; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\FunctionVariant; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\FunctionVariantWithPhpDocs; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; class EnumCasesMethodReflection implements ExtendedMethodReflection @@ -59,22 +60,26 @@ public function getPrototype(): ClassMemberReflection return $unitEnum->getNativeMethod('cases'); } - /** - * @return ParametersAcceptor[] - */ public function getVariants(): array { return [ - new FunctionVariant( + new FunctionVariantWithPhpDocs( TemplateTypeMap::createEmpty(), TemplateTypeMap::createEmpty(), [], false, $this->returnType, + new MixedType(), + $this->returnType, ), ]; } + public function getNamedArgumentsVariants(): ?array + { + return null; + } + public function isDeprecated(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -90,6 +95,11 @@ public function isFinal(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function isInternal(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -105,4 +115,29 @@ public function hasSideEffects(): TrinaryLogic return TrinaryLogic::createNo(); } + public function getAsserts(): Assertions + { + return Assertions::createEmpty(); + } + + public function getSelfOutType(): ?Type + { + return null; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + } diff --git a/src/Reflection/Php/EnumUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Php/EnumUnresolvedPropertyPrototypeReflection.php new file mode 100644 index 0000000000..5d76711ce4 --- /dev/null +++ b/src/Reflection/Php/EnumUnresolvedPropertyPrototypeReflection.php @@ -0,0 +1,36 @@ +property; + } + + public function getTransformedProperty(): PropertyReflection + { + return $this->property; + } + + public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflection + { + return $this; + } + +} diff --git a/src/Reflection/Php/FakeBuiltinMethodReflection.php b/src/Reflection/Php/FakeBuiltinMethodReflection.php deleted file mode 100644 index 96e3bf18b6..0000000000 --- a/src/Reflection/Php/FakeBuiltinMethodReflection.php +++ /dev/null @@ -1,123 +0,0 @@ -methodName; - } - - public function getReflection(): ?ReflectionMethod - { - return null; - } - - public function getFileName(): ?string - { - return null; - } - - public function getDeclaringClass(): ReflectionClass|ReflectionEnum - { - return $this->declaringClass; - } - - public function getStartLine(): ?int - { - return null; - } - - public function getEndLine(): ?int - { - return null; - } - - public function getDocComment(): ?string - { - return null; - } - - public function isStatic(): bool - { - return false; - } - - public function isPrivate(): bool - { - return false; - } - - public function isPublic(): bool - { - return true; - } - - public function getPrototype(): BuiltinMethodReflection - { - throw new ReflectionException(); - } - - public function isDeprecated(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function isVariadic(): bool - { - return false; - } - - public function isFinal(): bool - { - return false; - } - - public function isInternal(): bool - { - return false; - } - - public function isAbstract(): bool - { - return false; - } - - public function getReturnType(): ReflectionIntersectionType|ReflectionNamedType|ReflectionUnionType|null - { - return null; - } - - public function getTentativeReturnType(): ReflectionIntersectionType|ReflectionNamedType|ReflectionUnionType|null - { - return null; - } - - /** - * @return ReflectionParameter[] - */ - public function getParameters(): array - { - return []; - } - -} diff --git a/src/Reflection/Php/NativeBuiltinMethodReflection.php b/src/Reflection/Php/NativeBuiltinMethodReflection.php index 992219233e..2f10d431c5 100644 --- a/src/Reflection/Php/NativeBuiltinMethodReflection.php +++ b/src/Reflection/Php/NativeBuiltinMethodReflection.php @@ -141,4 +141,9 @@ public function getParameters(): array return $this->reflection->getParameters(); } + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->reflection->returnsReference()); + } + } diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 59ab947635..83a9df25eb 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -18,6 +18,7 @@ use PHPStan\PhpDoc\StubPhpDocProvider; use PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension; use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\FunctionVariantWithPhpDocs; @@ -34,7 +35,6 @@ use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Enum\EnumCaseObjectType; @@ -43,6 +43,7 @@ use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; @@ -81,9 +82,6 @@ class PhpClassReflectionExtension /** @var array */ private array $inferClassConstructorPropertyTypesInProcess = []; - /** - * @param string[] $universalObjectCratesClasses - */ public function __construct( private ScopeFactory $scopeFactory, private NodeScopeResolver $nodeScopeResolver, @@ -97,7 +95,6 @@ public function __construct( private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, private FileTypeMapper $fileTypeMapper, private bool $inferPrivatePropertyTypeFromConstructor, - private array $universalObjectCratesClasses, ) { } @@ -244,7 +241,7 @@ private function createProperty( throw new ShouldNotHappenException(); } - if ($hierarchyDistances[$annotationProperty->getDeclaringClass()->getName()] < $hierarchyDistances[$distanceDeclaringClass]) { + if ($hierarchyDistances[$annotationProperty->getDeclaringClass()->getName()] <= $hierarchyDistances[$distanceDeclaringClass]) { return $annotationProperty; } } @@ -293,6 +290,8 @@ private function createProperty( $phpDocType = $phpDocType !== null ? TemplateTypeHelper::resolveTemplateTypes( $phpDocType, $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + $phpDocBlockClassReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createInvariant(), ) : null; $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; $isDeprecated = $resolvedPhpDoc->isDeprecated(); @@ -395,20 +394,7 @@ public function getMethod(ClassReflection $classReflection, string $methodName): public function hasNativeMethod(ClassReflection $classReflection, string $methodName): bool { - $hasMethod = $this->hasMethod($classReflection, $methodName); - if ($hasMethod) { - return true; - } - - if ($methodName === '__get' && UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( - $this->reflectionProviderProvider->getReflectionProvider(), - $this->universalObjectCratesClasses, - $classReflection, - )) { - return true; - } - - return false; + return $this->hasMethod($classReflection, $methodName); } public function getNativeMethod(ClassReflection $classReflection, string $methodName): ExtendedMethodReflection @@ -417,27 +403,13 @@ public function getNativeMethod(ClassReflection $classReflection, string $method return $this->nativeMethods[$classReflection->getCacheKey()][$methodName]; } - if ($classReflection->getNativeReflection()->hasMethod($methodName)) { - $nativeMethodReflection = new NativeBuiltinMethodReflection( - $classReflection->getNativeReflection()->getMethod($methodName), - ); - } else { - if ( - $methodName !== '__get' - || !UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( - $this->reflectionProviderProvider->getReflectionProvider(), - $this->universalObjectCratesClasses, - $classReflection, - )) { - throw new ShouldNotHappenException(); - } - - $nativeMethodReflection = new FakeBuiltinMethodReflection( - $methodName, - $classReflection->getNativeReflection(), - ); + if (!$classReflection->getNativeReflection()->hasMethod($methodName)) { + throw new ShouldNotHappenException(); } + $reflectionMethod = $classReflection->getNativeReflection()->getMethod($methodName); + $nativeMethodReflection = new NativeBuiltinMethodReflection($reflectionMethod); + if (!isset($this->nativeMethods[$classReflection->getCacheKey()][$nativeMethodReflection->getName()])) { $method = $this->createMethod($classReflection, $nativeMethodReflection, false); $this->nativeMethods[$classReflection->getCacheKey()][$nativeMethodReflection->getName()] = $method; @@ -468,7 +440,7 @@ private function createMethod( throw new ShouldNotHappenException(); } - if ($hierarchyDistances[$annotationMethod->getDeclaringClass()->getName()] < $hierarchyDistances[$distanceDeclaringClass]) { + if ($hierarchyDistances[$annotationMethod->getDeclaringClass()->getName()] <= $hierarchyDistances[$distanceDeclaringClass]) { return $annotationMethod; } } @@ -497,84 +469,142 @@ private function createMethod( } if ($this->signatureMapProvider->hasMethodSignature($declaringClassName, $methodReflection->getName())) { - $variants = []; + $variantsByType = ['positional' => []]; $reflectionMethod = null; $throwType = null; + $asserts = Assertions::createEmpty(); + $selfOutType = null; + $phpDocComment = null; if ($classReflection->getNativeReflection()->hasMethod($methodReflection->getName())) { $reflectionMethod = $classReflection->getNativeReflection()->getMethod($methodReflection->getName()); } - $methodSignatures = $this->signatureMapProvider->getMethodSignatures($declaringClassName, $methodReflection->getName(), $reflectionMethod); - foreach ($methodSignatures as $methodSignature) { - $phpDocParameterNameMapping = []; - foreach ($methodSignature->getParameters() as $parameter) { - $phpDocParameterNameMapping[$parameter->getName()] = $parameter->getName(); + $methodSignaturesResult = $this->signatureMapProvider->getMethodSignatures($declaringClassName, $methodReflection->getName(), $reflectionMethod); + foreach ($methodSignaturesResult as $signatureType => $methodSignatures) { + if ($methodSignatures === null) { + continue; } - $stubPhpDocReturnType = null; - $stubPhpDocParameterTypes = []; - $stubPhpDocParameterVariadicity = []; - $phpDocParameterTypes = []; - $phpDocReturnType = null; - $stubPhpDocPair = null; - if (count($methodSignatures) === 1) { - $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $methodReflection->getName(), array_map(static fn (ParameterSignature $parameterSignature): string => $parameterSignature->getName(), $methodSignature->getParameters())); - if ($stubPhpDocPair !== null) { - [$stubPhpDoc, $stubDeclaringClass] = $stubPhpDocPair; - $templateTypeMap = $stubDeclaringClass->getActiveTemplateTypeMap(); - $returnTag = $stubPhpDoc->getReturnTag(); - if ($returnTag !== null) { - $stubPhpDocReturnType = TemplateTypeHelper::resolveTemplateTypes( - $returnTag->getType(), - $templateTypeMap, - ); - } - foreach ($stubPhpDoc->getParamTags() as $name => $paramTag) { - $stubPhpDocParameterTypes[$name] = TemplateTypeHelper::resolveTemplateTypes( - $paramTag->getType(), - $templateTypeMap, - ); - $stubPhpDocParameterVariadicity[$name] = $paramTag->isVariadic(); - } + foreach ($methodSignatures as $methodSignature) { + $phpDocParameterNameMapping = []; + foreach ($methodSignature->getParameters() as $parameter) { + $phpDocParameterNameMapping[$parameter->getName()] = $parameter->getName(); + } + $stubPhpDocReturnType = null; + $stubPhpDocParameterTypes = []; + $stubPhpDocParameterVariadicity = []; + $phpDocParameterTypes = []; + $phpDocReturnType = null; + $stubPhpDocPair = null; + $stubPhpParameterOutTypes = []; + $phpDocParameterOutTypes = []; + $immediatelyInvokedCallableParameters = []; + $closureThisParameters = []; + $stubImmediatelyInvokedCallableParameters = []; + $stubClosureThisParameters = []; + if (count($methodSignatures) === 1) { + $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $methodReflection->getName(), array_map(static fn (ParameterSignature $parameterSignature): string => $parameterSignature->getName(), $methodSignature->getParameters())); + if ($stubPhpDocPair !== null) { + [$stubPhpDoc, $stubDeclaringClass] = $stubPhpDocPair; + $templateTypeMap = $stubDeclaringClass->getActiveTemplateTypeMap(); + $callSiteVarianceMap = $stubDeclaringClass->getCallSiteVarianceMap(); + $returnTag = $stubPhpDoc->getReturnTag(); + $stubImmediatelyInvokedCallableParameters = array_map(static fn (bool $immediate) => TrinaryLogic::createFromBoolean($immediate), $stubPhpDoc->getParamsImmediatelyInvokedCallable()); + if ($returnTag !== null) { + $stubPhpDocReturnType = TemplateTypeHelper::resolveTemplateTypes( + $returnTag->getType(), + $templateTypeMap, + $callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ); + } + + $stubClosureThisParameters = array_map(static fn ($tag) => $tag->getType(), $stubPhpDoc->getParamClosureThisTags()); + foreach ($stubPhpDoc->getParamTags() as $name => $paramTag) { + $stubPhpDocParameterTypes[$name] = TemplateTypeHelper::resolveTemplateTypes( + $paramTag->getType(), + $templateTypeMap, + $callSiteVarianceMap, + TemplateTypeVariance::createContravariant(), + ); + $stubPhpDocParameterVariadicity[$name] = $paramTag->isVariadic(); + } - $throwsTag = $stubPhpDoc->getThrowsTag(); - if ($throwsTag !== null) { - $throwType = $throwsTag->getType(); + $throwsTag = $stubPhpDoc->getThrowsTag(); + if ($throwsTag !== null) { + $throwType = $throwsTag->getType(); + } + + $asserts = Assertions::createFromResolvedPhpDocBlock($stubPhpDoc); + + $selfOutTypeTag = $stubPhpDoc->getSelfOutTag(); + if ($selfOutTypeTag !== null) { + $selfOutType = $selfOutTypeTag->getType(); + } + + foreach ($stubPhpDoc->getParamOutTags() as $name => $paramOutTag) { + $stubPhpParameterOutTypes[$name] = TemplateTypeHelper::resolveTemplateTypes( + $paramOutTag->getType(), + $templateTypeMap, + $callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ); + } + + if ($declaringClassName === $stubDeclaringClass->getName() && $stubPhpDoc->hasPhpDocString()) { + $phpDocComment = $stubPhpDoc->getPhpDocString(); + } } } - } - if ($stubPhpDocPair === null && $reflectionMethod !== null && $reflectionMethod->getDocComment() !== false) { - $filename = $reflectionMethod->getFileName(); - if ($filename !== false) { - $phpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc( - $filename, - $declaringClassName, - null, - $reflectionMethod->getName(), - $reflectionMethod->getDocComment(), - ); - $throwsTag = $phpDocBlock->getThrowsTag(); - if ($throwsTag !== null) { - $throwType = $throwsTag->getType(); - } - $returnTag = $phpDocBlock->getReturnTag(); - if ($returnTag !== null) { - $phpDocReturnType = $returnTag->getType(); - } - foreach ($phpDocBlock->getParamTags() as $name => $paramTag) { - $phpDocParameterTypes[$name] = $paramTag->getType(); - } + if ($stubPhpDocPair === null && $reflectionMethod !== null && $reflectionMethod->getDocComment() !== false) { + $filename = $reflectionMethod->getFileName(); + if ($filename !== false) { + $phpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc( + $filename, + $declaringClassName, + null, + $reflectionMethod->getName(), + $reflectionMethod->getDocComment(), + ); + $throwsTag = $phpDocBlock->getThrowsTag(); + if ($throwsTag !== null) { + $throwType = $throwsTag->getType(); + } + $returnTag = $phpDocBlock->getReturnTag(); + if ($returnTag !== null && count($methodSignatures) === 1) { + $phpDocReturnType = $returnTag->getType(); + } + $immediatelyInvokedCallableParameters = array_map(static fn ($immediate) => TrinaryLogic::createFromBoolean($immediate), $phpDocBlock->getParamsImmediatelyInvokedCallable()); + $closureThisParameters = array_map(static fn ($tag) => $tag->getType(), $phpDocBlock->getParamClosureThisTags()); + foreach ($phpDocBlock->getParamTags() as $name => $paramTag) { + $phpDocParameterTypes[$name] = $paramTag->getType(); + } + $asserts = Assertions::createFromResolvedPhpDocBlock($phpDocBlock); - $signatureParameters = $methodSignature->getParameters(); - foreach ($reflectionMethod->getParameters() as $paramI => $reflectionParameter) { - if (!array_key_exists($paramI, $signatureParameters)) { - continue; + $selfOutTypeTag = $phpDocBlock->getSelfOutTag(); + if ($selfOutTypeTag !== null) { + $selfOutType = $selfOutTypeTag->getType(); } - $phpDocParameterNameMapping[$signatureParameters[$paramI]->getName()] = $reflectionParameter->getName(); + if ($phpDocBlock->hasPhpDocString()) { + $phpDocComment = $phpDocBlock->getPhpDocString(); + } + + foreach ($phpDocBlock->getParamOutTags() as $name => $paramOutTag) { + $phpDocParameterOutTypes[$name] = $paramOutTag->getType(); + } + + $signatureParameters = $methodSignature->getParameters(); + foreach ($reflectionMethod->getParameters() as $paramI => $reflectionParameter) { + if (!array_key_exists($paramI, $signatureParameters)) { + continue; + } + + $phpDocParameterNameMapping[$signatureParameters[$paramI]->getName()] = $reflectionParameter->getName(); + } } } + $variantsByType[$signatureType][] = $this->createNativeMethodVariant($methodSignature, $stubPhpDocParameterTypes, $stubPhpDocParameterVariadicity, $stubPhpDocReturnType, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping, $stubPhpParameterOutTypes, $phpDocParameterOutTypes, $stubImmediatelyInvokedCallableParameters, $immediatelyInvokedCallableParameters, $stubClosureThisParameters, $closureThisParameters, $signatureType !== 'named'); } - $variants[] = $this->createNativeMethodVariant($methodSignature, $stubPhpDocParameterTypes, $stubPhpDocParameterVariadicity, $stubPhpDocReturnType, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping); } if ($this->signatureMapProvider->hasMethodMetadata($declaringClassName, $methodReflection->getName())) { @@ -586,16 +616,29 @@ private function createMethod( $this->reflectionProviderProvider->getReflectionProvider(), $declaringClass, $methodReflection, - $variants, + $variantsByType['positional'], + $variantsByType['named'] ?? null, $hasSideEffects, $throwType, + $asserts, + $selfOutType, + $phpDocComment, ); } - $declaringTraitName = $this->findMethodTrait($methodReflection); + return $this->createUserlandMethodReflection( + $declaringClass, + $declaringClass, + $methodReflection, + $this->findMethodTrait($methodReflection), + ); + } + + public function createUserlandMethodReflection(ClassReflection $fileDeclaringClass, ClassReflection $actualDeclaringClass, BuiltinMethodReflection $methodReflection, ?string $declaringTraitName): PhpMethodReflection + { $resolvedPhpDoc = null; - $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $methodReflection->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters())); - $phpDocBlockClassReflection = $declaringClass; + $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($fileDeclaringClass, $methodReflection->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters())); + $phpDocBlockClassReflection = $fileDeclaringClass; if ($methodReflection->getReflection() !== null) { $methodDeclaringClass = $methodReflection->getReflection()->getBetterReflection()->getDeclaringClass(); @@ -624,13 +667,13 @@ private function createMethod( $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod( $docComment, - $declaringClass->getFileName(), - $declaringClass, + $fileDeclaringClass->getFileName(), + $fileDeclaringClass, $declaringTraitName, $methodReflection->getName(), $positionalParameterNames, ); - $phpDocBlockClassReflection = $declaringClass; + $phpDocBlockClassReflection = $fileDeclaringClass; } $declaringTrait = null; @@ -664,8 +707,8 @@ private function createMethod( } $propertyDocblock = $this->fileTypeMapper->getResolvedPhpDoc( - $declaringClass->getFileName(), - $declaringClassName, + $fileDeclaringClass->getFileName(), + $fileDeclaringClass->getName(), $declaringTraitName, $methodReflection->getName(), $parameterProperty->getDocComment(), @@ -684,6 +727,9 @@ private function createMethod( } $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); + $immediatelyInvokedCallableParameters = array_map(static fn (bool $immediate) => TrinaryLogic::createFromBoolean($immediate), $resolvedPhpDoc->getParamsImmediatelyInvokedCallable()); + $closureThisParameters = array_map(static fn ($tag) => $tag->getType(), $resolvedPhpDoc->getParamClosureThisTags()); + foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) { if (array_key_exists($paramName, $phpDocParameterTypes)) { continue; @@ -694,12 +740,25 @@ private function createMethod( $phpDocParameterTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( $paramType, $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + $phpDocBlockClassReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createContravariant(), + ); + } + + $phpDocParameterOutTypes = []; + foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) { + $phpDocParameterOutTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( + $paramOutTag->getType(), + $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + $phpDocBlockClassReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createCovariant(), ); } + $nativeReturnType = TypehintHelper::decideTypeFromReflection( $methodReflection->getReturnType(), null, - $declaringClass->getName(), + $actualDeclaringClass, ); $phpDocReturnType = $this->getPhpDocReturnType($phpDocBlockClassReflection, $resolvedPhpDoc, $nativeReturnType); $phpDocThrowType = $resolvedPhpDoc->getThrowsTag() !== null ? $resolvedPhpDoc->getThrowsTag()->getType() : null; @@ -708,9 +767,15 @@ private function createMethod( $isInternal = $resolvedPhpDoc->isInternal(); $isFinal = $resolvedPhpDoc->isFinal(); $isPure = $resolvedPhpDoc->isPure(); + $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); + $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null; + $phpDocComment = null; + if ($resolvedPhpDoc->hasPhpDocString()) { + $phpDocComment = $resolvedPhpDoc->getPhpDocString(); + } return $this->methodReflectionFactory->create( - $declaringClass, + $actualDeclaringClass, $declaringTrait, $methodReflection, $templateTypeMap, @@ -722,6 +787,12 @@ private function createMethod( $isInternal, $isFinal, $isPure, + $asserts, + $selfOutType, + $phpDocComment, + $phpDocParameterOutTypes, + $immediatelyInvokedCallableParameters, + $closureThisParameters, ); } @@ -730,6 +801,12 @@ private function createMethod( * @param array $stubPhpDocParameterVariadicity * @param array $phpDocParameterTypes * @param array $phpDocParameterNameMapping + * @param array $stubPhpDocParameterOutTypes + * @param array $phpDocParameterOutTypes + * @param array $stubImmediatelyInvokedCallableParameters + * @param array $immediatelyInvokedCallableParameters + * @param array $stubClosureThisParameters + * @param array $closureThisParameters */ private function createNativeMethodVariant( FunctionSignature $methodSignature, @@ -739,12 +816,20 @@ private function createNativeMethodVariant( array $phpDocParameterTypes, ?Type $phpDocReturnType, array $phpDocParameterNameMapping, + array $stubPhpDocParameterOutTypes, + array $phpDocParameterOutTypes, + array $stubImmediatelyInvokedCallableParameters, + array $immediatelyInvokedCallableParameters, + array $stubClosureThisParameters, + array $closureThisParameters, + bool $usePhpDocParameterNames, ): FunctionVariantWithPhpDocs { $parameters = []; foreach ($methodSignature->getParameters() as $parameterSignature) { $type = null; $phpDocType = null; + $parameterOutType = null; $phpDocParameterName = $phpDocParameterNameMapping[$parameterSignature->getName()] ?? $parameterSignature->getName(); @@ -755,8 +840,31 @@ private function createNativeMethodVariant( $phpDocType = $phpDocParameterTypes[$phpDocParameterName]; } + if (isset($stubPhpDocParameterOutTypes[$parameterSignature->getName()])) { + $parameterOutType = $stubPhpDocParameterOutTypes[$parameterSignature->getName()]; + } elseif (isset($phpDocParameterOutTypes[$phpDocParameterName])) { + $parameterOutType = $phpDocParameterOutTypes[$phpDocParameterName]; + } + + if (isset($stubImmediatelyInvokedCallableParameters[$parameterSignature->getName()])) { + $immediatelyInvoked = $stubImmediatelyInvokedCallableParameters[$parameterSignature->getName()]; + } elseif (isset($immediatelyInvokedCallableParameters[$phpDocParameterName])) { + $immediatelyInvoked = $immediatelyInvokedCallableParameters[$phpDocParameterName]; + } else { + $immediatelyInvoked = TrinaryLogic::createMaybe(); + } + + $closureThisType = null; + if (isset($stubClosureThisParameters[$parameterSignature->getName()])) { + $closureThisType = $stubClosureThisParameters[$parameterSignature->getName()]; + } elseif (isset($closureThisParameters[$phpDocParameterName])) { + $closureThisType = $closureThisParameters[$phpDocParameterName]; + } + $parameters[] = new NativeParameterWithPhpDocsReflection( - $phpDocParameterName, + $usePhpDocParameterNames + ? $phpDocParameterName + : $parameterSignature->getName(), $parameterSignature->isOptional(), $type ?? $parameterSignature->getType(), $phpDocType ?? new MixedType(), @@ -764,13 +872,17 @@ private function createNativeMethodVariant( $parameterSignature->passedByReference(), $stubPhpDocParameterVariadicity[$parameterSignature->getName()] ?? $parameterSignature->isVariadic(), $parameterSignature->getDefaultValue(), + $parameterOutType ?? $parameterSignature->getOutType(), + $immediatelyInvoked, + $closureThisType, ); } - $returnType = null; if ($stubPhpDocReturnType !== null) { $returnType = $stubPhpDocReturnType; $phpDocReturnType = $stubPhpDocReturnType; + } else { + $returnType = TypehintHelper::decideType($methodSignature->getReturnType(), $phpDocReturnType); } return new FunctionVariantWithPhpDocs( @@ -778,7 +890,7 @@ private function createNativeMethodVariant( null, $parameters, $methodSignature->isVariadic(), - $returnType ?? $methodSignature->getReturnType(), + $returnType, $phpDocReturnType ?? new MixedType(), $methodSignature->getNativeReturnType(), ); @@ -870,14 +982,12 @@ private function inferAndCachePropertyTypes( $namespace = implode('\\', array_slice($classNameParts, 0, -1)); } - $classScope = $this->scopeFactory->create( - ScopeContext::create($fileName), - false, - [], - $constructor, - $namespace, - )->enterClass($declaringClass); - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments] = $this->nodeScopeResolver->getPhpDocs($classScope, $methodNode); + $classScope = $this->scopeFactory->create(ScopeContext::create($fileName)); + if ($namespace !== null) { + $classScope = $classScope->enterNamespace($namespace); + } + $classScope = $classScope->enterClass($declaringClass); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes] = $this->nodeScopeResolver->getPhpDocs($classScope, $methodNode); $methodScope = $classScope->enterClassMethod( $methodNode, $templateTypeMap, @@ -890,6 +1000,12 @@ private function inferAndCachePropertyTypes( $isFinal, $isPure, $acceptsNamedArguments, + $asserts, + $selfOutType, + $phpDocComment, + $phpDocParameterOutTypes, + $phpDocImmediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, ); $propertyTypes = []; @@ -922,7 +1038,7 @@ private function inferAndCachePropertyTypes( } $propertyType = $propertyType->generalize(GeneralizePrecision::lessSpecific()); - if ($propertyType instanceof ConstantArrayType) { + if ($propertyType->isConstantArray()->yes()) { $propertyType = new ArrayType(new MixedType(true), new MixedType(true)); } @@ -995,6 +1111,8 @@ private function getPhpDocReturnType(ClassReflection $phpDocBlockClassReflection $phpDocReturnType = TemplateTypeHelper::resolveTemplateTypes( $phpDocReturnType, $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + $phpDocBlockClassReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createCovariant(), ); if ($returnTag->isExplicit() || $nativeReturnType->isSuperTypeOf($phpDocReturnType)->yes()) { diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index aefc759c37..8e2429016c 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -7,6 +7,7 @@ use PhpParser\Node\FunctionLike; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\FunctionVariantWithPhpDocs; use PHPStan\Reflection\ParameterReflectionWithPhpDocs; @@ -18,11 +19,13 @@ use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; -use PHPStan\Type\VoidType; use function array_reverse; use function is_array; use function is_string; +/** + * @api + */ class PhpFunctionFromParserNodeReflection implements FunctionReflection { @@ -37,6 +40,9 @@ class PhpFunctionFromParserNodeReflection implements FunctionReflection * @param Type[] $realParameterTypes * @param Type[] $phpDocParameterTypes * @param Type[] $realParameterDefaultValues + * @param Type[] $parameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters */ public function __construct( FunctionLike $functionLike, @@ -52,8 +58,13 @@ public function __construct( private bool $isDeprecated, private bool $isInternal, private bool $isFinal, - private ?bool $isPure, + protected ?bool $isPure, private bool $acceptsNamedArguments, + private Assertions $assertions, + private ?string $phpDocComment, + private array $parameterOutTypes, + private array $immediatelyInvokedCallableParameters, + private array $phpDocClosureThisTypeParameters, ) { $this->functionLike = $functionLike; @@ -104,6 +115,11 @@ public function getVariants(): array return $this->variants; } + public function getNamedArgumentsVariants(): ?array + { + return null; + } + /** * @return ParameterReflectionWithPhpDocs[] */ @@ -121,6 +137,19 @@ private function getParameters(): array if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { throw new ShouldNotHappenException(); } + + if (isset($this->immediatelyInvokedCallableParameters[$parameter->var->name])) { + $immediatelyInvokedCallable = TrinaryLogic::createFromBoolean($this->immediatelyInvokedCallableParameters[$parameter->var->name]); + } else { + $immediatelyInvokedCallable = TrinaryLogic::createMaybe(); + } + + if (isset($this->phpDocClosureThisTypeParameters[$parameter->var->name])) { + $closureThisType = $this->phpDocClosureThisTypeParameters[$parameter->var->name]; + } else { + $closureThisType = null; + } + $parameters[] = new PhpParameterFromParserNodeReflection( $parameter->var->name, $isOptional, @@ -131,6 +160,9 @@ private function getParameters(): array : PassedByReference::createNo(), $this->realParameterDefaultValues[$parameter->var->name] ?? null, $parameter->variadic, + $this->parameterOutTypes[$parameter->var->name] ?? null, + $immediatelyInvokedCallable, + $closureThisType, ); } @@ -148,7 +180,7 @@ private function isVariadic(): bool return false; } - private function getReturnType(): Type + protected function getReturnType(): Type { return TypehintHelper::decideType($this->realReturnType, $this->phpDocReturnType); } @@ -181,6 +213,15 @@ public function isFinal(): TrinaryLogic return TrinaryLogic::createFromBoolean($finalMethod || $this->isFinal); } + public function isFinalByKeyword(): TrinaryLogic + { + $finalMethod = false; + if ($this->functionLike instanceof ClassMethod) { + $finalMethod = $this->functionLike->isFinal(); + } + return TrinaryLogic::createFromBoolean($finalMethod); + } + public function getThrowType(): ?Type { return $this->throwType; @@ -188,7 +229,7 @@ public function getThrowType(): ?Type public function hasSideEffects(): TrinaryLogic { - if ($this->getReturnType() instanceof VoidType) { + if ($this->getReturnType()->isVoid()->yes()) { return TrinaryLogic::createYes(); } if ($this->isPure !== null) { @@ -244,4 +285,28 @@ private function nodeIsOrContainsYield(Node $node): bool return false; } + public function getAsserts(): Assertions + { + return $this->assertions; + } + + public function getDocComment(): ?string + { + return $this->phpDocComment; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->functionLike->returnsByRef()); + } + + public function isPure(): TrinaryLogic + { + if ($this->isPure === null) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createFromBoolean($this->isPure); + } + } diff --git a/src/Reflection/Php/PhpFunctionReflection.php b/src/Reflection/Php/PhpFunctionReflection.php index a98b3e2a33..d437c86f5e 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -12,6 +12,7 @@ use PHPStan\Cache\Cache; use PHPStan\Parser\FunctionCallStatementFinder; use PHPStan\Parser\Parser; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\FunctionVariantWithPhpDocs; use PHPStan\Reflection\InitializerExprTypeResolver; @@ -23,7 +24,7 @@ use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; -use PHPStan\Type\VoidType; +use function array_key_exists; use function array_map; use function filemtime; use function is_file; @@ -37,7 +38,10 @@ class PhpFunctionReflection implements FunctionReflection private ?array $variants = null; /** - * @param Type[] $phpDocParameterTypes + * @param array $phpDocParameterTypes + * @param array $phpDocParameterOutTypes + * @param array $phpDocParameterImmediatelyInvokedCallable + * @param array $phpDocParameterClosureThisTypes */ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, @@ -55,6 +59,11 @@ public function __construct( private bool $isFinal, private ?string $filename, private ?bool $isPure, + private Assertions $asserts, + private ?string $phpDocComment, + private array $phpDocParameterOutTypes, + private array $phpDocParameterImmediatelyInvokedCallable, + private array $phpDocParameterClosureThisTypes, ) { } @@ -99,23 +108,39 @@ public function getVariants(): array return $this->variants; } + public function getNamedArgumentsVariants(): ?array + { + return null; + } + /** * @return ParameterReflectionWithPhpDocs[] */ private function getParameters(): array { - return array_map(fn (ReflectionParameter $reflection): PhpParameterReflection => new PhpParameterReflection( - $this->initializerExprTypeResolver, - $reflection, - $this->phpDocParameterTypes[$reflection->getName()] ?? null, - null, - ), $this->reflection->getParameters()); + return array_map(function (ReflectionParameter $reflection): PhpParameterReflection { + if (array_key_exists($reflection->getName(), $this->phpDocParameterImmediatelyInvokedCallable)) { + $immediatelyInvokedCallable = TrinaryLogic::createFromBoolean($this->phpDocParameterImmediatelyInvokedCallable[$reflection->getName()]); + } else { + $immediatelyInvokedCallable = TrinaryLogic::createMaybe(); + } + return new PhpParameterReflection( + $this->initializerExprTypeResolver, + $reflection, + $this->phpDocParameterTypes[$reflection->getName()] ?? null, + null, + $this->phpDocParameterOutTypes[$reflection->getName()] ?? null, + $immediatelyInvokedCallable, + $this->phpDocParameterClosureThisTypes[$reflection->getName()] ?? null, + ); + }, $this->reflection->getParameters()); } private function isVariadic(): bool { $isNativelyVariadic = $this->reflection->isVariadic(); - if (!$isNativelyVariadic && $this->reflection->getFileName() !== false) { + if (!$isNativelyVariadic && $this->reflection + ->getFileName() !== false) { $fileName = $this->reflection->getFileName(); if (is_file($fileName)) { $functionName = $this->reflection->getName(); @@ -233,7 +258,7 @@ public function getThrowType(): ?Type public function hasSideEffects(): TrinaryLogic { - if ($this->getReturnType() instanceof VoidType) { + if ($this->getReturnType()->isVoid()->yes()) { return TrinaryLogic::createYes(); } if ($this->isPure !== null) { @@ -243,9 +268,33 @@ public function hasSideEffects(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isPure(): TrinaryLogic + { + if ($this->isPure === null) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createFromBoolean($this->isPure); + } + public function isBuiltin(): bool { return $this->reflection->isInternal(); } + public function getAsserts(): Assertions + { + return $this->asserts; + } + + public function getDocComment(): ?string + { + return $this->phpDocComment; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->reflection->returnsReference()); + } + } diff --git a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php index 9d07c70302..7e858b714e 100644 --- a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php @@ -4,28 +4,37 @@ use PhpParser\Node; use PhpParser\Node\Stmt\ClassMethod; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\MissingMethodFromReflectionException; +use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\IntegerType; +use PHPStan\Type\MixedType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VoidType; +use function in_array; use function strtolower; -class PhpMethodFromParserNodeReflection extends PhpFunctionFromParserNodeReflection implements MethodReflection +/** + * @api + */ +class PhpMethodFromParserNodeReflection extends PhpFunctionFromParserNodeReflection implements ExtendedMethodReflection { /** * @param Type[] $realParameterTypes * @param Type[] $phpDocParameterTypes * @param Type[] $realParameterDefaultValues + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters */ public function __construct( private ClassReflection $declaringClass, @@ -44,16 +53,16 @@ public function __construct( bool $isFinal, ?bool $isPure, bool $acceptsNamedArguments, + Assertions $assertions, + private ?Type $selfOutType, + ?string $phpDocComment, + array $parameterOutTypes, + array $immediatelyInvokedCallableParameters, + array $phpDocClosureThisTypeParameters, ) { $name = strtolower($classMethod->name->name); - if ( - $name === '__construct' - || $name === '__destruct' - || $name === '__unset' - || $name === '__wakeup' - || $name === '__clone' - ) { + if (in_array($name, ['__construct', '__destruct', '__unset', '__wakeup', '__clone'], true)) { $realReturnType = new VoidType(); } if ($name === '__tostring') { @@ -68,6 +77,22 @@ public function __construct( if ($name === '__set_state') { $realReturnType = TypeCombinator::intersect(new ObjectWithoutClassType(), $realReturnType); } + if ($name === '__set') { + $realReturnType = new VoidType(); + } + + if ($name === '__debuginfo') { + $realReturnType = TypeCombinator::intersect(TypeCombinator::addNull( + new ArrayType(new MixedType(true), new MixedType(true)), + ), $realReturnType); + } + + if ($name === '__unserialize') { + $realReturnType = new VoidType(); + } + if ($name === '__serialize') { + $realReturnType = new ArrayType(new MixedType(true), new MixedType(true)); + } parent::__construct( $classMethod, @@ -85,6 +110,11 @@ public function __construct( $isFinal || $classMethod->isFinal(), $isPure, $acceptsNamedArguments, + $assertions, + $phpDocComment, + $parameterOutTypes, + $immediatelyInvokedCallableParameters, + $phpDocClosureThisTypeParameters, ); } @@ -124,14 +154,39 @@ public function isPublic(): bool return $this->getClassMethod()->isPublic(); } - public function getDocComment(): ?string + public function isBuiltin(): bool { - return null; + return false; } - public function isBuiltin(): bool + public function getSelfOutType(): ?Type { - return false; + return $this->selfOutType; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->getClassMethod()->returnsByRef()); + } + + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->getClassMethod()->isAbstract()); + } + + public function hasSideEffects(): TrinaryLogic + { + if ( + strtolower($this->getName()) !== '__construct' + && $this->getReturnType()->isVoid()->yes() + ) { + return TrinaryLogic::createYes(); + } + if ($this->isPure !== null) { + return TrinaryLogic::createFromBoolean(!$this->isPure); + } + + return TrinaryLogic::createMaybe(); } } diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index 715d71ff93..c0e5a7dfec 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -11,6 +11,7 @@ use PHPStan\Cache\Cache; use PHPStan\Parser\FunctionCallStatementFinder; use PHPStan\Parser\Parser; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; @@ -29,6 +30,7 @@ use PHPStan\Type\MixedType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; +use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; use PHPStan\Type\VoidType; @@ -36,8 +38,8 @@ use function array_map; use function explode; use function filemtime; +use function in_array; use function is_bool; -use function is_file; use function sprintf; use function strtolower; use function time; @@ -59,6 +61,9 @@ class PhpMethodReflection implements ExtendedMethodReflection /** * @param Type[] $phpDocParameterTypes + * @param Type[] $phpDocParameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters */ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, @@ -78,6 +83,12 @@ public function __construct( private bool $isInternal, private bool $isFinal, private ?bool $isPure, + private Assertions $asserts, + private ?Type $selfOutType, + private ?string $phpDocComment, + private array $phpDocParameterOutTypes, + private array $immediatelyInvokedCallableParameters, + private array $phpDocClosureThisTypeParameters, ) { } @@ -92,11 +103,6 @@ public function getDeclaringTrait(): ?ClassReflection return $this->declaringTrait; } - public function getDocComment(): ?string - { - return $this->reflection->getDocComment(); - } - /** * @return self|MethodPrototypeReflection */ @@ -104,7 +110,10 @@ public function getPrototype(): ClassMemberReflection { try { $prototypeMethod = $this->reflection->getPrototype(); - $prototypeDeclaringClass = $this->reflectionProvider->getClass($prototypeMethod->getDeclaringClass()->getName()); + $prototypeDeclaringClass = $this->declaringClass->getAncestorWithClassName($prototypeMethod->getDeclaringClass()->getName()); + if ($prototypeDeclaringClass === null) { + $prototypeDeclaringClass = $this->reflectionProvider->getClass($prototypeMethod->getDeclaringClass()->getName()); + } $tentativeReturnType = null; if ($prototypeMethod->getTentativeReturnType() !== null) { @@ -119,6 +128,7 @@ public function getPrototype(): ClassMemberReflection $prototypeMethod->isPublic(), $prototypeMethod->isAbstract(), $prototypeMethod->isFinal(), + $prototypeMethod->isInternal(), $prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants(), $tentativeReturnType, ); @@ -194,6 +204,11 @@ public function getVariants(): array return $this->variants; } + public function getNamedArgumentsVariants(): ?array + { + return null; + } + /** * @return ParameterReflectionWithPhpDocs[] */ @@ -205,6 +220,9 @@ private function getParameters(): array $reflection, $this->phpDocParameterTypes[$reflection->getName()] ?? null, $this->getDeclaringClass()->getName(), + $this->phpDocParameterOutTypes[$reflection->getName()] ?? null, + $this->immediatelyInvokedCallableParameters[$reflection->getName()] ?? TrinaryLogic::createMaybe(), + $this->phpDocClosureThisTypeParameters[$reflection->getName()] ?? null, ), $this->reflection->getParameters()); } @@ -221,8 +239,8 @@ private function isVariadic(): bool $filename = $this->declaringTrait->getFileName(); } - if (!$isNativelyVariadic && $filename !== null && is_file($filename)) { - $modifiedTime = filemtime($filename); + if (!$isNativelyVariadic && $filename !== null) { + $modifiedTime = @filemtime($filename); if ($modifiedTime === false) { $modifiedTime = time(); } @@ -313,32 +331,29 @@ private function getReturnType(): Type { if ($this->returnType === null) { $name = strtolower($this->getName()); - if ( - $name === '__construct' - || $name === '__destruct' - || $name === '__unset' - || $name === '__wakeup' - || $name === '__clone' - ) { - return $this->returnType = TypehintHelper::decideType(new VoidType(), $this->phpDocReturnType); - } - if ($name === '__tostring') { - return $this->returnType = TypehintHelper::decideType(new StringType(), $this->phpDocReturnType); - } - if ($name === '__isset') { - return $this->returnType = TypehintHelper::decideType(new BooleanType(), $this->phpDocReturnType); - } - if ($name === '__sleep') { - return $this->returnType = TypehintHelper::decideType(new ArrayType(new IntegerType(), new StringType()), $this->phpDocReturnType); - } - if ($name === '__set_state') { - return $this->returnType = TypehintHelper::decideType(new ObjectWithoutClassType(), $this->phpDocReturnType); + $returnType = $this->reflection->getReturnType(); + if ($returnType === null) { + if (in_array($name, ['__construct', '__destruct', '__unset', '__wakeup', '__clone'], true)) { + return $this->returnType = TypehintHelper::decideType(new VoidType(), $this->phpDocReturnType); + } + if ($name === '__tostring') { + return $this->returnType = TypehintHelper::decideType(new StringType(), $this->phpDocReturnType); + } + if ($name === '__isset') { + return $this->returnType = TypehintHelper::decideType(new BooleanType(), $this->phpDocReturnType); + } + if ($name === '__sleep') { + return $this->returnType = TypehintHelper::decideType(new ArrayType(new IntegerType(), new StringType()), $this->phpDocReturnType); + } + if ($name === '__set_state') { + return $this->returnType = TypehintHelper::decideType(new ObjectWithoutClassType(), $this->phpDocReturnType); + } } $this->returnType = TypehintHelper::decideTypeFromReflection( - $this->reflection->getReturnType(), + $returnType, $this->phpDocReturnType, - $this->declaringClass->getName(), + $this->declaringClass, ); } @@ -360,7 +375,7 @@ private function getNativeReturnType(): Type $this->nativeReturnType = TypehintHelper::decideTypeFromReflection( $this->reflection->getReturnType(), null, - $this->declaringClass->getName(), + $this->declaringClass, ); } @@ -378,17 +393,26 @@ public function getDeprecatedDescription(): ?string public function isDeprecated(): TrinaryLogic { - return $this->reflection->isDeprecated()->or(TrinaryLogic::createFromBoolean($this->isDeprecated)); + if ($this->isDeprecated) { + return TrinaryLogic::createYes(); + } + + return $this->reflection->isDeprecated(); } public function isInternal(): TrinaryLogic { - return TrinaryLogic::createFromBoolean($this->reflection->isInternal() || $this->isInternal); + return TrinaryLogic::createFromBoolean($this->isInternal || $this->reflection->isInternal()); } public function isFinal(): TrinaryLogic { - return TrinaryLogic::createFromBoolean($this->reflection->isFinal() || $this->isFinal); + return TrinaryLogic::createFromBoolean($this->isFinal || $this->reflection->isFinal()); + } + + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->reflection->isFinal()); } public function isAbstract(): bool @@ -403,12 +427,9 @@ public function getThrowType(): ?Type public function hasSideEffects(): TrinaryLogic { - $name = strtolower($this->getName()); - $isVoid = $this->getReturnType() instanceof VoidType; - if ( - $name !== '__construct' - && $isVoid + strtolower($this->getName()) !== '__construct' + && $this->getReturnType()->isVoid()->yes() ) { return TrinaryLogic::createYes(); } @@ -416,7 +437,40 @@ public function hasSideEffects(): TrinaryLogic return TrinaryLogic::createFromBoolean(!$this->isPure); } + if ((new ThisType($this->declaringClass))->isSuperTypeOf($this->getReturnType())->yes()) { + return TrinaryLogic::createYes(); + } + return TrinaryLogic::createMaybe(); } + public function getAsserts(): Assertions + { + return $this->asserts; + } + + public function getSelfOutType(): ?Type + { + return $this->selfOutType; + } + + public function getDocComment(): ?string + { + return $this->phpDocComment; + } + + public function returnsByReference(): TrinaryLogic + { + return $this->reflection->returnsByReference(); + } + + public function isPure(): TrinaryLogic + { + if ($this->isPure === null) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createFromBoolean($this->isPure); + } + } diff --git a/src/Reflection/Php/PhpMethodReflectionFactory.php b/src/Reflection/Php/PhpMethodReflectionFactory.php index 8b56043ac5..8da0dcb5c6 100644 --- a/src/Reflection/Php/PhpMethodReflectionFactory.php +++ b/src/Reflection/Php/PhpMethodReflectionFactory.php @@ -2,7 +2,9 @@ namespace PHPStan\Reflection\Php; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassReflection; +use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Type; @@ -11,7 +13,9 @@ interface PhpMethodReflectionFactory /** * @param Type[] $phpDocParameterTypes - * + * @param Type[] $phpDocParameterOutTypes + * @param array $immediatelyInvokedCallableParameters + * @param array $phpDocClosureThisTypeParameters */ public function create( ClassReflection $declaringClass, @@ -25,7 +29,13 @@ public function create( bool $isDeprecated, bool $isInternal, bool $isFinal, - ?bool $isPure = null, + ?bool $isPure, + Assertions $asserts, + ?Type $selfOutType, + ?string $phpDocComment, + array $phpDocParameterOutTypes, + array $immediatelyInvokedCallableParameters = [], + array $phpDocClosureThisTypeParameters = [], ): PhpMethodReflection; } diff --git a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php index 0625921242..9dd5a25958 100644 --- a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php @@ -4,8 +4,8 @@ use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\PassedByReference; +use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; -use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; @@ -23,6 +23,9 @@ public function __construct( private PassedByReference $passedByReference, private ?Type $defaultValue, private bool $variadic, + private ?Type $outType, + private TrinaryLogic $immediatelyInvokedCallable, + private ?Type $closureThisType, ) { } @@ -42,7 +45,7 @@ public function getType(): Type if ($this->type === null) { $phpDocType = $this->phpDocType; if ($phpDocType !== null && $this->defaultValue !== null) { - if ($this->defaultValue instanceof NullType) { + if ($this->defaultValue->isNull()->yes()) { $inferred = $phpDocType->inferTemplateTypes($this->defaultValue); if ($inferred->isEmpty()) { $phpDocType = TypeCombinator::addNull($phpDocType); @@ -80,4 +83,19 @@ public function getDefaultValue(): ?Type return $this->defaultValue; } + public function getOutType(): ?Type + { + return $this->outType; + } + + public function isImmediatelyInvokedCallable(): TrinaryLogic + { + return $this->immediatelyInvokedCallable; + } + + public function getClosureThisType(): ?Type + { + return $this->closureThisType; + } + } diff --git a/src/Reflection/Php/PhpParameterReflection.php b/src/Reflection/Php/PhpParameterReflection.php index e24ea24077..0e6ce5ae77 100644 --- a/src/Reflection/Php/PhpParameterReflection.php +++ b/src/Reflection/Php/PhpParameterReflection.php @@ -7,8 +7,8 @@ use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\PassedByReference; +use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; -use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; @@ -25,6 +25,9 @@ public function __construct( private ReflectionParameter $reflection, private ?Type $phpDocType, private ?string $declaringClassName, + private ?Type $outType, + private TrinaryLogic $immediatelyInvokedCallable, + private ?Type $closureThisType, ) { } @@ -51,7 +54,7 @@ public function getType(): Type $this->reflection->getDefaultValueExpression(), InitializerExprContext::fromReflectionParameter($this->reflection), ); - if ($defaultValueType instanceof NullType) { + if ($defaultValueType->isNull()->yes()) { $phpDocType = TypeCombinator::addNull($phpDocType); } } @@ -114,4 +117,19 @@ public function getDefaultValue(): ?Type return null; } + public function getOutType(): ?Type + { + return $this->outType; + } + + public function isImmediatelyInvokedCallable(): TrinaryLogic + { + return $this->immediatelyInvokedCallable; + } + + public function getClosureThisType(): ?Type + { + return $this->closureThisType; + } + } diff --git a/src/Reflection/Php/PhpPropertyReflection.php b/src/Reflection/Php/PhpPropertyReflection.php index e48e8deee4..00fc7e3e8e 100644 --- a/src/Reflection/Php/PhpPropertyReflection.php +++ b/src/Reflection/Php/PhpPropertyReflection.php @@ -87,7 +87,7 @@ public function getReadableType(): Type $this->type = TypehintHelper::decideTypeFromReflection( $this->nativeType, $this->phpDocType, - $this->declaringClass->getName(), + $this->declaringClass, ); } @@ -134,7 +134,7 @@ public function getNativeType(): Type $this->finalNativeType = TypehintHelper::decideTypeFromReflection( $this->nativeType, null, - $this->declaringClass->getName(), + $this->declaringClass, ); } diff --git a/src/Reflection/PhpVersionStaticAccessor.php b/src/Reflection/PhpVersionStaticAccessor.php new file mode 100644 index 0000000000..909c357874 --- /dev/null +++ b/src/Reflection/PhpVersionStaticAccessor.php @@ -0,0 +1,30 @@ +findMethod($classReflection, $methodName) !== null; + } + + /** + * @return ExtendedMethodReflection + */ + public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection + { + $method = $this->findMethod($classReflection, $methodName); + if ($method === null) { + throw new ShouldNotHappenException(); + } + + return $method; + } + + /** + * @return ExtendedMethodReflection|null + */ + private function findMethod(ClassReflection $classReflection, string $methodName): ?MethodReflection + { + if (!$classReflection->isInterface()) { + return null; + } + + $extendsTags = $classReflection->getRequireExtendsTags(); + foreach ($extendsTags as $extendsTag) { + $type = $extendsTag->getType(); + + if (!$type->hasMethod($methodName)->yes()) { + continue; + } + + return $type->getMethod($methodName, new OutOfClassScope()); + } + + $interfaces = $classReflection->getInterfaces(); + foreach ($interfaces as $interface) { + $method = $this->findMethod($interface, $methodName); + if ($method !== null) { + return $method; + } + } + + return null; + } + +} diff --git a/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php b/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php new file mode 100644 index 0000000000..ecaa6d3109 --- /dev/null +++ b/src/Reflection/RequireExtension/RequireExtendsPropertiesClassReflectionExtension.php @@ -0,0 +1,57 @@ +findProperty($classReflection, $propertyName) !== null; + } + + public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection + { + $property = $this->findProperty($classReflection, $propertyName); + if ($property === null) { + throw new ShouldNotHappenException(); + } + + return $property; + } + + private function findProperty(ClassReflection $classReflection, string $propertyName): ?PropertyReflection + { + if (!$classReflection->isInterface()) { + return null; + } + + $requireExtendsTags = $classReflection->getRequireExtendsTags(); + foreach ($requireExtendsTags as $requireExtendsTag) { + $type = $requireExtendsTag->getType(); + + if (!$type->hasProperty($propertyName)->yes()) { + continue; + } + + return $type->getProperty($propertyName, new OutOfClassScope()); + } + + $interfaces = $classReflection->getInterfaces(); + foreach ($interfaces as $interface) { + $property = $this->findProperty($interface, $propertyName); + if ($property !== null) { + return $property; + } + } + + return null; + } + +} diff --git a/src/Reflection/ResolvedFunctionVariant.php b/src/Reflection/ResolvedFunctionVariant.php index 092570c6cd..a6139853fb 100644 --- a/src/Reflection/ResolvedFunctionVariant.php +++ b/src/Reflection/ResolvedFunctionVariant.php @@ -2,135 +2,15 @@ namespace PHPStan\Reflection; -use PHPStan\Reflection\Php\DummyParameter; -use PHPStan\Type\ConditionalTypeForParameter; -use PHPStan\Type\ErrorType; -use PHPStan\Type\Generic\TemplateType; -use PHPStan\Type\Generic\TemplateTypeHelper; -use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Type; -use PHPStan\Type\TypeTraverser; -use PHPStan\Type\TypeUtils; -use function array_key_exists; -use function array_map; -class ResolvedFunctionVariant implements ParametersAcceptor +interface ResolvedFunctionVariant extends ParametersAcceptorWithPhpDocs { - /** @var ParameterReflection[]|null */ - private ?array $parameters = null; + public function getOriginalParametersAcceptor(): ParametersAcceptor; - private ?Type $returnTypeWithUnresolvableTemplateTypes = null; + public function getReturnTypeWithUnresolvableTemplateTypes(): Type; - private ?Type $returnType = null; - - /** - * @param array $passedArgs - */ - public function __construct( - private ParametersAcceptor $parametersAcceptor, - private TemplateTypeMap $resolvedTemplateTypeMap, - private array $passedArgs, - ) - { - } - - public function getOriginalParametersAcceptor(): ParametersAcceptor - { - return $this->parametersAcceptor; - } - - public function getTemplateTypeMap(): TemplateTypeMap - { - return $this->parametersAcceptor->getTemplateTypeMap(); - } - - public function getResolvedTemplateTypeMap(): TemplateTypeMap - { - return $this->resolvedTemplateTypeMap; - } - - public function getParameters(): array - { - $parameters = $this->parameters; - - if ($parameters === null) { - $parameters = array_map(fn (ParameterReflection $param): ParameterReflection => new DummyParameter( - $param->getName(), - TypeUtils::resolveLateResolvableTypes( - TemplateTypeHelper::resolveTemplateTypes( - $this->resolveConditionalTypesForParameter($param->getType()), - $this->resolvedTemplateTypeMap, - ), - false, - ), - $param->isOptional(), - $param->passedByReference(), - $param->isVariadic(), - $param->getDefaultValue(), - ), $this->parametersAcceptor->getParameters()); - - $this->parameters = $parameters; - } - - return $parameters; - } - - public function isVariadic(): bool - { - return $this->parametersAcceptor->isVariadic(); - } - - public function getReturnTypeWithUnresolvableTemplateTypes(): Type - { - return $this->returnTypeWithUnresolvableTemplateTypes ??= - $this->resolveConditionalTypesForParameter( - $this->resolveResolvableTemplateTypes($this->parametersAcceptor->getReturnType()), - ); - } - - public function getReturnType(): Type - { - $type = $this->returnType; - - if ($type === null) { - $type = TypeUtils::resolveLateResolvableTypes( - TemplateTypeHelper::resolveTemplateTypes( - $this->getReturnTypeWithUnresolvableTemplateTypes(), - $this->resolvedTemplateTypeMap, - ), - false, - ); - - $this->returnType = $type; - } - - return $type; - } - - private function resolveResolvableTemplateTypes(Type $type): Type - { - return TypeTraverser::map($type, function (Type $type, callable $traverse): Type { - if ($type instanceof TemplateType && !$type->isArgument()) { - $newType = $this->resolvedTemplateTypeMap->getType($type->getName()); - if ($newType !== null && !$newType instanceof ErrorType) { - return $newType; - } - } - - return $traverse($type); - }); - } - - private function resolveConditionalTypesForParameter(Type $type): Type - { - return TypeTraverser::map($type, function (Type $type, callable $traverse): Type { - if ($type instanceof ConditionalTypeForParameter && array_key_exists($type->getParameterName(), $this->passedArgs)) { - $type = $type->toConditional($this->passedArgs[$type->getParameterName()]); - } - - return $traverse($type); - }); - } + public function getPhpDocReturnTypeWithUnresolvableTemplateTypes(): Type; } diff --git a/src/Reflection/ResolvedFunctionVariantWithCallable.php b/src/Reflection/ResolvedFunctionVariantWithCallable.php new file mode 100644 index 0000000000..4714738167 --- /dev/null +++ b/src/Reflection/ResolvedFunctionVariantWithCallable.php @@ -0,0 +1,114 @@ +parametersAcceptor->getOriginalParametersAcceptor(); + } + + public function getTemplateTypeMap(): TemplateTypeMap + { + return $this->parametersAcceptor->getTemplateTypeMap(); + } + + public function getResolvedTemplateTypeMap(): TemplateTypeMap + { + return $this->parametersAcceptor->getResolvedTemplateTypeMap(); + } + + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->parametersAcceptor->getCallSiteVarianceMap(); + } + + public function getParameters(): array + { + return $this->parametersAcceptor->getParameters(); + } + + public function isVariadic(): bool + { + return $this->parametersAcceptor->isVariadic(); + } + + public function getReturnTypeWithUnresolvableTemplateTypes(): Type + { + return $this->parametersAcceptor->getReturnTypeWithUnresolvableTemplateTypes(); + } + + public function getPhpDocReturnTypeWithUnresolvableTemplateTypes(): Type + { + return $this->parametersAcceptor->getPhpDocReturnTypeWithUnresolvableTemplateTypes(); + } + + public function getReturnType(): Type + { + return $this->parametersAcceptor->getReturnType(); + } + + public function getPhpDocReturnType(): Type + { + return $this->parametersAcceptor->getPhpDocReturnType(); + } + + public function getNativeReturnType(): Type + { + return $this->parametersAcceptor->getNativeReturnType(); + } + + public function getThrowPoints(): array + { + return $this->throwPoints; + } + + public function isPure(): TrinaryLogic + { + return $this->isPure; + } + + public function getImpurePoints(): array + { + return $this->impurePoints; + } + + public function getInvalidateExpressions(): array + { + return $this->invalidateExpressions; + } + + public function getUsedVariables(): array + { + return $this->usedVariables; + } + +} diff --git a/src/Reflection/ResolvedFunctionVariantWithOriginal.php b/src/Reflection/ResolvedFunctionVariantWithOriginal.php new file mode 100644 index 0000000000..6d72ef75bc --- /dev/null +++ b/src/Reflection/ResolvedFunctionVariantWithOriginal.php @@ -0,0 +1,299 @@ + $passedArgs + */ + public function __construct( + private ParametersAcceptorWithPhpDocs $parametersAcceptor, + private TemplateTypeMap $resolvedTemplateTypeMap, + private TemplateTypeVarianceMap $callSiteVarianceMap, + private array $passedArgs, + ) + { + } + + public function getOriginalParametersAcceptor(): ParametersAcceptor + { + return $this->parametersAcceptor; + } + + public function getTemplateTypeMap(): TemplateTypeMap + { + return $this->parametersAcceptor->getTemplateTypeMap(); + } + + public function getResolvedTemplateTypeMap(): TemplateTypeMap + { + return $this->resolvedTemplateTypeMap; + } + + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->callSiteVarianceMap; + } + + public function getParameters(): array + { + $parameters = $this->parameters; + + if ($parameters === null) { + $parameters = array_map( + function (ParameterReflectionWithPhpDocs $param): ParameterReflectionWithPhpDocs { + $paramType = TypeUtils::resolveLateResolvableTypes( + TemplateTypeHelper::resolveTemplateTypes( + $this->resolveConditionalTypesForParameter($param->getType()), + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createContravariant(), + ), + false, + ); + + $paramOutType = $param->getOutType(); + if ($paramOutType !== null) { + $paramOutType = TypeUtils::resolveLateResolvableTypes( + TemplateTypeHelper::resolveTemplateTypes( + $this->resolveConditionalTypesForParameter($paramOutType), + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ), + false, + ); + } + + $closureThisType = $param->getClosureThisType(); + if ($closureThisType !== null) { + $closureThisType = TypeUtils::resolveLateResolvableTypes( + TemplateTypeHelper::resolveTemplateTypes( + $this->resolveConditionalTypesForParameter($closureThisType), + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ), + false, + ); + } + + return new DummyParameterWithPhpDocs( + $param->getName(), + $paramType, + $param->isOptional(), + $param->passedByReference(), + $param->isVariadic(), + $param->getDefaultValue(), + $param->getNativeType(), + $param->getPhpDocType(), + $paramOutType, + $param->isImmediatelyInvokedCallable(), + $closureThisType, + ); + }, + $this->parametersAcceptor->getParameters(), + ); + + $this->parameters = $parameters; + } + + return $parameters; + } + + public function isVariadic(): bool + { + return $this->parametersAcceptor->isVariadic(); + } + + public function getReturnTypeWithUnresolvableTemplateTypes(): Type + { + return $this->returnTypeWithUnresolvableTemplateTypes ??= + $this->resolveConditionalTypesForParameter( + $this->resolveResolvableTemplateTypes($this->parametersAcceptor->getReturnType(), TemplateTypeVariance::createCovariant()), + ); + } + + public function getPhpDocReturnTypeWithUnresolvableTemplateTypes(): Type + { + return $this->phpDocReturnTypeWithUnresolvableTemplateTypes ??= + $this->resolveConditionalTypesForParameter( + $this->resolveResolvableTemplateTypes($this->parametersAcceptor->getPhpDocReturnType(), TemplateTypeVariance::createCovariant()), + ); + } + + public function getReturnType(): Type + { + $type = $this->returnType; + + if ($type === null) { + $type = TypeUtils::resolveLateResolvableTypes( + TemplateTypeHelper::resolveTemplateTypes( + $this->getReturnTypeWithUnresolvableTemplateTypes(), + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ), + false, + ); + + $this->returnType = $type; + } + + return $type; + } + + public function getPhpDocReturnType(): Type + { + $type = $this->phpDocReturnType; + + if ($type === null) { + $type = TypeUtils::resolveLateResolvableTypes( + TemplateTypeHelper::resolveTemplateTypes( + $this->getPhpDocReturnTypeWithUnresolvableTemplateTypes(), + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ), + false, + ); + + $this->phpDocReturnType = $type; + } + + return $type; + } + + public function getNativeReturnType(): Type + { + return $this->parametersAcceptor->getNativeReturnType(); + } + + private function resolveResolvableTemplateTypes(Type $type, TemplateTypeVariance $positionVariance): Type + { + $references = $type->getReferencedTemplateTypes($positionVariance); + + $objectCb = function (Type $type, callable $traverse) use ($references): Type { + if ( + $type instanceof TemplateType + && !$type->isArgument() + && $type->getScope()->getFunctionName() !== null + ) { + $newType = $this->resolvedTemplateTypeMap->getType($type->getName()); + if ($newType === null || $newType instanceof ErrorType) { + return $traverse($type); + } + + $newType = TemplateTypeHelper::generalizeInferredTemplateType($type, $newType); + $variance = TemplateTypeVariance::createInvariant(); + foreach ($references as $reference) { + // this uses identity to distinguish between different occurrences of the same template type + // see https://github.com/phpstan/phpstan-src/pull/2485#discussion_r1328555397 for details + if ($reference->getType() === $type) { + $variance = $reference->getPositionVariance(); + break; + } + } + + $callSiteVariance = $this->callSiteVarianceMap->getVariance($type->getName()); + if ($callSiteVariance === null || $callSiteVariance->invariant()) { + return $newType; + } + + if (!$callSiteVariance->covariant() && $variance->covariant()) { + return $traverse($type->getBound()); + } + + if (!$callSiteVariance->contravariant() && $variance->contravariant()) { + return new NonAcceptingNeverType(); + } + + return $newType; + } + + return $traverse($type); + }; + + return TypeTraverser::map($type, function (Type $type, callable $traverse) use ($references, $objectCb): Type { + if (BleedingEdgeToggle::isBleedingEdge() && $type instanceof GenericObjectType) { + return TypeTraverser::map($type, $objectCb); + } + + if ($type instanceof TemplateType && !$type->isArgument()) { + $newType = $this->resolvedTemplateTypeMap->getType($type->getName()); + if ($newType === null || $newType instanceof ErrorType) { + return $traverse($type); + } + + $variance = TemplateTypeVariance::createInvariant(); + foreach ($references as $reference) { + // this uses identity to distinguish between different occurrences of the same template type + // see https://github.com/phpstan/phpstan-src/pull/2485#discussion_r1328555397 for details + if ($reference->getType() === $type) { + $variance = $reference->getPositionVariance(); + break; + } + } + + $callSiteVariance = $this->callSiteVarianceMap->getVariance($type->getName()); + if ($callSiteVariance === null || $callSiteVariance->invariant()) { + return $newType; + } + + if (!$callSiteVariance->covariant() && $variance->covariant()) { + return $traverse($type->getBound()); + } + + if (!$callSiteVariance->contravariant() && $variance->contravariant()) { + return new NonAcceptingNeverType(); + } + + return $newType; + } + + return $traverse($type); + }); + } + + private function resolveConditionalTypesForParameter(Type $type): Type + { + return TypeTraverser::map($type, function (Type $type, callable $traverse): Type { + if ($type instanceof ConditionalTypeForParameter && array_key_exists($type->getParameterName(), $this->passedArgs)) { + $type = $type->toConditional($this->passedArgs[$type->getParameterName()]); + } + + return $traverse($type); + }); + } + +} diff --git a/src/Reflection/ResolvedMethodReflection.php b/src/Reflection/ResolvedMethodReflection.php index f1514cb786..31433330b8 100644 --- a/src/Reflection/ResolvedMethodReflection.php +++ b/src/Reflection/ResolvedMethodReflection.php @@ -4,16 +4,31 @@ use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; +use function is_bool; -class ResolvedMethodReflection implements MethodReflection +class ResolvedMethodReflection implements ExtendedMethodReflection { - /** @var ParametersAcceptor[]|null */ + /** @var ParametersAcceptorWithPhpDocs[]|null */ private ?array $variants = null; - public function __construct(private MethodReflection $reflection, private TemplateTypeMap $resolvedTemplateTypeMap) + /** @var ParametersAcceptorWithPhpDocs[]|null */ + private ?array $namedArgumentVariants = null; + + private ?Assertions $asserts = null; + + private Type|false|null $selfOutType = false; + + public function __construct( + private ExtendedMethodReflection $reflection, + private TemplateTypeMap $resolvedTemplateTypeMap, + private TemplateTypeVarianceMap $callSiteVarianceMap, + ) { } @@ -27,9 +42,6 @@ public function getPrototype(): ClassMemberReflection return $this->reflection->getPrototype(); } - /** - * @return ParametersAcceptor[] - */ public function getVariants(): array { $variants = $this->variants; @@ -37,18 +49,41 @@ public function getVariants(): array return $variants; } - $variants = []; - foreach ($this->reflection->getVariants() as $variant) { - $variants[] = new ResolvedFunctionVariant( + return $this->variants = $this->resolveVariants($this->reflection->getVariants()); + } + + public function getNamedArgumentsVariants(): ?array + { + $variants = $this->namedArgumentVariants; + if ($variants !== null) { + return $variants; + } + + $innerVariants = $this->reflection->getNamedArgumentsVariants(); + if ($innerVariants === null) { + return null; + } + + return $this->namedArgumentVariants = $this->resolveVariants($innerVariants); + } + + /** + * @param ParametersAcceptorWithPhpDocs[] $variants + * @return ResolvedFunctionVariant[] + */ + private function resolveVariants(array $variants): array + { + $result = []; + foreach ($variants as $variant) { + $result[] = new ResolvedFunctionVariantWithOriginal( $variant, $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, [], ); } - $this->variants = $variants; - - return $variants; + return $result; } public function getDeclaringClass(): ClassReflection @@ -100,6 +135,11 @@ public function isFinal(): TrinaryLogic return $this->reflection->isFinal(); } + public function isFinalByKeyword(): TrinaryLogic + { + return $this->reflection->isFinalByKeyword(); + } + public function isInternal(): TrinaryLogic { return $this->reflection->isInternal(); @@ -115,4 +155,53 @@ public function hasSideEffects(): TrinaryLogic return $this->reflection->hasSideEffects(); } + public function isPure(): TrinaryLogic + { + return $this->reflection->isPure(); + } + + public function getAsserts(): Assertions + { + return $this->asserts ??= $this->reflection->getAsserts()->mapTypes(fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( + $type, + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createInvariant(), + )); + } + + public function getSelfOutType(): ?Type + { + if ($this->selfOutType === false) { + $selfOutType = $this->reflection->getSelfOutType(); + if ($selfOutType !== null) { + $selfOutType = TemplateTypeHelper::resolveTemplateTypes( + $selfOutType, + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createInvariant(), + ); + } + + $this->selfOutType = $selfOutType; + } + + return $this->selfOutType; + } + + public function returnsByReference(): TrinaryLogic + { + return $this->reflection->returnsByReference(); + } + + public function isAbstract(): TrinaryLogic + { + $abstract = $this->reflection->isAbstract(); + if (is_bool($abstract)) { + return TrinaryLogic::createFromBoolean($abstract); + } + + return $abstract; + } + } diff --git a/src/Reflection/ResolvedPropertyReflection.php b/src/Reflection/ResolvedPropertyReflection.php index 0596d61d6b..7888b26abd 100644 --- a/src/Reflection/ResolvedPropertyReflection.php +++ b/src/Reflection/ResolvedPropertyReflection.php @@ -6,6 +6,8 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; class ResolvedPropertyReflection implements WrapperPropertyReflection @@ -15,7 +17,11 @@ class ResolvedPropertyReflection implements WrapperPropertyReflection private ?Type $writableType = null; - public function __construct(private PropertyReflection $reflection, private TemplateTypeMap $templateTypeMap) + public function __construct( + private PropertyReflection $reflection, + private TemplateTypeMap $templateTypeMap, + private TemplateTypeVarianceMap $callSiteVarianceMap, + ) { } @@ -63,10 +69,14 @@ public function getReadableType(): Type $type = TemplateTypeHelper::resolveTemplateTypes( $this->reflection->getReadableType(), $this->templateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), ); $type = TemplateTypeHelper::resolveTemplateTypes( $type, $this->templateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), ); $this->readableType = $type; @@ -84,10 +94,14 @@ public function getWritableType(): Type $type = TemplateTypeHelper::resolveTemplateTypes( $this->reflection->getWritableType(), $this->templateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createContravariant(), ); $type = TemplateTypeHelper::resolveTemplateTypes( $type, $this->templateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createContravariant(), ); $this->writableType = $type; diff --git a/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php b/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php index 5207e08386..9b8c21910b 100644 --- a/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php +++ b/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php @@ -22,16 +22,17 @@ class FunctionSignatureMapProvider implements SignatureMapProvider { - /** @var mixed[]|null */ - private ?array $signatureMap = null; + /** @var array */ + private static array $signatureMaps = []; /** @var array|null */ - private ?array $functionMetadata = null; + private static ?array $functionMetadata = null; public function __construct( private SignatureMapParser $parser, private InitializerExprTypeResolver $initializerExprTypeResolver, private PhpVersion $phpVersion, + private bool $stricterFunctionMap, ) { } @@ -64,7 +65,7 @@ public function getFunctionSignatures(string $functionName, ?string $className, $variantFunctionName = $functionName . '\'' . $i; } - return $signatures; + return ['positional' => $signatures, 'named' => null]; } private function createSignature(string $functionName, ?string $className, ?ReflectionFunctionAbstract $reflectionFunction): FunctionSignature @@ -100,6 +101,7 @@ private function createSignature(string $functionName, ?string $className, ?Refl $nativeParameters[$i]->getDefaultValueExpression(), InitializerExprContext::fromReflectionParameter($nativeParameters[$i]), ) : null, + $parameter->getOutType(), ); } @@ -124,7 +126,7 @@ public function hasMethodMetadata(string $className, string $methodName): bool public function hasFunctionMetadata(string $name): bool { - $signatureMap = $this->getFunctionMetadataMap(); + $signatureMap = self::getFunctionMetadataMap(); return array_key_exists(strtolower($name), $signatureMap); } @@ -147,21 +149,21 @@ public function getFunctionMetadata(string $functionName): array throw new ShouldNotHappenException(); } - return $this->getFunctionMetadataMap()[$functionName]; + return self::getFunctionMetadataMap()[$functionName]; } /** * @return array */ - private function getFunctionMetadataMap(): array + private static function getFunctionMetadataMap(): array { - if ($this->functionMetadata === null) { + if (self::$functionMetadata === null) { /** @var array $metadata */ $metadata = require __DIR__ . '/../../../resources/functionMetadata.php'; - $this->functionMetadata = array_change_key_case($metadata, CASE_LOWER); + self::$functionMetadata = array_change_key_case($metadata, CASE_LOWER); } - return $this->functionMetadata; + return self::$functionMetadata; } /** @@ -169,54 +171,82 @@ private function getFunctionMetadataMap(): array */ public function getSignatureMap(): array { - if ($this->signatureMap === null) { - $signatureMap = require __DIR__ . '/../../../resources/functionMap.php'; - if (!is_array($signatureMap)) { + $cacheKey = sprintf('%d-%d', $this->phpVersion->getVersionId(), $this->stricterFunctionMap ? 1 : 0); + if (array_key_exists($cacheKey, self::$signatureMaps)) { + return self::$signatureMaps[$cacheKey]; + } + + $signatureMap = require __DIR__ . '/../../../resources/functionMap.php'; + if (!is_array($signatureMap)) { + throw new ShouldNotHappenException('Signature map could not be loaded.'); + } + + $signatureMap = array_change_key_case($signatureMap, CASE_LOWER); + + if ($this->stricterFunctionMap) { + $stricterFunctionMap = require __DIR__ . '/../../../resources/functionMap_bleedingEdge.php'; + if (!is_array($stricterFunctionMap)) { throw new ShouldNotHappenException('Signature map could not be loaded.'); } - $signatureMap = array_change_key_case($signatureMap, CASE_LOWER); + $signatureMap = $this->computeSignatureMap($signatureMap, $stricterFunctionMap); - if ($this->phpVersion->getVersionId() >= 70400) { - $php74MapDelta = require __DIR__ . '/../../../resources/functionMap_php74delta.php'; - if (!is_array($php74MapDelta)) { + if ($this->phpVersion->getVersionId() >= 80000) { + $php80StricterFunctionMapDelta = require __DIR__ . '/../../../resources/functionMap_php80delta_bleedingEdge.php'; + if (!is_array($php80StricterFunctionMapDelta)) { throw new ShouldNotHappenException('Signature map could not be loaded.'); } - $signatureMap = $this->computeSignatureMap($signatureMap, $php74MapDelta); + $signatureMap = $this->computeSignatureMap($signatureMap, $php80StricterFunctionMapDelta); } + } - if ($this->phpVersion->getVersionId() >= 80000) { - $php80MapDelta = require __DIR__ . '/../../../resources/functionMap_php80delta.php'; - if (!is_array($php80MapDelta)) { - throw new ShouldNotHappenException('Signature map could not be loaded.'); - } + if ($this->phpVersion->getVersionId() >= 70400) { + $php74MapDelta = require __DIR__ . '/../../../resources/functionMap_php74delta.php'; + if (!is_array($php74MapDelta)) { + throw new ShouldNotHappenException('Signature map could not be loaded.'); + } + + $signatureMap = $this->computeSignatureMap($signatureMap, $php74MapDelta); + } - $signatureMap = $this->computeSignatureMap($signatureMap, $php80MapDelta); + if ($this->phpVersion->getVersionId() >= 80000) { + $php80MapDelta = require __DIR__ . '/../../../resources/functionMap_php80delta.php'; + if (!is_array($php80MapDelta)) { + throw new ShouldNotHappenException('Signature map could not be loaded.'); } - if ($this->phpVersion->getVersionId() >= 80100) { - $php81MapDelta = require __DIR__ . '/../../../resources/functionMap_php81delta.php'; - if (!is_array($php81MapDelta)) { - throw new ShouldNotHappenException('Signature map could not be loaded.'); - } + $signatureMap = $this->computeSignatureMap($signatureMap, $php80MapDelta); + } - $signatureMap = $this->computeSignatureMap($signatureMap, $php81MapDelta); + if ($this->phpVersion->getVersionId() >= 80100) { + $php81MapDelta = require __DIR__ . '/../../../resources/functionMap_php81delta.php'; + if (!is_array($php81MapDelta)) { + throw new ShouldNotHappenException('Signature map could not be loaded.'); } - if ($this->phpVersion->getVersionId() >= 80200) { - $php82MapDelta = require __DIR__ . '/../../../resources/functionMap_php82delta.php'; - if (!is_array($php82MapDelta)) { - throw new ShouldNotHappenException('Signature map could not be loaded.'); - } + $signatureMap = $this->computeSignatureMap($signatureMap, $php81MapDelta); + } - $signatureMap = $this->computeSignatureMap($signatureMap, $php82MapDelta); + if ($this->phpVersion->getVersionId() >= 80200) { + $php82MapDelta = require __DIR__ . '/../../../resources/functionMap_php82delta.php'; + if (!is_array($php82MapDelta)) { + throw new ShouldNotHappenException('Signature map could not be loaded.'); } - $this->signatureMap = $signatureMap; + $signatureMap = $this->computeSignatureMap($signatureMap, $php82MapDelta); } - return $this->signatureMap; + if ($this->phpVersion->getVersionId() >= 80300) { + $php83MapDelta = require __DIR__ . '/../../../resources/functionMap_php83delta.php'; + if (!is_array($php83MapDelta)) { + throw new ShouldNotHappenException('Signature map could not be loaded.'); + } + + $signatureMap = $this->computeSignatureMap($signatureMap, $php83MapDelta); + } + + return self::$signatureMaps[$cacheKey] = $signatureMap; } /** @@ -236,4 +266,14 @@ private function computeSignatureMap(array $signatureMap, array $delta): array return $signatureMap; } + public function hasClassConstantMetadata(string $className, string $constantName): bool + { + return false; + } + + public function getClassConstantMetadata(string $className, string $constantName): array + { + throw new ShouldNotHappenException(); + } + } diff --git a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php index 76fa77b362..7c850a582e 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -8,22 +8,17 @@ use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDoc\StubPhpDocProvider; -use PHPStan\Reflection\FunctionVariant; +use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\FunctionVariantWithPhpDocs; use PHPStan\Reflection\Native\NativeFunctionReflection; -use PHPStan\Reflection\Native\NativeParameterReflection; +use PHPStan\Reflection\Native\NativeParameterWithPhpDocsReflection; use PHPStan\TrinaryLogic; -use PHPStan\Type\ArrayType; -use PHPStan\Type\BooleanType; use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\FloatType; use PHPStan\Type\Generic\TemplateTypeMap; -use PHPStan\Type\IntegerType; -use PHPStan\Type\NullType; -use PHPStan\Type\StringAlwaysAcceptingObjectWithToStringType; -use PHPStan\Type\StringType; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; -use PHPStan\Type\UnionType; +use function array_key_exists; use function array_map; use function strtolower; @@ -31,7 +26,7 @@ class NativeFunctionReflectionProvider { /** @var NativeFunctionReflection[] */ - private static array $functionMap = []; + private array $functionMap = []; public function __construct(private SignatureMapProvider $signatureMapProvider, private Reflector $reflector, private FileTypeMapper $fileTypeMapper, private StubPhpDocProvider $stubPhpDocProvider) { @@ -40,8 +35,9 @@ public function __construct(private SignatureMapProvider $signatureMapProvider, public function findFunctionReflection(string $functionName): ?NativeFunctionReflection { $lowerCasedFunctionName = strtolower($functionName); - if (isset(self::$functionMap[$lowerCasedFunctionName])) { - return self::$functionMap[$lowerCasedFunctionName]; + $realFunctionName = $lowerCasedFunctionName; + if (isset($this->functionMap[$lowerCasedFunctionName])) { + return $this->functionMap[$lowerCasedFunctionName]; } if (!$this->signatureMapProvider->hasFunctionSignature($lowerCasedFunctionName)) { @@ -51,9 +47,15 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $throwType = null; $reflectionFunctionAdapter = null; $isDeprecated = false; + $phpDocReturnType = null; + $asserts = Assertions::createEmpty(); + $docComment = null; + $returnsByReference = TrinaryLogic::createMaybe(); try { $reflectionFunction = $this->reflector->reflectFunction($functionName); $reflectionFunctionAdapter = new ReflectionFunction($reflectionFunction); + $returnsByReference = TrinaryLogic::createFromBoolean($reflectionFunctionAdapter->returnsReference()); + $realFunctionName = $reflectionFunction->getName(); if ($reflectionFunction->getFileName() !== null) { $fileName = $reflectionFunction->getFileName(); $docComment = $reflectionFunction->getDocComment(); @@ -70,76 +72,64 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef // pass } - $functionSignatures = $this->signatureMapProvider->getFunctionSignatures($lowerCasedFunctionName, null, $reflectionFunctionAdapter); + $functionSignaturesResult = $this->signatureMapProvider->getFunctionSignatures($lowerCasedFunctionName, null, $reflectionFunctionAdapter); - $phpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($lowerCasedFunctionName, array_map(static fn (ParameterSignature $parameter): string => $parameter->getName(), $functionSignatures[0]->getParameters())); - if ($phpDoc !== null && $phpDoc->getThrowsTag() !== null) { - $throwType = $phpDoc->getThrowsTag()->getType(); + $phpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($lowerCasedFunctionName, array_map(static fn (ParameterSignature $parameter): string => $parameter->getName(), $functionSignaturesResult['positional'][0]->getParameters())); + if ($phpDoc !== null) { + if ($phpDoc->hasPhpDocString()) { + $docComment = $phpDoc->getPhpDocString(); + } + if ($phpDoc->getThrowsTag() !== null) { + $throwType = $phpDoc->getThrowsTag()->getType(); + } + $asserts = Assertions::createFromResolvedPhpDocBlock($phpDoc); + $phpDocReturnType = $this->getReturnTypeFromPhpDoc($phpDoc); } - $variants = []; - $functionSignatures = $this->signatureMapProvider->getFunctionSignatures($lowerCasedFunctionName, null, $reflectionFunctionAdapter); - foreach ($functionSignatures as $functionSignature) { - $variants[] = new FunctionVariant( - TemplateTypeMap::createEmpty(), - null, - array_map(static function (ParameterSignature $parameterSignature) use ($lowerCasedFunctionName, $phpDoc): NativeParameterReflection { - $type = $parameterSignature->getType(); - - $phpDocType = null; - if ($phpDoc !== null) { - $phpDocParam = $phpDoc->getParamTags()[$parameterSignature->getName()] ?? null; - if ($phpDocParam !== null) { - $phpDocType = $phpDocParam->getType(); + $variantsByType = ['positional' => []]; + foreach ($functionSignaturesResult as $signatureType => $functionSignatures) { + foreach ($functionSignatures ?? [] as $functionSignature) { + $variantsByType[$signatureType][] = new FunctionVariantWithPhpDocs( + TemplateTypeMap::createEmpty(), + null, + array_map(static function (ParameterSignature $parameterSignature) use ($phpDoc): NativeParameterWithPhpDocsReflection { + $type = $parameterSignature->getType(); + + $phpDocType = null; + $immediatelyInvokedCallable = TrinaryLogic::createMaybe(); + $closureThisType = null; + if ($phpDoc !== null) { + if (array_key_exists($parameterSignature->getName(), $phpDoc->getParamTags())) { + $phpDocType = $phpDoc->getParamTags()[$parameterSignature->getName()]->getType(); + } + if (array_key_exists($parameterSignature->getName(), $phpDoc->getParamsImmediatelyInvokedCallable())) { + $immediatelyInvokedCallable = TrinaryLogic::createFromBoolean($phpDoc->getParamsImmediatelyInvokedCallable()[$parameterSignature->getName()]); + } + if (array_key_exists($parameterSignature->getName(), $phpDoc->getParamClosureThisTags())) { + $closureThisType = $phpDoc->getParamClosureThisTags()[$parameterSignature->getName()]->getType(); + } } - } - if ( - $parameterSignature->getName() === 'values' - && ( - $lowerCasedFunctionName === 'printf' - || $lowerCasedFunctionName === 'sprintf' - ) - ) { - $type = new UnionType([ - new StringAlwaysAcceptingObjectWithToStringType(), - new IntegerType(), - new FloatType(), - new NullType(), - new BooleanType(), - ]); - } - if ( - $parameterSignature->getName() === 'fields' - && $lowerCasedFunctionName === 'fputcsv' - ) { - $type = new ArrayType( - new UnionType([ - new StringType(), - new IntegerType(), - ]), - new UnionType([ - new StringAlwaysAcceptingObjectWithToStringType(), - new IntegerType(), - new FloatType(), - new NullType(), - new BooleanType(), - ]), + return new NativeParameterWithPhpDocsReflection( + $parameterSignature->getName(), + $parameterSignature->isOptional(), + TypehintHelper::decideType($type, $phpDocType), + $phpDocType ?? new MixedType(), + $type, + $parameterSignature->passedByReference(), + $parameterSignature->isVariadic(), + $parameterSignature->getDefaultValue(), + $phpDoc !== null ? NativeFunctionReflectionProvider::getParamOutTypeFromPhpDoc($parameterSignature->getName(), $phpDoc) : null, + $immediatelyInvokedCallable, + $closureThisType, ); - } - - return new NativeParameterReflection( - $parameterSignature->getName(), - $parameterSignature->isOptional(), - TypehintHelper::decideType($type, $phpDocType), - $parameterSignature->passedByReference(), - $parameterSignature->isVariadic(), - $parameterSignature->getDefaultValue(), - ); - }, $functionSignature->getParameters()), - $functionSignature->isVariadic(), - TypehintHelper::decideType($functionSignature->getReturnType(), $phpDoc !== null ? $this->getReturnTypeFromPhpDoc($phpDoc) : null), - ); + }, $functionSignature->getParameters()), + $functionSignature->isVariadic(), + TypehintHelper::decideType($functionSignature->getReturnType(), $phpDocReturnType), + $phpDocReturnType ?? new MixedType(), + $functionSignature->getReturnType(), + ); + } } if ($this->signatureMapProvider->hasFunctionMetadata($lowerCasedFunctionName)) { @@ -149,13 +139,17 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef } $functionReflection = new NativeFunctionReflection( - $lowerCasedFunctionName, - $variants, + $realFunctionName, + $variantsByType['positional'], + $variantsByType['named'] ?? null, $throwType, $hasSideEffects, $isDeprecated, + $asserts, + $docComment, + $returnsByReference, ); - self::$functionMap[$lowerCasedFunctionName] = $functionReflection; + $this->functionMap[$lowerCasedFunctionName] = $functionReflection; return $functionReflection; } @@ -170,4 +164,15 @@ private function getReturnTypeFromPhpDoc(ResolvedPhpDocBlock $phpDoc): ?Type return $returnTag->getType(); } + private static function getParamOutTypeFromPhpDoc(string $paramName, ResolvedPhpDocBlock $stubPhpDoc): ?Type + { + $paramOutTags = $stubPhpDoc->getParamOutTags(); + + if (array_key_exists($paramName, $paramOutTags)) { + return $paramOutTags[$paramName]->getType(); + } + + return null; + } + } diff --git a/src/Reflection/SignatureMap/ParameterSignature.php b/src/Reflection/SignatureMap/ParameterSignature.php index a06284186a..fc9316c300 100644 --- a/src/Reflection/SignatureMap/ParameterSignature.php +++ b/src/Reflection/SignatureMap/ParameterSignature.php @@ -16,6 +16,7 @@ public function __construct( private PassedByReference $passedByReference, private bool $variadic, private ?Type $defaultValue, + private ?Type $outType, ) { } @@ -55,4 +56,9 @@ public function getDefaultValue(): ?Type return $this->defaultValue; } + public function getOutType(): ?Type + { + return $this->outType; + } + } diff --git a/src/Reflection/SignatureMap/Php8SignatureMapProvider.php b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php index ec36447f27..d114609b35 100644 --- a/src/Reflection/SignatureMap/Php8SignatureMapProvider.php +++ b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php @@ -2,9 +2,10 @@ namespace PHPStan\Reflection\SignatureMap; +use PhpParser\Node\AttributeGroup; use PhpParser\Node\Expr\Variable; -use PhpParser\Node\FunctionLike; use PhpParser\Node\Scalar\String_; +use PhpParser\Node\Stmt\ClassConst; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; @@ -38,6 +39,9 @@ class Php8SignatureMapProvider implements SignatureMapProvider /** @var array> */ private array $methodNodes = []; + /** @var array> */ + private array $constantTypes = []; + private Php8StubsMap $map; public function __construct( @@ -70,7 +74,6 @@ public function hasMethodSignature(string $className, string $methodName): bool /** * @return array{ClassMethod, string}|null - * @throws ShouldNotHappenException */ private function findMethodNode(string $className, string $methodName): ?array { @@ -98,7 +101,7 @@ private function findMethodNode(string $className, string $methodName): ?array } if ($stmt->name->toLowerString() === $lowerMethodName) { - if (!$this->isForCurrentVersion($stmt)) { + if (!$this->isForCurrentVersion($stmt->attrGroups)) { continue; } return $this->methodNodes[$lowerClassName][$lowerMethodName] = [$stmt, $stubFile]; @@ -108,9 +111,12 @@ private function findMethodNode(string $className, string $methodName): ?array return null; } - private function isForCurrentVersion(FunctionLike $functionLike): bool + /** + * @param AttributeGroup[] $attrGroups + */ + private function isForCurrentVersion(array $attrGroups): bool { - foreach ($functionLike->getAttrGroups() as $attrGroup) { + foreach ($attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { if ($attr->name->toString() === 'Until') { $arg = $attr->args[0]->value; @@ -173,7 +179,7 @@ public function getMethodSignatures(string $className, string $methodName, ?Refl return $this->getMergedSignatures($signature, $functionMapSignatures); } - return [$signature]; + return ['positional' => [$signature], 'named' => null]; } public function getFunctionSignatures(string $functionName, ?string $className, ReflectionFunctionAbstract|null $reflectionFunction): array @@ -190,7 +196,7 @@ public function getFunctionSignatures(string $functionName, ?string $className, throw new ShouldNotHappenException(sprintf('Function %s stub not found in %s.', $functionName, $stubFile)); } foreach ($functions[$lowerName] as $functionNode) { - if (!$this->isForCurrentVersion($functionNode->getNode())) { + if (!$this->isForCurrentVersion($functionNode->getNode()->getAttrGroups())) { continue; } @@ -201,23 +207,85 @@ public function getFunctionSignatures(string $functionName, ?string $className, return $this->getMergedSignatures($signature, $functionMapSignatures); } - return [$signature]; + return ['positional' => [$signature], 'named' => null]; } throw new ShouldNotHappenException(sprintf('Function %s stub not found in %s.', $functionName, $stubFile)); } /** - * @param array $functionMapSignatures - * @return array + * @param array{positional: array, named: ?array} $functionMapSignatures + * @return array{positional: array, named: ?array} */ private function getMergedSignatures(FunctionSignature $nativeSignature, array $functionMapSignatures): array { - if (count($functionMapSignatures) === 1) { - return [$this->mergeSignatures($nativeSignature, $functionMapSignatures[0])]; + if (count($functionMapSignatures['positional']) === 1) { + return ['positional' => [$this->mergeSignatures($nativeSignature, $functionMapSignatures['positional'][0])], 'named' => null]; + } + + if (count($functionMapSignatures['positional']) === 0) { + return ['positional' => [], 'named' => null]; + } + + $nativeParams = $nativeSignature->getParameters(); + $namedArgumentsVariants = []; + $allParamNamesMatchNative = true; + foreach ($functionMapSignatures['positional'] as $functionMapSignature) { + $isPrevParamVariadic = false; + $hasMiddleVariadicParam = false; + // avoid weird functions like array_diff_uassoc + foreach ($functionMapSignature->getParameters() as $i => $functionParam) { + $nativeParam = $nativeParams[$i] ?? null; + $allParamNamesMatchNative = $allParamNamesMatchNative && $nativeParam !== null && $functionParam->getName() === $nativeParam->getName(); + $hasMiddleVariadicParam = $hasMiddleVariadicParam || $isPrevParamVariadic; + $isPrevParamVariadic = $functionParam->isVariadic() || ( + $nativeParam !== null + ? $nativeParam->isVariadic() + : false + ); + } + + if ($hasMiddleVariadicParam) { + continue; + } + + $parameters = []; + foreach ($functionMapSignature->getParameters() as $i => $functionParam) { + if (!array_key_exists($i, $nativeParams)) { + continue 2; + } + + // it seems that variadic parameters cannot be named in native functions/methods. + $nativeParam = $nativeParams[$i]; + if ($nativeParam->isVariadic()) { + break; + } + + $parameters[] = new ParameterSignature( + $nativeParam->getName(), + $functionParam->isOptional(), + $functionParam->getType(), + $functionParam->getNativeType(), + $functionParam->passedByReference(), + $functionParam->isVariadic(), + $functionParam->getDefaultValue(), + $functionParam->getOutType(), + ); + } + + $namedArgumentsVariants[] = new FunctionSignature( + $parameters, + $functionMapSignature->getReturnType(), + $functionMapSignature->getNativeReturnType(), + $functionMapSignature->isVariadic(), + ); } - return $functionMapSignatures; + if ($allParamNamesMatchNative || count($namedArgumentsVariants) === 0) { + $namedArgumentsVariants = null; + } + + return ['positional' => $functionMapSignatures['positional'], 'named' => $namedArgumentsVariants]; } private function mergeSignatures(FunctionSignature $nativeSignature, FunctionSignature $functionMapSignature): FunctionSignature @@ -245,6 +313,7 @@ private function mergeSignatures(FunctionSignature $nativeSignature, FunctionSig $nativeParameter->passedByReference()->yes() ? $functionMapParameter->passedByReference() : $nativeParameter->passedByReference(), $nativeParameter->isVariadic(), $nativeParameter->getDefaultValue(), + $nativeParameter->getOutType(), ); } @@ -342,6 +411,7 @@ private function getSignature( $param->default, InitializerExprContext::fromStubParameter($className, $stubFile, $function), ) : null, + null, ); $variadic = $variadic || $param->variadic; @@ -357,4 +427,76 @@ private function getSignature( ); } + public function hasClassConstantMetadata(string $className, string $constantName): bool + { + $lowerClassName = strtolower($className); + if (!array_key_exists($lowerClassName, $this->map->classes)) { + return false; + } + + return $this->findConstantType($className, $constantName) !== null; + } + + public function getClassConstantMetadata(string $className, string $constantName): array + { + $lowerClassName = strtolower($className); + if (!array_key_exists($lowerClassName, $this->map->classes)) { + throw new ShouldNotHappenException(); + } + + $type = $this->findConstantType($className, $constantName); + if ($type === null) { + throw new ShouldNotHappenException(); + } + + return [ + 'nativeType' => $type, + ]; + } + + private function findConstantType(string $className, string $constantName): ?Type + { + $lowerClassName = strtolower($className); + $lowerConstantName = strtolower($constantName); + if (isset($this->constantTypes[$lowerClassName][$lowerConstantName])) { + return $this->constantTypes[$lowerClassName][$lowerConstantName]; + } + + $stubFile = self::DIRECTORY . '/' . $this->map->classes[$lowerClassName]; + $nodes = $this->fileNodesFetcher->fetchNodes($stubFile); + $classes = $nodes->getClassNodes(); + if (count($classes) !== 1) { + throw new ShouldNotHappenException(sprintf('Class %s stub not found in %s.', $className, $stubFile)); + } + + $class = $classes[$lowerClassName]; + if (count($class) !== 1) { + throw new ShouldNotHappenException(sprintf('Class %s stub not found in %s.', $className, $stubFile)); + } + + foreach ($class[0]->getNode()->stmts as $stmt) { + if (!$stmt instanceof ClassConst) { + continue; + } + + foreach ($stmt->consts as $const) { + if ($const->name->toString() !== $constantName) { + continue; + } + + if (!$this->isForCurrentVersion($stmt->attrGroups)) { + continue; + } + + if ($stmt->type === null) { + return null; + } + + return $this->constantTypes[$lowerClassName][$lowerConstantName] = ParserNodeTypeToPHPStanType::resolve($stmt->type, null); + } + } + + return null; + } + } diff --git a/src/Reflection/SignatureMap/SignatureMapParser.php b/src/Reflection/SignatureMap/SignatureMapParser.php index f7e1a844e1..b74f447ffa 100644 --- a/src/Reflection/SignatureMap/SignatureMapParser.php +++ b/src/Reflection/SignatureMap/SignatureMapParser.php @@ -10,7 +10,7 @@ use PHPStan\Type\MixedType; use PHPStan\Type\Type; use function array_slice; -use function strpos; +use function str_starts_with; use function substr; class SignatureMapParser @@ -72,6 +72,7 @@ private function getParameters(array $parameterMap): array $passedByReference, $isVariadic, null, + null, ); } @@ -94,13 +95,13 @@ private function getParameterInfoFromName(string $parameterNameString): array $isVariadic = $matches['variadic'] !== ''; $reference = $matches['reference']; - if (strpos($reference, '&...') === 0) { + if (str_starts_with($reference, '&...')) { $reference = '&' . substr($reference, 4); $isVariadic = true; } - if (strpos($reference, '&rw') === 0) { + if (str_starts_with($reference, '&rw')) { $passedByReference = PassedByReference::createReadsArgument(); - } elseif (strpos($reference, '&w') === 0 || strpos($reference, '&') === 0) { + } elseif (str_starts_with($reference, '&')) { $passedByReference = PassedByReference::createCreatesNewVariable(); } else { $passedByReference = PassedByReference::createNo(); diff --git a/src/Reflection/SignatureMap/SignatureMapProvider.php b/src/Reflection/SignatureMap/SignatureMapProvider.php index 30a7933bac..f7ec5ed5ce 100644 --- a/src/Reflection/SignatureMap/SignatureMapProvider.php +++ b/src/Reflection/SignatureMap/SignatureMapProvider.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection\SignatureMap; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; +use PHPStan\Type\Type; use ReflectionFunctionAbstract; interface SignatureMapProvider @@ -12,10 +13,10 @@ public function hasMethodSignature(string $className, string $methodName): bool; public function hasFunctionSignature(string $name): bool; - /** @return array */ + /** @return array{positional: array, named: ?array} */ public function getMethodSignatures(string $className, string $methodName, ?ReflectionMethod $reflectionMethod): array; - /** @return array */ + /** @return array{positional: array, named: ?array} */ public function getFunctionSignatures(string $functionName, ?string $className, ?ReflectionFunctionAbstract $reflectionFunction): array; public function hasMethodMetadata(string $className, string $methodName): bool; @@ -32,4 +33,11 @@ public function getMethodMetadata(string $className, string $methodName): array; */ public function getFunctionMetadata(string $functionName): array; + public function hasClassConstantMetadata(string $className, string $constantName): bool; + + /** + * @return array{nativeType: Type} + */ + public function getClassConstantMetadata(string $className, string $constantName): array; + } diff --git a/src/Reflection/TrivialParametersAcceptor.php b/src/Reflection/TrivialParametersAcceptor.php index 9c773dfc77..3f663f7909 100644 --- a/src/Reflection/TrivialParametersAcceptor.php +++ b/src/Reflection/TrivialParametersAcceptor.php @@ -2,16 +2,21 @@ namespace PHPStan\Reflection; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\MixedType; use PHPStan\Type\Type; +use function sprintf; /** @api */ -class TrivialParametersAcceptor implements ParametersAcceptor +class TrivialParametersAcceptor implements ParametersAcceptorWithPhpDocs, CallableParametersAcceptor { /** @api */ - public function __construct() + public function __construct(private string $callableName = 'callable') { } @@ -25,9 +30,11 @@ public function getResolvedTemplateTypeMap(): TemplateTypeMap return TemplateTypeMap::createEmpty(); } - /** - * @return array - */ + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return TemplateTypeVarianceMap::createEmpty(); + } + public function getParameters(): array { return []; @@ -43,4 +50,45 @@ public function getReturnType(): Type return new MixedType(); } + public function getPhpDocReturnType(): Type + { + return new MixedType(); + } + + public function getNativeReturnType(): Type + { + return new MixedType(); + } + + public function getThrowPoints(): array + { + return []; + } + + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getImpurePoints(): array + { + return [ + new SimpleImpurePoint( + 'functionCall', + sprintf('call to a %s', $this->callableName), + false, + ), + ]; + } + + public function getInvalidateExpressions(): array + { + return []; + } + + public function getUsedVariables(): array + { + return []; + } + } diff --git a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php index 51e964c685..a6f23841f3 100644 --- a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php @@ -4,11 +4,11 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Dummy\ChangedTypeMethodReflection; -use PHPStan\Reflection\FunctionVariant; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParameterReflection; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\Php\DummyParameter; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\FunctionVariantWithPhpDocs; +use PHPStan\Reflection\ParameterReflectionWithPhpDocs; +use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; +use PHPStan\Reflection\Php\DummyParameterWithPhpDocs; use PHPStan\Reflection\ResolvedMethodReflection; use PHPStan\Type\Type; use function array_map; @@ -19,7 +19,7 @@ class CallbackUnresolvedMethodPrototypeReflection implements UnresolvedMethodPro /** @var callable(Type): Type */ private $transformStaticTypeCallback; - private ?MethodReflection $transformedMethod = null; + private ?ExtendedMethodReflection $transformedMethod = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -27,7 +27,7 @@ class CallbackUnresolvedMethodPrototypeReflection implements UnresolvedMethodPro * @param callable(Type): Type $transformStaticTypeCallback */ public function __construct( - private MethodReflection $methodReflection, + private ExtendedMethodReflection $methodReflection, private ClassReflection $resolvedDeclaringClass, private bool $resolveTemplateTypeMapToBounds, callable $transformStaticTypeCallback, @@ -50,21 +50,23 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototype ); } - public function getNakedMethod(): MethodReflection + public function getNakedMethod(): ExtendedMethodReflection { return $this->methodReflection; } - public function getTransformedMethod(): MethodReflection + public function getTransformedMethod(): ExtendedMethodReflection { if ($this->transformedMethod !== null) { return $this->transformedMethod; } $templateTypeMap = $this->resolvedDeclaringClass->getActiveTemplateTypeMap(); + $callSiteVarianceMap = $this->resolvedDeclaringClass->getCallSiteVarianceMap(); return $this->transformedMethod = new ResolvedMethodReflection( $this->transformMethodWithStaticType($this->resolvedDeclaringClass, $this->methodReflection), $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap, + $callSiteVarianceMap, ); } @@ -78,24 +80,40 @@ public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflectio ); } - private function transformMethodWithStaticType(ClassReflection $declaringClass, MethodReflection $method): MethodReflection + private function transformMethodWithStaticType(ClassReflection $declaringClass, ExtendedMethodReflection $method): ExtendedMethodReflection { - $variants = array_map(fn (ParametersAcceptor $acceptor): ParametersAcceptor => new FunctionVariant( + $variantFn = fn (ParametersAcceptorWithPhpDocs $acceptor): ParametersAcceptorWithPhpDocs => new FunctionVariantWithPhpDocs( $acceptor->getTemplateTypeMap(), $acceptor->getResolvedTemplateTypeMap(), - array_map(fn (ParameterReflection $parameter): ParameterReflection => new DummyParameter( - $parameter->getName(), - $this->transformStaticType($parameter->getType()), - $parameter->isOptional(), - $parameter->passedByReference(), - $parameter->isVariadic(), - $parameter->getDefaultValue(), - ), $acceptor->getParameters()), + array_map( + fn (ParameterReflectionWithPhpDocs $parameter): ParameterReflectionWithPhpDocs => new DummyParameterWithPhpDocs( + $parameter->getName(), + $this->transformStaticType($parameter->getType()), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + $parameter->getNativeType(), + $this->transformStaticType($parameter->getPhpDocType()), + $parameter->getOutType() !== null ? $this->transformStaticType($parameter->getOutType()) : null, + $parameter->isImmediatelyInvokedCallable(), + $parameter->getClosureThisType() !== null ? $this->transformStaticType($parameter->getClosureThisType()) : null, + ), + $acceptor->getParameters(), + ), $acceptor->isVariadic(), $this->transformStaticType($acceptor->getReturnType()), - ), $method->getVariants()); + $this->transformStaticType($acceptor->getPhpDocReturnType()), + $this->transformStaticType($acceptor->getNativeReturnType()), + $acceptor->getCallSiteVarianceMap(), + ); + $variants = array_map($variantFn, $method->getVariants()); + $namedArgumentVariants = $method->getNamedArgumentsVariants(); + $namedArgumentVariants = $namedArgumentVariants !== null + ? array_map($variantFn, $namedArgumentVariants) + : null; - return new ChangedTypeMethodReflection($declaringClass, $method, $variants); + return new ChangedTypeMethodReflection($declaringClass, $method, $variants, $namedArgumentVariants); } private function transformStaticType(Type $type): Type diff --git a/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php index ab53a24f99..5140d7296b 100644 --- a/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php @@ -56,10 +56,12 @@ public function getTransformedProperty(): PropertyReflection return $this->transformedProperty; } $templateTypeMap = $this->resolvedDeclaringClass->getActiveTemplateTypeMap(); + $callSiteVarianceMap = $this->resolvedDeclaringClass->getCallSiteVarianceMap(); return $this->transformedProperty = new ResolvedPropertyReflection( $this->transformPropertyWithStaticType($this->resolvedDeclaringClass, $this->propertyReflection), $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap, + $callSiteVarianceMap, ); } diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php index dfd47cbef2..bf4421d1f7 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php @@ -4,11 +4,11 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Dummy\ChangedTypeMethodReflection; -use PHPStan\Reflection\FunctionVariant; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParameterReflection; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\Php\DummyParameter; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\FunctionVariantWithPhpDocs; +use PHPStan\Reflection\ParameterReflectionWithPhpDocs; +use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; +use PHPStan\Reflection\Php\DummyParameterWithPhpDocs; use PHPStan\Reflection\ResolvedMethodReflection; use PHPStan\Type\StaticType; use PHPStan\Type\Type; @@ -18,12 +18,12 @@ class CalledOnTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { - private ?MethodReflection $transformedMethod = null; + private ?ExtendedMethodReflection $transformedMethod = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; public function __construct( - private MethodReflection $methodReflection, + private ExtendedMethodReflection $methodReflection, private ClassReflection $resolvedDeclaringClass, private bool $resolveTemplateTypeMapToBounds, private Type $calledOnType, @@ -45,21 +45,23 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototype ); } - public function getNakedMethod(): MethodReflection + public function getNakedMethod(): ExtendedMethodReflection { return $this->methodReflection; } - public function getTransformedMethod(): MethodReflection + public function getTransformedMethod(): ExtendedMethodReflection { if ($this->transformedMethod !== null) { return $this->transformedMethod; } $templateTypeMap = $this->resolvedDeclaringClass->getActiveTemplateTypeMap(); + $callSiteVarianceMap = $this->resolvedDeclaringClass->getCallSiteVarianceMap(); return $this->transformedMethod = new ResolvedMethodReflection( $this->transformMethodWithStaticType($this->resolvedDeclaringClass, $this->methodReflection), $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap, + $callSiteVarianceMap, ); } @@ -73,24 +75,40 @@ public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflectio ); } - private function transformMethodWithStaticType(ClassReflection $declaringClass, MethodReflection $method): MethodReflection + private function transformMethodWithStaticType(ClassReflection $declaringClass, ExtendedMethodReflection $method): ExtendedMethodReflection { - $variants = array_map(fn (ParametersAcceptor $acceptor): ParametersAcceptor => new FunctionVariant( + $variantFn = fn (ParametersAcceptorWithPhpDocs $acceptor): ParametersAcceptorWithPhpDocs => new FunctionVariantWithPhpDocs( $acceptor->getTemplateTypeMap(), $acceptor->getResolvedTemplateTypeMap(), - array_map(fn (ParameterReflection $parameter): ParameterReflection => new DummyParameter( - $parameter->getName(), - $this->transformStaticType($parameter->getType()), - $parameter->isOptional(), - $parameter->passedByReference(), - $parameter->isVariadic(), - $parameter->getDefaultValue(), - ), $acceptor->getParameters()), + array_map( + fn (ParameterReflectionWithPhpDocs $parameter): ParameterReflectionWithPhpDocs => new DummyParameterWithPhpDocs( + $parameter->getName(), + $this->transformStaticType($parameter->getType()), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + $parameter->getNativeType(), + $this->transformStaticType($parameter->getPhpDocType()), + $parameter->getOutType() !== null ? $this->transformStaticType($parameter->getOutType()) : null, + $parameter->isImmediatelyInvokedCallable(), + $parameter->getClosureThisType() !== null ? $this->transformStaticType($parameter->getClosureThisType()) : null, + ), + $acceptor->getParameters(), + ), $acceptor->isVariadic(), $this->transformStaticType($acceptor->getReturnType()), - ), $method->getVariants()); + $this->transformStaticType($acceptor->getPhpDocReturnType()), + $this->transformStaticType($acceptor->getNativeReturnType()), + $acceptor->getCallSiteVarianceMap(), + ); + $variants = array_map($variantFn, $method->getVariants()); + $namedArgumentsVariants = $method->getNamedArgumentsVariants(); + $namedArgumentsVariants = $namedArgumentsVariants !== null + ? array_map($variantFn, $namedArgumentsVariants) + : null; - return new ChangedTypeMethodReflection($declaringClass, $method, $variants); + return new ChangedTypeMethodReflection($declaringClass, $method, $variants, $namedArgumentsVariants); } private function transformStaticType(Type $type): Type diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php index 13e7f5b875..e9a8b8a161 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php @@ -51,10 +51,12 @@ public function getTransformedProperty(): PropertyReflection return $this->transformedProperty; } $templateTypeMap = $this->resolvedDeclaringClass->getActiveTemplateTypeMap(); + $callSiteVarianceMap = $this->resolvedDeclaringClass->getCallSiteVarianceMap(); return $this->transformedProperty = new ResolvedPropertyReflection( $this->transformPropertyWithStaticType($this->resolvedDeclaringClass, $this->propertyReflection), $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap, + $callSiteVarianceMap, ); } diff --git a/src/Reflection/Type/IntersectionTypeMethodReflection.php b/src/Reflection/Type/IntersectionTypeMethodReflection.php index 0115f8b328..fb9a8ecdd3 100644 --- a/src/Reflection/Type/IntersectionTypeMethodReflection.php +++ b/src/Reflection/Type/IntersectionTypeMethodReflection.php @@ -2,23 +2,27 @@ namespace PHPStan\Reflection\Type; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\FunctionVariant; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\FunctionVariantWithPhpDocs; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function array_map; use function count; use function implode; +use function is_bool; -class IntersectionTypeMethodReflection implements MethodReflection +class IntersectionTypeMethodReflection implements ExtendedMethodReflection { /** - * @param MethodReflection[] $methods + * @param ExtendedMethodReflection[] $methods */ public function __construct(private string $methodName, private array $methods) { @@ -75,16 +79,26 @@ public function getPrototype(): ClassMemberReflection public function getVariants(): array { $returnType = TypeCombinator::intersect(...array_map(static fn (MethodReflection $method): Type => TypeCombinator::intersect(...array_map(static fn (ParametersAcceptor $acceptor): Type => $acceptor->getReturnType(), $method->getVariants())), $this->methods)); + $phpDocReturnType = TypeCombinator::intersect(...array_map(static fn (MethodReflection $method): Type => TypeCombinator::intersect(...array_map(static fn (ParametersAcceptor $acceptor): Type => $acceptor->getPhpDocReturnType(), $method->getVariants())), $this->methods)); + $nativeReturnType = TypeCombinator::intersect(...array_map(static fn (MethodReflection $method): Type => TypeCombinator::intersect(...array_map(static fn (ParametersAcceptor $acceptor): Type => $acceptor->getNativeReturnType(), $method->getVariants())), $this->methods)); - return array_map(static fn (ParametersAcceptor $acceptor): ParametersAcceptor => new FunctionVariant( + return array_map(static fn (ParametersAcceptorWithPhpDocs $acceptor): ParametersAcceptorWithPhpDocs => new FunctionVariantWithPhpDocs( $acceptor->getTemplateTypeMap(), $acceptor->getResolvedTemplateTypeMap(), $acceptor->getParameters(), $acceptor->isVariadic(), $returnType, + $phpDocReturnType, + $nativeReturnType, + $acceptor->getCallSiteVarianceMap(), ), $this->methods[0]->getVariants()); } + public function getNamedArgumentsVariants(): ?array + { + return null; + } + public function isDeprecated(): TrinaryLogic { return TrinaryLogic::lazyMaxMin($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->isDeprecated()); @@ -117,6 +131,11 @@ public function isFinal(): TrinaryLogic return TrinaryLogic::lazyMaxMin($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->isFinal()); } + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->isFinalByKeyword()); + } + public function isInternal(): TrinaryLogic { return TrinaryLogic::lazyMaxMin($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->isInternal()); @@ -147,9 +166,40 @@ public function hasSideEffects(): TrinaryLogic return TrinaryLogic::lazyMaxMin($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->hasSideEffects()); } + public function isPure(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->isPure()); + } + public function getDocComment(): ?string { return null; } + public function getAsserts(): Assertions + { + $assertions = Assertions::createEmpty(); + + foreach ($this->methods as $method) { + $assertions = $assertions->intersectWith($method->getAsserts()); + } + + return $assertions; + } + + public function getSelfOutType(): ?Type + { + return null; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->returnsByReference()); + } + + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => is_bool($method->isAbstract()) ? TrinaryLogic::createFromBoolean($method->isAbstract()) : $method->isAbstract()); + } + } diff --git a/src/Reflection/Type/IntersectionTypePropertyReflection.php b/src/Reflection/Type/IntersectionTypePropertyReflection.php index 001abb5b8f..e93cc59e67 100644 --- a/src/Reflection/Type/IntersectionTypePropertyReflection.php +++ b/src/Reflection/Type/IntersectionTypePropertyReflection.php @@ -28,35 +28,17 @@ public function getDeclaringClass(): ClassReflection public function isStatic(): bool { - foreach ($this->properties as $property) { - if ($property->isStatic()) { - return true; - } - } - - return false; + return $this->computeResult(static fn (PropertyReflection $property) => $property->isStatic()); } public function isPrivate(): bool { - foreach ($this->properties as $property) { - if (!$property->isPrivate()) { - return false; - } - } - - return true; + return $this->computeResult(static fn (PropertyReflection $property) => $property->isPrivate()); } public function isPublic(): bool { - foreach ($this->properties as $property) { - if ($property->isPublic()) { - return true; - } - } - - return false; + return $this->computeResult(static fn (PropertyReflection $property) => $property->isPublic()); } public function isDeprecated(): TrinaryLogic @@ -108,35 +90,30 @@ public function getWritableType(): Type public function canChangeTypeAfterAssignment(): bool { - foreach ($this->properties as $property) { - if (!$property->canChangeTypeAfterAssignment()) { - return false; - } - } - - return true; + return $this->computeResult(static fn (PropertyReflection $property) => $property->canChangeTypeAfterAssignment()); } public function isReadable(): bool { - foreach ($this->properties as $property) { - if (!$property->isReadable()) { - return false; - } - } - - return true; + return $this->computeResult(static fn (PropertyReflection $property) => $property->isReadable()); } public function isWritable(): bool { + return $this->computeResult(static fn (PropertyReflection $property) => $property->isWritable()); + } + + /** + * @param callable(PropertyReflection): bool $cb + */ + private function computeResult(callable $cb): bool + { + $result = false; foreach ($this->properties as $property) { - if (!$property->isWritable()) { - return false; - } + $result = $result || $cb($property); } - return true; + return $result; } } diff --git a/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php index be6d2d944f..5cda2321f1 100644 --- a/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\Type; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Type; use function array_map; @@ -9,7 +10,7 @@ class IntersectionTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { - private ?MethodReflection $transformedMethod = null; + private ?ExtendedMethodReflection $transformedMethod = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -32,12 +33,12 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototype return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->methodName, array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->methodPrototypes)); } - public function getNakedMethod(): MethodReflection + public function getNakedMethod(): ExtendedMethodReflection { return $this->getTransformedMethod(); } - public function getTransformedMethod(): MethodReflection + public function getTransformedMethod(): ExtendedMethodReflection { if ($this->transformedMethod !== null) { return $this->transformedMethod; diff --git a/src/Reflection/Type/UnionTypeMethodReflection.php b/src/Reflection/Type/UnionTypeMethodReflection.php index 35d4df08b6..65ca5bd674 100644 --- a/src/Reflection/Type/UnionTypeMethodReflection.php +++ b/src/Reflection/Type/UnionTypeMethodReflection.php @@ -2,8 +2,10 @@ namespace PHPStan\Reflection\Type; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\TrinaryLogic; @@ -13,12 +15,13 @@ use function array_merge; use function count; use function implode; +use function is_bool; -class UnionTypeMethodReflection implements MethodReflection +class UnionTypeMethodReflection implements ExtendedMethodReflection { /** - * @param MethodReflection[] $methods + * @param ExtendedMethodReflection[] $methods */ public function __construct(private string $methodName, private array $methods) { @@ -79,6 +82,11 @@ public function getVariants(): array return [ParametersAcceptorSelector::combineAcceptors($variants)]; } + public function getNamedArgumentsVariants(): ?array + { + return null; + } + public function isDeprecated(): TrinaryLogic { return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->isDeprecated()); @@ -111,6 +119,11 @@ public function isFinal(): TrinaryLogic return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->isFinal()); } + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->isFinalByKeyword()); + } + public function isInternal(): TrinaryLogic { return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->isInternal()); @@ -141,9 +154,34 @@ public function hasSideEffects(): TrinaryLogic return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (MethodReflection $method): TrinaryLogic => $method->hasSideEffects()); } + public function isPure(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->isPure()); + } + public function getDocComment(): ?string { return null; } + public function getAsserts(): Assertions + { + return Assertions::createEmpty(); + } + + public function getSelfOutType(): ?Type + { + return null; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->returnsByReference()); + } + + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => is_bool($method->isAbstract()) ? TrinaryLogic::createFromBoolean($method->isAbstract()) : $method->isAbstract()); + } + } diff --git a/src/Reflection/Type/UnionTypePropertyReflection.php b/src/Reflection/Type/UnionTypePropertyReflection.php index e79b7f452b..d2587839c3 100644 --- a/src/Reflection/Type/UnionTypePropertyReflection.php +++ b/src/Reflection/Type/UnionTypePropertyReflection.php @@ -28,35 +28,17 @@ public function getDeclaringClass(): ClassReflection public function isStatic(): bool { - foreach ($this->properties as $property) { - if (!$property->isStatic()) { - return false; - } - } - - return true; + return $this->computeResult(static fn (PropertyReflection $property) => $property->isStatic()); } public function isPrivate(): bool { - foreach ($this->properties as $property) { - if ($property->isPrivate()) { - return true; - } - } - - return false; + return $this->computeResult(static fn (PropertyReflection $property) => $property->isPrivate()); } public function isPublic(): bool { - foreach ($this->properties as $property) { - if (!$property->isPublic()) { - return false; - } - } - - return true; + return $this->computeResult(static fn (PropertyReflection $property) => $property->isPublic()); } public function isDeprecated(): TrinaryLogic @@ -108,35 +90,30 @@ public function getWritableType(): Type public function canChangeTypeAfterAssignment(): bool { - foreach ($this->properties as $property) { - if (!$property->canChangeTypeAfterAssignment()) { - return false; - } - } - - return true; + return $this->computeResult(static fn (PropertyReflection $property) => $property->canChangeTypeAfterAssignment()); } public function isReadable(): bool { - foreach ($this->properties as $property) { - if (!$property->isReadable()) { - return false; - } - } - - return true; + return $this->computeResult(static fn (PropertyReflection $property) => $property->isReadable()); } public function isWritable(): bool { + return $this->computeResult(static fn (PropertyReflection $property) => $property->isWritable()); + } + + /** + * @param callable(PropertyReflection): bool $cb + */ + private function computeResult(callable $cb): bool + { + $result = true; foreach ($this->properties as $property) { - if (!$property->isWritable()) { - return false; - } + $result = $result && $cb($property); } - return true; + return $result; } } diff --git a/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php index 4d8cf48a19..9f0e62bc0c 100644 --- a/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\Type; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Type; use function array_map; @@ -9,7 +10,7 @@ class UnionTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { - private ?MethodReflection $transformedMethod = null; + private ?ExtendedMethodReflection $transformedMethod = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -32,12 +33,12 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototype return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->methodName, array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->methodPrototypes)); } - public function getNakedMethod(): MethodReflection + public function getNakedMethod(): ExtendedMethodReflection { return $this->getTransformedMethod(); } - public function getTransformedMethod(): MethodReflection + public function getTransformedMethod(): ExtendedMethodReflection { if ($this->transformedMethod !== null) { return $this->transformedMethod; diff --git a/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php index 746992aadc..fe47fa1452 100644 --- a/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php @@ -9,8 +9,6 @@ class UnionTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection { - private string $propertyName; - private ?PropertyReflection $transformedProperty = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -19,11 +17,10 @@ class UnionTypeUnresolvedPropertyPrototypeReflection implements UnresolvedProper * @param UnresolvedPropertyPrototypeReflection[] $propertyPrototypes */ public function __construct( - string $methodName, + private string $propertyName, private array $propertyPrototypes, ) { - $this->propertyName = $methodName; } public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototypeReflection diff --git a/src/Reflection/Type/UnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/UnresolvedMethodPrototypeReflection.php index 27f36dfaa3..4665670cb4 100644 --- a/src/Reflection/Type/UnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/UnresolvedMethodPrototypeReflection.php @@ -2,7 +2,7 @@ namespace PHPStan\Reflection\Type; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Type\Type; interface UnresolvedMethodPrototypeReflection @@ -10,9 +10,9 @@ interface UnresolvedMethodPrototypeReflection public function doNotResolveTemplateTypeMapToBounds(): self; - public function getNakedMethod(): MethodReflection; + public function getNakedMethod(): ExtendedMethodReflection; - public function getTransformedMethod(): MethodReflection; + public function getTransformedMethod(): ExtendedMethodReflection; public function withCalledOnType(Type $type): self; diff --git a/src/Reflection/WrappedExtendedMethodReflection.php b/src/Reflection/WrappedExtendedMethodReflection.php index dd8b93347f..c565601261 100644 --- a/src/Reflection/WrappedExtendedMethodReflection.php +++ b/src/Reflection/WrappedExtendedMethodReflection.php @@ -2,8 +2,12 @@ namespace PHPStan\Reflection; +use PHPStan\Reflection\Php\DummyParameterWithPhpDocs; use PHPStan\TrinaryLogic; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; +use function array_map; class WrappedExtendedMethodReflection implements ExtendedMethodReflection { @@ -49,7 +53,43 @@ public function getPrototype(): ClassMemberReflection public function getVariants(): array { - return $this->method->getVariants(); + $variants = []; + foreach ($this->method->getVariants() as $variant) { + if ($variant instanceof ParametersAcceptorWithPhpDocs) { + $variants[] = $variant; + continue; + } + + $variants[] = new FunctionVariantWithPhpDocs( + $variant->getTemplateTypeMap(), + $variant->getResolvedTemplateTypeMap(), + array_map(static fn (ParameterReflection $parameter): ParameterReflectionWithPhpDocs => $parameter instanceof ParameterReflectionWithPhpDocs ? $parameter : new DummyParameterWithPhpDocs( + $parameter->getName(), + $parameter->getType(), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + new MixedType(), + $parameter->getType(), + null, + TrinaryLogic::createMaybe(), + null, + ), $variant->getParameters()), + $variant->isVariadic(), + $variant->getReturnType(), + $variant->getReturnType(), + new MixedType(), + TemplateTypeVarianceMap::createEmpty(), + ); + } + + return $variants; + } + + public function getNamedArgumentsVariants(): ?array + { + return null; } public function isDeprecated(): TrinaryLogic @@ -67,6 +107,11 @@ public function isFinal(): TrinaryLogic return $this->method->isFinal(); } + public function isFinalByKeyword(): TrinaryLogic + { + return $this->isFinal(); + } + public function isInternal(): TrinaryLogic { return $this->method->isInternal(); @@ -82,4 +127,29 @@ public function hasSideEffects(): TrinaryLogic return $this->method->hasSideEffects(); } + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getAsserts(): Assertions + { + return Assertions::createEmpty(); + } + + public function getSelfOutType(): ?Type + { + return null; + } + + public function returnsByReference(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + } diff --git a/src/Rules/Api/ApiClassConstFetchRule.php b/src/Rules/Api/ApiClassConstFetchRule.php index 9525c72c95..528d2daf8b 100644 --- a/src/Rules/Api/ApiClassConstFetchRule.php +++ b/src/Rules/Api/ApiClassConstFetchRule.php @@ -9,7 +9,7 @@ use PHPStan\Rules\RuleErrorBuilder; use function count; use function sprintf; -use function strpos; +use function str_contains; /** * @implements Rule @@ -53,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array 'Accessing %s::%s is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', $classReflection->getDisplayName(), $node->name->toString(), - ))->tip(sprintf( + ))->identifier('phpstanApi.classConstant')->tip(sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", 'https://github.com/phpstan/phpstan/discussions', ))->build(); @@ -75,7 +75,7 @@ public function processNode(Node $node, Scope $scope): array continue; } - if (strpos($methodDocComment, '@api') === false) { + if (!str_contains($methodDocComment, '@api')) { continue; } diff --git a/src/Rules/Api/ApiClassExtendsRule.php b/src/Rules/Api/ApiClassExtendsRule.php index 1899358e2c..72aad19740 100644 --- a/src/Rules/Api/ApiClassExtendsRule.php +++ b/src/Rules/Api/ApiClassExtendsRule.php @@ -53,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array $ruleError = RuleErrorBuilder::message(sprintf( 'Extending %s is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', $extendedClassReflection->getDisplayName(), - ))->tip(sprintf( + ))->identifier('phpstanApi.class')->tip(sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", 'https://github.com/phpstan/phpstan/discussions', ))->build(); diff --git a/src/Rules/Api/ApiClassImplementsRule.php b/src/Rules/Api/ApiClassImplementsRule.php index 805d9fca1f..521279beca 100644 --- a/src/Rules/Api/ApiClassImplementsRule.php +++ b/src/Rules/Api/ApiClassImplementsRule.php @@ -6,10 +6,9 @@ use PhpParser\Node\Stmt\Class_; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\Type; use function array_merge; use function count; use function in_array; @@ -44,7 +43,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ private function checkName(Scope $scope, Node\Name $name): array { @@ -61,15 +60,12 @@ private function checkName(Scope $scope, Node\Name $name): array $ruleError = RuleErrorBuilder::message(sprintf( 'Implementing %s is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', $implementedClassReflection->getDisplayName(), - ))->tip(sprintf( + ))->identifier('phpstanApi.interface')->tip(sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", 'https://github.com/phpstan/phpstan/discussions', ))->build(); - if (in_array($implementedClassReflection->getName(), [ - Type::class, - ReflectionProvider::class, - ], true)) { + if (in_array($implementedClassReflection->getName(), BcUncoveredInterface::CLASSES, true)) { return [$ruleError]; } diff --git a/src/Rules/Api/ApiInstanceofRule.php b/src/Rules/Api/ApiInstanceofRule.php index 5b7ce6351f..5f2c4d531a 100644 --- a/src/Rules/Api/ApiInstanceofRule.php +++ b/src/Rules/Api/ApiInstanceofRule.php @@ -6,15 +6,15 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; use function count; use function sprintf; +use function strtolower; /** * @implements Rule @@ -53,11 +53,13 @@ public function processNode(Node $node, Scope $scope): array $ruleError = RuleErrorBuilder::message(sprintf( 'Asking about instanceof %s is not covered by backward compatibility promise. The %s might change in a minor PHPStan version.', $classReflection->getDisplayName(), - $classReflection->isInterface() ? 'interface' : 'class', - ))->tip(sprintf( - "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions', - ))->build(); + strtolower($classReflection->getClassTypeDescription()), + )) + ->identifier(sprintf('phpstanApi.%s', strtolower($classReflection->getClassTypeDescription()))) + ->tip(sprintf( + "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", + 'https://github.com/phpstan/phpstan/discussions', + ))->build(); $docBlock = $classReflection->getResolvedPhpDoc(); if ($docBlock === null) { @@ -75,7 +77,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ private function processCoveredClass(Node\Expr\Instanceof_ $node, Scope $scope, ClassReflection $classReflection): array { @@ -84,14 +86,16 @@ private function processCoveredClass(Node\Expr\Instanceof_ $node, Scope $scope, } $instanceofType = $scope->getType($node); - if ($instanceofType instanceof ConstantBooleanType) { + if ($instanceofType->isTrue()->or($instanceofType->isFalse())->yes()) { return []; } + $classType = new ObjectType($classReflection->getName(), null, $classReflection); + $exprType = $scope->getType($node->expr); if ($exprType instanceof UnionType) { foreach ($exprType->getTypes() as $innerType) { - if ($innerType instanceof TypeWithClassName && $innerType->getClassName() === $classReflection->getName()) { + if ($innerType->getObjectClassNames() !== [] && $classType->isSuperTypeOf($innerType)->yes()) { return []; } } @@ -101,7 +105,7 @@ private function processCoveredClass(Node\Expr\Instanceof_ $node, Scope $scope, RuleErrorBuilder::message(sprintf( 'Although %s is covered by backward compatibility promise, this instanceof assumption might break because it\'s not guaranteed to always stay the same.', $classReflection->getDisplayName(), - ))->tip(sprintf( + ))->identifier('phpstanApi.instanceofAssumption')->tip(sprintf( "In case of questions how to solve this correctly, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", 'https://github.com/phpstan/phpstan/discussions', ))->build(), diff --git a/src/Rules/Api/ApiInstanceofTypeRule.php b/src/Rules/Api/ApiInstanceofTypeRule.php new file mode 100644 index 0000000000..fd4a3c6e20 --- /dev/null +++ b/src/Rules/Api/ApiInstanceofTypeRule.php @@ -0,0 +1,161 @@ + + */ +class ApiInstanceofTypeRule implements Rule +{ + + private const MAP = [ + TypeWithClassName::class => 'Type::getObjectClassNames() or Type::getObjectClassReflections()', + EnumCaseObjectType::class => 'Type::getEnumCases()', + ConstantArrayType::class => 'Type::getConstantArrays()', + ArrayType::class => 'Type::isArray() or Type::getArrays()', + ConstantStringType::class => 'Type::getConstantStrings()', + StringType::class => 'Type::isString()', + ClassStringType::class => 'Type::isClassStringType()', + IntegerType::class => 'Type::isInteger()', + FloatType::class => 'Type::isFloat()', + NullType::class => 'Type::isNull()', + VoidType::class => 'Type::isVoid()', + BooleanType::class => 'Type::isBoolean()', + ConstantBooleanType::class => 'Type::isTrue() or Type::isFalse()', + CallableType::class => 'Type::isCallable() and Type::getCallableParametersAcceptors()', + IterableType::class => 'Type::isIterable()', + ObjectWithoutClassType::class => 'Type::isObject()', + ObjectType::class => 'Type::isObject() or Type::getObjectClassNames()', + GenericClassStringType::class => 'Type::isClassStringType() and Type::getClassStringObjectType()', + GenericObjectType::class => null, + IntersectionType::class => null, + ConstantType::class => 'Type::isConstantValue() or Type::generalize()', + ConstantScalarType::class => 'Type::isConstantScalarValue() or Type::getConstantScalarTypes() or Type::getConstantScalarValues()', + ObjectShapeType::class => 'Type::isObject() and Type::hasProperty()', + + // accessory types + NonEmptyArrayType::class => 'Type::isIterableAtLeastOnce()', + OversizedArrayType::class => 'Type::isOversizedArray()', + AccessoryArrayListType::class => 'Type::isList()', + AccessoryNumericStringType::class => 'Type::isNumericString()', + AccessoryLiteralStringType::class => 'Type::isLiteralString()', + AccessoryNonEmptyStringType::class => 'Type::isNonEmptyString()', + AccessoryNonFalsyStringType::class => 'Type::isNonFalsyString()', + HasMethodType::class => 'Type::hasMethod()', + HasPropertyType::class => 'Type::hasProperty()', + HasOffsetType::class => 'Type::hasOffsetValueType()', + AccessoryType::class => 'methods on PHPStan\\Type\\Type', + ]; + + public function __construct( + private ReflectionProvider $reflectionProvider, + private bool $enabled, + private bool $deprecationRulesInstalled, + ) + { + } + + public function getNodeType(): string + { + return Instanceof_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$this->enabled && !$this->deprecationRulesInstalled) { + return []; + } + + if (!$node->class instanceof Node\Name) { + return []; + } + + if ($node->getAttribute(TypeTraverserInstanceofVisitor::ATTRIBUTE_NAME, false) === true) { + return []; + } + + $lowerMap = []; + foreach (self::MAP as $className => $method) { + $lowerMap[strtolower($className)] = $method; + } + + $className = $scope->resolveName($node->class); + $lowerClassName = strtolower($className); + if (!array_key_exists($lowerClassName, $lowerMap)) { + return []; + } + + if ($this->reflectionProvider->hasClass($className)) { + $classReflection = $this->reflectionProvider->getClass($className); + if ($classReflection->isSubclassOf(AccessoryType::class)) { + if ($className === $classReflection->getName()) { + return []; + } + } + } + + $tip = 'Learn more: https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated'; + if ($lowerMap[$lowerClassName] === null) { + return [ + RuleErrorBuilder::message(sprintf( + 'Doing instanceof %s is error-prone and deprecated.', + $className, + ))->identifier('phpstanApi.instanceofType')->tip($tip)->build(), + ]; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Doing instanceof %s is error-prone and deprecated. Use %s instead.', + $className, + $lowerMap[$lowerClassName], + ))->identifier('phpstanApi.instanceofType')->tip($tip)->build(), + ]; + } + +} diff --git a/src/Rules/Api/ApiInstantiationRule.php b/src/Rules/Api/ApiInstantiationRule.php index 9c4cc7e5bb..bea7e29b68 100644 --- a/src/Rules/Api/ApiInstantiationRule.php +++ b/src/Rules/Api/ApiInstantiationRule.php @@ -8,7 +8,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; -use function strpos; +use function str_contains; /** * @implements Rule @@ -47,7 +47,7 @@ public function processNode(Node $node, Scope $scope): array $ruleError = RuleErrorBuilder::message(sprintf( 'Creating new %s is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', $classReflection->getDisplayName(), - ))->tip(sprintf( + ))->identifier('phpstanApi.constructor')->tip(sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", 'https://github.com/phpstan/phpstan/discussions', ))->build(); @@ -62,7 +62,7 @@ public function processNode(Node $node, Scope $scope): array return [$ruleError]; } - if (strpos($docComment, '@api') === false) { + if (!str_contains($docComment, '@api')) { return [$ruleError]; } diff --git a/src/Rules/Api/ApiInterfaceExtendsRule.php b/src/Rules/Api/ApiInterfaceExtendsRule.php index d409b4ff72..fc5d8ca505 100644 --- a/src/Rules/Api/ApiInterfaceExtendsRule.php +++ b/src/Rules/Api/ApiInterfaceExtendsRule.php @@ -6,10 +6,9 @@ use PhpParser\Node\Stmt\Interface_; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\Type; use function array_merge; use function count; use function in_array; @@ -44,7 +43,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ private function checkName(Scope $scope, Node\Name $name): array { @@ -61,15 +60,12 @@ private function checkName(Scope $scope, Node\Name $name): array $ruleError = RuleErrorBuilder::message(sprintf( 'Extending %s is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', $extendedInterfaceReflection->getDisplayName(), - ))->tip(sprintf( + ))->identifier('phpstanApi.interface')->tip(sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", 'https://github.com/phpstan/phpstan/discussions', ))->build(); - if (in_array($extendedInterfaceReflection->getName(), [ - Type::class, - ReflectionProvider::class, - ], true)) { + if (in_array($extendedInterfaceReflection->getName(), BcUncoveredInterface::CLASSES, true)) { return [$ruleError]; } diff --git a/src/Rules/Api/ApiMethodCallRule.php b/src/Rules/Api/ApiMethodCallRule.php index 540f834f00..52472fb47e 100644 --- a/src/Rules/Api/ApiMethodCallRule.php +++ b/src/Rules/Api/ApiMethodCallRule.php @@ -9,7 +9,7 @@ use PHPStan\Rules\RuleErrorBuilder; use function count; use function sprintf; -use function strpos; +use function str_contains; /** * @implements Rule @@ -50,7 +50,7 @@ public function processNode(Node $node, Scope $scope): array 'Calling %s::%s() is not covered by backward compatibility promise. The method might change in a minor PHPStan version.', $declaringClass->getDisplayName(), $methodReflection->getName(), - ))->tip(sprintf( + ))->identifier('phpstanApi.method')->tip(sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", 'https://github.com/phpstan/phpstan/discussions', ))->build(); @@ -76,7 +76,7 @@ private function isCovered(MethodReflection $methodReflection): bool return false; } - return strpos($methodDocComment, '@api') !== false; + return str_contains($methodDocComment, '@api'); } } diff --git a/src/Rules/Api/ApiRuleHelper.php b/src/Rules/Api/ApiRuleHelper.php index 59123647d8..021353bbd0 100644 --- a/src/Rules/Api/ApiRuleHelper.php +++ b/src/Rules/Api/ApiRuleHelper.php @@ -6,8 +6,8 @@ use PHPStan\File\ParentDirectoryRelativePathHelper; use function dirname; use function pathinfo; +use function str_starts_with; use function stripos; -use function strpos; use function strtolower; use const PATHINFO_BASENAME; @@ -72,11 +72,11 @@ public function isPhpStanName(string $namespace): bool return true; } - if (strpos($namespace, 'PHPStan\\PhpDocParser\\') === 0) { + if (str_starts_with($namespace, 'PHPStan\\PhpDocParser\\')) { return false; } - if (strpos($namespace, 'PHPStan\\BetterReflection\\') === 0) { + if (str_starts_with($namespace, 'PHPStan\\BetterReflection\\')) { return false; } diff --git a/src/Rules/Api/ApiStaticCallRule.php b/src/Rules/Api/ApiStaticCallRule.php index cda4c89a27..6d48c393c0 100644 --- a/src/Rules/Api/ApiStaticCallRule.php +++ b/src/Rules/Api/ApiStaticCallRule.php @@ -10,7 +10,7 @@ use PHPStan\Rules\RuleErrorBuilder; use function count; use function sprintf; -use function strpos; +use function str_contains; /** * @implements Rule @@ -65,7 +65,7 @@ public function processNode(Node $node, Scope $scope): array 'Calling %s::%s() is not covered by backward compatibility promise. The method might change in a minor PHPStan version.', $declaringClass->getDisplayName(), $methodReflection->getName(), - ))->tip(sprintf( + ))->identifier('phpstanApi.method')->tip(sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", 'https://github.com/phpstan/phpstan/discussions', ))->build(); @@ -91,7 +91,7 @@ private function isCovered(MethodReflection $methodReflection): bool return false; } - return strpos($methodDocComment, '@api') !== false; + return str_contains($methodDocComment, '@api'); } } diff --git a/src/Rules/Api/ApiTraitUseRule.php b/src/Rules/Api/ApiTraitUseRule.php index 0367161235..33da77eb36 100644 --- a/src/Rules/Api/ApiTraitUseRule.php +++ b/src/Rules/Api/ApiTraitUseRule.php @@ -48,7 +48,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Using %s is not covered by backward compatibility promise. The trait might change in a minor PHPStan version.', $traitReflection->getDisplayName(), - ))->tip($tip)->build(); + ))->identifier('phpstanApi.trait')->tip($tip)->build(); } return $errors; diff --git a/src/Rules/Api/BcUncoveredInterface.php b/src/Rules/Api/BcUncoveredInterface.php new file mode 100644 index 0000000000..9a78dc5cc7 --- /dev/null +++ b/src/Rules/Api/BcUncoveredInterface.php @@ -0,0 +1,42 @@ + + */ +class GetTemplateTypeRule implements Rule +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $args = $node->getArgs(); + if (count($args) < 2) { + return []; + } + if (!$node->name instanceof Node\Identifier) { + return []; + } + + if ($node->name->toLowerString() !== 'gettemplatetype') { + return []; + } + + $calledOnType = $scope->getType($node->var); + $methodReflection = $scope->getMethodReflection($calledOnType, $node->name->toString()); + if ($methodReflection === null) { + return []; + } + + if (!$methodReflection->getDeclaringClass()->is(Type::class)) { + return []; + } + + $classType = $scope->getType($args[0]->value); + $templateType = $scope->getType($args[1]->value); + $errors = []; + foreach ($classType->getConstantStrings() as $classNameType) { + if (!$this->reflectionProvider->hasClass($classNameType->getValue())) { + continue; + } + $classReflection = $this->reflectionProvider->getClass($classNameType->getValue()); + $templateTypeMap = $classReflection->getTemplateTypeMap(); + foreach ($templateType->getConstantStrings() as $templateTypeName) { + if ($templateTypeMap->hasType($templateTypeName->getValue())) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to %s::%s() references unknown template type %s on class %s.', + $methodReflection->getDeclaringClass()->getDisplayName(), + $methodReflection->getName(), + $templateTypeName->getValue(), + $classReflection->getDisplayName(), + ))->identifier('phpstanApi.getTemplateType')->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Api/NodeConnectingVisitorAttributesRule.php b/src/Rules/Api/NodeConnectingVisitorAttributesRule.php index 3db9885335..0907413ca7 100644 --- a/src/Rules/Api/NodeConnectingVisitorAttributesRule.php +++ b/src/Rules/Api/NodeConnectingVisitorAttributesRule.php @@ -16,7 +16,7 @@ use function get_class; use function in_array; use function sprintf; -use function strpos; +use function str_starts_with; /** * @implements Rule @@ -63,7 +63,7 @@ public function processNode(Node $node, Scope $scope): array $classReflection = $scope->getClassReflection(); $hasPhpStanInterface = false; foreach (array_keys($classReflection->getInterfaces()) as $interfaceName) { - if (strpos($interfaceName, 'PHPStan\\') !== 0) { + if (!str_starts_with($interfaceName, 'PHPStan\\')) { continue; } @@ -90,6 +90,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf('Node attribute \'%s\' is no longer available.', $argType->getValue())) + ->identifier('phpParser.nodeConnectingAttribute') ->tip('See: https://phpstan.org/blog/preprocessing-ast-for-custom-rules') ->build(), ]; diff --git a/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php b/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php index 892bbc1660..cc449b8f30 100644 --- a/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php +++ b/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php @@ -13,7 +13,7 @@ use function dirname; use function is_dir; use function is_file; -use function strpos; +use function str_starts_with; /** * @implements Rule @@ -46,12 +46,13 @@ public function processNode(Node $node, Scope $scope): array } $packageName = $composerJson['name'] ?? null; - if ($packageName !== null && strpos($packageName, 'phpstan/') === 0) { + if ($packageName !== null && str_starts_with($packageName, 'phpstan/')) { return []; } return [ RuleErrorBuilder::message('Declaring PHPStan namespace is not allowed in 3rd party packages.') + ->identifier('phpstanApi.phpstanNamespace') ->tip("See:\n https://phpstan.org/developing-extensions/backward-compatibility-promise") ->build(), ]; diff --git a/src/Rules/Api/RuntimeReflectionFunctionRule.php b/src/Rules/Api/RuntimeReflectionFunctionRule.php index d71a62d1eb..8b8fbf6722 100644 --- a/src/Rules/Api/RuntimeReflectionFunctionRule.php +++ b/src/Rules/Api/RuntimeReflectionFunctionRule.php @@ -10,7 +10,7 @@ use function array_keys; use function in_array; use function sprintf; -use function strpos; +use function str_starts_with; /** * @implements Rule @@ -55,7 +55,7 @@ public function processNode(Node $node, Scope $scope): array $classReflection = $scope->getClassReflection(); $hasPhpStanInterface = false; foreach (array_keys($classReflection->getInterfaces()) as $interfaceName) { - if (strpos($interfaceName, 'PHPStan\\') !== 0) { + if (!str_starts_with($interfaceName, 'PHPStan\\')) { continue; } @@ -69,7 +69,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( sprintf('Function %s() is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', $functionReflection->getName()), - )->build(), + )->identifier('phpstanApi.runtimeReflection')->build(), ]; } diff --git a/src/Rules/Api/RuntimeReflectionInstantiationRule.php b/src/Rules/Api/RuntimeReflectionInstantiationRule.php index 24a7579af2..3e5dc7524c 100644 --- a/src/Rules/Api/RuntimeReflectionInstantiationRule.php +++ b/src/Rules/Api/RuntimeReflectionInstantiationRule.php @@ -20,7 +20,7 @@ use function array_keys; use function in_array; use function sprintf; -use function strpos; +use function str_starts_with; /** * @implements Rule @@ -74,7 +74,7 @@ public function processNode(Node $node, Scope $scope): array $scopeClassReflection = $scope->getClassReflection(); $hasPhpStanInterface = false; foreach (array_keys($scopeClassReflection->getInterfaces()) as $interfaceName) { - if (strpos($interfaceName, 'PHPStan\\') !== 0) { + if (!str_starts_with($interfaceName, 'PHPStan\\')) { continue; } @@ -88,7 +88,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( sprintf('Creating new %s is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.', $classReflection->getName()), - )->build(), + )->identifier('phpstanApi.runtimeReflection')->build(), ]; } diff --git a/src/Rules/Arrays/AppendedArrayItemTypeRule.php b/src/Rules/Arrays/AppendedArrayItemTypeRule.php index 396de12949..96409f9aa1 100644 --- a/src/Rules/Arrays/AppendedArrayItemTypeRule.php +++ b/src/Rules/Arrays/AppendedArrayItemTypeRule.php @@ -12,7 +12,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; -use PHPStan\Type\ArrayType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -62,7 +61,7 @@ public function processNode(Node $node, Scope $scope): array } $assignedToType = $propertyReflection->getWritableType(); - if (!($assignedToType instanceof ArrayType)) { + if (!$assignedToType->isArray()->yes()) { return []; } @@ -72,15 +71,19 @@ public function processNode(Node $node, Scope $scope): array $assignedValueType = $scope->getType($node); } - $itemType = $assignedToType->getItemType(); - if (!$this->ruleLevelHelper->accepts($itemType, $assignedValueType, $scope->isDeclareStrictTypes())) { + $itemType = $assignedToType->getIterableValueType(); + $accepts = $this->ruleLevelHelper->acceptsWithReason($itemType, $assignedValueType, $scope->isDeclareStrictTypes()); + if (!$accepts->result) { $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($itemType, $assignedValueType); return [ RuleErrorBuilder::message(sprintf( 'Array (%s) does not accept %s.', $assignedToType->describe($verbosityLevel), $assignedValueType->describe($verbosityLevel), - ))->build(), + )) + ->acceptsReasonsTip($accepts->reasons) + ->identifier('array.valueType') + ->build(), ]; } diff --git a/src/Rules/Arrays/AppendedArrayKeyTypeRule.php b/src/Rules/Arrays/AppendedArrayKeyTypeRule.php index aa45ce31b4..cf35062faf 100644 --- a/src/Rules/Arrays/AppendedArrayKeyTypeRule.php +++ b/src/Rules/Arrays/AppendedArrayKeyTypeRule.php @@ -9,7 +9,6 @@ use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ArrayType; use PHPStan\Type\IntegerType; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -53,7 +52,7 @@ public function processNode(Node $node, Scope $scope): array } $arrayType = $propertyReflection->getReadableType(); - if (!$arrayType instanceof ArrayType) { + if (!$arrayType->isArray()->yes()) { return []; } @@ -65,7 +64,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $keyType = ArrayType::castToArrayKeyType($dimensionType); + $keyType = $dimensionType->toArrayKey(); if (!$this->checkUnionTypes && $keyType instanceof UnionType) { return []; } @@ -80,7 +79,7 @@ public function processNode(Node $node, Scope $scope): array 'Array (%s) does not accept key %s.', $arrayType->describe($verbosity), $keyType->describe(VerbosityLevel::value()), - ))->build(), + ))->identifier('array.keyType')->build(), ]; } diff --git a/src/Rules/Arrays/ArrayDestructuringRule.php b/src/Rules/Arrays/ArrayDestructuringRule.php index 37f63d4d7c..332d192698 100644 --- a/src/Rules/Arrays/ArrayDestructuringRule.php +++ b/src/Rules/Arrays/ArrayDestructuringRule.php @@ -6,14 +6,13 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Assign; -use PhpParser\Node\Scalar\LNumber; use PHPStan\Analyser\Scope; +use PHPStan\Node\Expr\TypeExpr; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\Constant\ConstantIntegerType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; @@ -54,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array /** * @param Node\Expr\List_|Node\Expr\Array_ $var - * @return RuleError[] + * @return list */ private function getErrors(Scope $scope, Expr $var, Expr $expr): array { @@ -70,7 +69,9 @@ private function getErrors(Scope $scope, Expr $var, Expr $expr): array } if (!$exprType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($exprType)->yes()) { return [ - RuleErrorBuilder::message(sprintf('Cannot use array destructuring on %s.', $exprType->describe(VerbosityLevel::typeOnly())))->build(), + RuleErrorBuilder::message(sprintf('Cannot use array destructuring on %s.', $exprType->describe(VerbosityLevel::typeOnly()))) + ->identifier('offsetAccess.nonArray') + ->build(), ]; } @@ -88,11 +89,7 @@ private function getErrors(Scope $scope, Expr $var, Expr $expr): array $keyExpr = new Node\Scalar\LNumber($i); } else { $keyType = $scope->getType($item->key); - if ($keyType instanceof ConstantIntegerType) { - $keyExpr = new LNumber($keyType->getValue()); - } elseif ($keyType instanceof ConstantStringType) { - $keyExpr = new Node\Scalar\String_($keyType->getValue()); - } + $keyExpr = new TypeExpr($keyType); } $itemErrors = $this->nonexistentOffsetInArrayDimFetchCheck->check( @@ -103,11 +100,6 @@ private function getErrors(Scope $scope, Expr $var, Expr $expr): array ); $errors = array_merge($errors, $itemErrors); - if ($keyExpr === null) { - $i++; - continue; - } - if (!$item->value instanceof Node\Expr\List_ && !$item->value instanceof Node\Expr\Array_) { $i++; continue; diff --git a/src/Rules/Arrays/ArrayUnpackingRule.php b/src/Rules/Arrays/ArrayUnpackingRule.php index 7425021b72..21092c2100 100644 --- a/src/Rules/Arrays/ArrayUnpackingRule.php +++ b/src/Rules/Arrays/ArrayUnpackingRule.php @@ -58,7 +58,7 @@ public function processNode(Node $node, Scope $scope): array 'Array unpacking cannot be used on an array with %sstring keys: %s', $isString->yes() ? '' : 'potential ', $scope->getType($node->value)->describe(VerbosityLevel::value()), - ))->build(), + ))->identifier('arrayUnpacking.stringOffset')->build(), ]; } diff --git a/src/Rules/Arrays/DeadForeachRule.php b/src/Rules/Arrays/DeadForeachRule.php index 0423c78fc5..a9b64b441f 100644 --- a/src/Rules/Arrays/DeadForeachRule.php +++ b/src/Rules/Arrays/DeadForeachRule.php @@ -30,7 +30,9 @@ public function processNode(Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message('Empty array passed to foreach.')->build(), + RuleErrorBuilder::message('Empty array passed to foreach.') + ->identifier('foreach.emptyArray') + ->build(), ]; } diff --git a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php index 70d03ebb5c..606ef7aa20 100644 --- a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php +++ b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php @@ -60,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array $printedValues[$value][] = $printedValue; if (!isset($valueLines[$value])) { - $valueLines[$value] = $item->getLine(); + $valueLines[$value] = $item->getStartLine(); } $previousCount = count($values); @@ -80,7 +80,7 @@ public function processNode(Node $node, Scope $scope): array count($printedValues[$value]) === 1 ? 'duplicate key' : 'duplicate keys', var_export($value, true), implode(', ', $printedValues[$value]), - ))->line($valueLines[$value])->build(); + ))->identifier('array.duplicateKey')->line($valueLines[$value])->build(); } return $messages; diff --git a/src/Rules/Arrays/EmptyArrayItemRule.php b/src/Rules/Arrays/EmptyArrayItemRule.php index 9bc3a395b2..94cb7e49b0 100644 --- a/src/Rules/Arrays/EmptyArrayItemRule.php +++ b/src/Rules/Arrays/EmptyArrayItemRule.php @@ -30,6 +30,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message('Literal array contains empty item.') ->nonIgnorable() + ->identifier('array.emptyItem') ->build(), ]; } diff --git a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php index d76135fb86..1242f85cfd 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php @@ -6,10 +6,11 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; -use PHPStan\Type\TypeUtils; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; -use function count; use function sprintf; /** @@ -18,7 +19,10 @@ class InvalidKeyInArrayDimFetchRule implements Rule { - public function __construct(private bool $reportMaybes) + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + private bool $reportMaybes, + ) { } @@ -33,28 +37,32 @@ public function processNode(Node $node, Scope $scope): array return []; } - $varType = $scope->getType($node->var); - if (count(TypeUtils::getAnyArrays($varType)) === 0) { + $dimensionType = $scope->getType($node->dim); + if ($dimensionType instanceof MixedType) { + return []; + } + + $varType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->var, + '', + static fn (Type $varType): bool => $varType->isArray()->no() || AllowedArrayKeysTypes::getType()->isSuperTypeOf($dimensionType)->yes(), + )->getType(); + + if ($varType instanceof ErrorType || $varType->isArray()->no()) { return []; } - $dimensionType = $scope->getType($node->dim); $isSuperType = AllowedArrayKeysTypes::getType()->isSuperTypeOf($dimensionType); - if ($isSuperType->no()) { - return [ - RuleErrorBuilder::message( - sprintf('Invalid array key type %s.', $dimensionType->describe(VerbosityLevel::typeOnly())), - )->build(), - ]; - } elseif ($this->reportMaybes && $isSuperType->maybe() && !$dimensionType instanceof MixedType) { - return [ - RuleErrorBuilder::message( - sprintf('Possibly invalid array key type %s.', $dimensionType->describe(VerbosityLevel::typeOnly())), - )->build(), - ]; + if ($isSuperType->yes() || ($isSuperType->maybe() && !$this->reportMaybes)) { + return []; } - return []; + return [ + RuleErrorBuilder::message( + sprintf('%s array key type %s.', $isSuperType->no() ? 'Invalid' : 'Possibly invalid', $dimensionType->describe(VerbosityLevel::typeOnly())), + )->identifier('offsetAccess.invalidOffset')->build(), + ]; } } diff --git a/src/Rules/Arrays/InvalidKeyInArrayItemRule.php b/src/Rules/Arrays/InvalidKeyInArrayItemRule.php index b692bbe15a..61c4fd342c 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayItemRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayItemRule.php @@ -37,13 +37,13 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( sprintf('Invalid array key type %s.', $dimensionType->describe(VerbosityLevel::typeOnly())), - )->build(), + )->identifier('array.invalidKey')->build(), ]; } elseif ($this->reportMaybes && $isSuperType->maybe() && !$dimensionType instanceof MixedType) { return [ RuleErrorBuilder::message( sprintf('Possibly invalid array key type %s.', $dimensionType->describe(VerbosityLevel::typeOnly())), - )->build(), + )->identifier('array.invalidKey')->build(), ]; } diff --git a/src/Rules/Arrays/IterableInForeachRule.php b/src/Rules/Arrays/IterableInForeachRule.php index 57fd2080f3..c455cd8d21 100644 --- a/src/Rules/Arrays/IterableInForeachRule.php +++ b/src/Rules/Arrays/IterableInForeachRule.php @@ -49,7 +49,7 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Argument of an invalid type %s supplied for foreach, only iterables are supported.', $type->describe(VerbosityLevel::typeOnly()), - ))->line($originalNode->expr->getLine())->build(), + ))->identifier('foreach.nonIterable')->line($originalNode->expr->getStartLine())->build(), ]; } diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php index 52f54e34ed..6ef401c577 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\BenevolentUnionType; @@ -14,18 +14,23 @@ use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; -use function count; use function sprintf; class NonexistentOffsetInArrayDimFetchCheck { - public function __construct(private RuleLevelHelper $ruleLevelHelper, private bool $reportMaybes) + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + private bool $reportMaybes, + private bool $bleedingEdge, + private bool $reportPossiblyNonexistentGeneralArrayOffset, + private bool $reportPossiblyNonexistentConstantArrayOffset, + ) { } /** - * @return RuleError[] + * @return list */ public function check( Scope $scope, @@ -45,27 +50,44 @@ public function check( return $typeResult->getUnknownClassErrors(); } - $hasOffsetValueType = $type->hasOffsetValueType($dimType); - $report = $hasOffsetValueType->no(); - if ($hasOffsetValueType->maybe()) { - $constantArrays = TypeUtils::getOldConstantArrays($type); - if (count($constantArrays) > 0) { - foreach ($constantArrays as $constantArray) { - if ($constantArray->hasOffsetValueType($dimType)->no()) { - $report = true; - break; - } - } - } + if ($scope->isInExpressionAssign($var) || $scope->isUndefinedExpressionAllowed($var)) { + return []; + } + + if ($type->hasOffsetValueType($dimType)->no()) { + return [ + RuleErrorBuilder::message(sprintf('Offset %s does not exist on %s.', $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value()))) + ->identifier('offsetAccess.notFound') + ->build(), + ]; } - if (!$report && $this->reportMaybes) { + if ($this->reportMaybes) { + $report = false; + if ($type instanceof BenevolentUnionType) { $flattenedTypes = [$type]; } else { $flattenedTypes = TypeUtils::flattenTypes($type); } foreach ($flattenedTypes as $innerType) { + if ( + $this->reportPossiblyNonexistentGeneralArrayOffset + && $innerType->isArray()->yes() + && !$innerType->isConstantArray()->yes() + && !$innerType->hasOffsetValueType($dimType)->yes() + ) { + $report = true; + break; + } + if ( + $this->reportPossiblyNonexistentConstantArrayOffset + && $innerType->isConstantArray()->yes() + && !$innerType->hasOffsetValueType($dimType)->yes() + ) { + $report = true; + break; + } if ($dimType instanceof UnionType) { if ($innerType->hasOffsetValueType($dimType)->no()) { $report = true; @@ -76,20 +98,25 @@ public function check( foreach (TypeUtils::flattenTypes($dimType) as $innerDimType) { if ($innerType->hasOffsetValueType($innerDimType)->no()) { $report = true; - break; + break 2; } } } - } - if ($report) { - if ($scope->isInExpressionAssign($var) || $scope->isUndefinedExpressionAllowed($var)) { - return []; + if ($report) { + if ($this->bleedingEdge || $this->reportPossiblyNonexistentGeneralArrayOffset || $this->reportPossiblyNonexistentConstantArrayOffset) { + return [ + RuleErrorBuilder::message(sprintf('Offset %s might not exist on %s.', $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value()))) + ->identifier('offsetAccess.notFound') + ->build(), + ]; + } + return [ + RuleErrorBuilder::message(sprintf('Offset %s does not exist on %s.', $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value()))) + ->identifier('offsetAccess.notFound') + ->build(), + ]; } - - return [ - RuleErrorBuilder::message(sprintf('Offset %s does not exist on %s.', $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value())))->build(), - ]; } return []; diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php index cee6554562..cf8f3f43c1 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php @@ -54,6 +54,10 @@ public function processNode(Node $node, Scope $scope): array return $isOffsetAccessibleTypeResult->getUnknownClassErrors(); } + if ($scope->hasExpressionType($node)->yes()) { + return []; + } + $isOffsetAccessible = $isOffsetAccessibleType->isOffsetAccessible(); if ($scope->isInExpressionAssign($node) && $isOffsetAccessible->yes()) { @@ -72,7 +76,7 @@ public function processNode(Node $node, Scope $scope): array 'Cannot access offset %s on %s.', $dimType->describe(VerbosityLevel::value()), $isOffsetAccessibleType->describe(VerbosityLevel::value()), - ))->build(), + ))->identifier('offsetAccess.nonOffsetAccessible')->build(), ]; } @@ -80,14 +84,14 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Cannot access an offset on %s.', $isOffsetAccessibleType->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('offsetAccess.nonOffsetAccessible')->build(), ]; } return []; } - if ($dimType === null || $scope->isSpecified($node)) { + if ($dimType === null) { return []; } diff --git a/src/Rules/Arrays/OffsetAccessAssignOpRule.php b/src/Rules/Arrays/OffsetAccessAssignOpRule.php index 46e3e1fb4e..ff66d9bb9f 100644 --- a/src/Rules/Arrays/OffsetAccessAssignOpRule.php +++ b/src/Rules/Arrays/OffsetAccessAssignOpRule.php @@ -81,7 +81,7 @@ static function (Type $dimType) use ($varType): bool { RuleErrorBuilder::message(sprintf( 'Cannot assign new offset to %s.', $varType->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('offsetAssign.dimType')->build(), ]; } @@ -90,7 +90,7 @@ static function (Type $dimType) use ($varType): bool { 'Cannot assign offset %s to %s.', $dimType->describe(VerbosityLevel::value()), $varType->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('offsetAssign.dimType')->build(), ]; } diff --git a/src/Rules/Arrays/OffsetAccessAssignmentRule.php b/src/Rules/Arrays/OffsetAccessAssignmentRule.php index ec4ca4dc9c..0bafe975d0 100644 --- a/src/Rules/Arrays/OffsetAccessAssignmentRule.php +++ b/src/Rules/Arrays/OffsetAccessAssignmentRule.php @@ -82,7 +82,7 @@ static function (Type $dimType) use ($varType): bool { RuleErrorBuilder::message(sprintf( 'Cannot assign new offset to %s.', $varType->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('offsetAssign.dimType')->build(), ]; } @@ -91,7 +91,7 @@ static function (Type $dimType) use ($varType): bool { 'Cannot assign offset %s to %s.', $dimType->describe(VerbosityLevel::value()), $varType->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('offsetAssign.dimType')->build(), ]; } diff --git a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php index 47d58c87fa..0368895760 100644 --- a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php +++ b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php @@ -53,7 +53,6 @@ public function processNode(Node $node, Scope $scope): array $assignedValueType = $scope->getType($node); } - $originalArrayType = $scope->getType($arrayDimFetch->var); $arrayTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, $arrayDimFetch->var, @@ -76,12 +75,14 @@ static function (Type $varType) use ($assignedValueType): bool { return []; } + $originalArrayType = $scope->getType($arrayDimFetch->var); + return [ RuleErrorBuilder::message(sprintf( '%s does not accept %s.', $originalArrayType->describe(VerbosityLevel::value()), $assignedValueType->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('offsetAssign.valueType')->build(), ]; } diff --git a/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php b/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php index 2f93bd119f..ce70e82425 100644 --- a/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php +++ b/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php @@ -29,7 +29,10 @@ public function processNode(Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message('Cannot use [] for reading.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Cannot use [] for reading.') + ->identifier('offsetAccess.noDim') + ->nonIgnorable() + ->build(), ]; } diff --git a/src/Rules/Arrays/UnpackIterableInArrayRule.php b/src/Rules/Arrays/UnpackIterableInArrayRule.php index 08ba2b7e84..10a3a67758 100644 --- a/src/Rules/Arrays/UnpackIterableInArrayRule.php +++ b/src/Rules/Arrays/UnpackIterableInArrayRule.php @@ -60,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Only iterables can be unpacked, %s given.', $type->describe(VerbosityLevel::typeOnly()), - ))->line($item->getLine())->build(); + ))->identifier('arrayUnpacking.nonIterable')->line($item->getStartLine())->build(); } return $errors; diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index 9522a5df2a..916c6e1c7d 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -20,15 +20,16 @@ class AttributesCheck public function __construct( private ReflectionProvider $reflectionProvider, private FunctionCallParametersCheck $functionCallParametersCheck, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, + private bool $deprecationRulesInstalled, ) { } /** * @param AttributeGroup[] $attrGroups - * @param Attribute::TARGET_* $requiredTarget - * @return RuleError[] + * @param int-mask-of $requiredTarget + * @return list */ public function check( Scope $scope, @@ -43,57 +44,81 @@ public function check( foreach ($attrGroup->attrs as $attribute) { $name = $attribute->name->toString(); if (!$this->reflectionProvider->hasClass($name)) { - $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s does not exist.', $name))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s does not exist.', $name)) + ->line($attribute->getStartLine()) + ->identifier('attribute.notFound') + ->build(); continue; } $attributeClass = $this->reflectionProvider->getClass($name); if (!$attributeClass->isAttributeClass()) { - $classLikeDescription = 'Class'; - if ($attributeClass->isInterface()) { - $classLikeDescription = 'Interface'; - } elseif ($attributeClass->isTrait()) { - $classLikeDescription = 'Trait'; - } elseif ($attributeClass->isEnum()) { - $classLikeDescription = 'Enum'; - } - - $errors[] = RuleErrorBuilder::message(sprintf('%s %s is not an Attribute class.', $classLikeDescription, $attributeClass->getDisplayName()))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('%s %s is not an Attribute class.', $attributeClass->getClassTypeDescription(), $attributeClass->getDisplayName())) + ->identifier('attribute.notAttribute') + ->line($attribute->getStartLine()) + ->build(); continue; } if ($attributeClass->isAbstract()) { - $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s is abstract.', $name))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s is abstract.', $name)) + ->identifier('attribute.abstract') + ->line($attribute->getStartLine()) + ->build(); } - foreach ($this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($name, $attribute)]) as $caseSensitivityError) { + foreach ($this->classCheck->checkClassNames([new ClassNameNodePair($name, $attribute)]) as $caseSensitivityError) { $errors[] = $caseSensitivityError; } $flags = $attributeClass->getAttributeClassFlags(); if (($flags & $requiredTarget) === 0) { - $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s does not have the %s target.', $name, $targetName))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s does not have the %s target.', $name, $targetName)) + ->identifier('attribute.target') + ->line($attribute->getStartLine()) + ->build(); } if (($flags & Attribute::IS_REPEATABLE) === 0) { $loweredName = strtolower($name); if (array_key_exists($loweredName, $alreadyPresent)) { - $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s is not repeatable but is already present above the %s.', $name, $targetName))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s is not repeatable but is already present above the %s.', $name, $targetName)) + ->identifier('attribute.nonRepeatable') + ->line($attribute->getStartLine()) + ->build(); } $alreadyPresent[$loweredName] = true; } + if ($this->deprecationRulesInstalled && $attributeClass->isDeprecated()) { + if ($attributeClass->getDeprecatedDescription() !== null) { + $deprecatedError = sprintf('Attribute class %s is deprecated: %s', $name, $attributeClass->getDeprecatedDescription()); + } else { + $deprecatedError = sprintf('Attribute class %s is deprecated.', $name); + } + $errors[] = RuleErrorBuilder::message($deprecatedError) + ->identifier('attribute.deprecated') + ->line($attribute->getStartLine()) + ->build(); + } + if (!$attributeClass->hasConstructor()) { if (count($attribute->args) > 0) { - $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s does not have a constructor and must be instantiated without any parameters.', $name))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s does not have a constructor and must be instantiated without any parameters.', $name)) + ->identifier('attribute.noConstructor') + ->line($attribute->getStartLine()) + ->build(); } continue; } $attributeConstructor = $attributeClass->getConstructor(); if (!$attributeConstructor->isPublic()) { - $errors[] = RuleErrorBuilder::message(sprintf('Constructor of attribute class %s is not public.', $name))->line($attribute->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Constructor of attribute class %s is not public.', $name)) + ->identifier('attribute.constructorNotPublic') + ->line($attribute->getStartLine()) + ->build(); } $attributeClassName = SprintfHelper::escapeFormatString($attributeClass->getDisplayName()); @@ -106,6 +131,7 @@ public function check( $scope, $attribute->args, $attributeConstructor->getVariants(), + $attributeConstructor->getNamedArgumentsVariants(), ), $scope, $attributeConstructor->getDeclaringClass()->isBuiltin(), @@ -126,6 +152,7 @@ public function check( 'Return type of call to ' . $attributeClassName . ' constructor contains unresolvable type.', 'Parameter %s of attribute class ' . $attributeClassName . ' constructor contains unresolvable type.', ], + 'attribute', ); foreach ($parameterErrors as $error) { diff --git a/src/Rules/Cast/EchoRule.php b/src/Rules/Cast/EchoRule.php index 705dc48ebd..d8033f5394 100644 --- a/src/Rules/Cast/EchoRule.php +++ b/src/Rules/Cast/EchoRule.php @@ -49,7 +49,7 @@ public function processNode(Node $node, Scope $scope): array 'Parameter #%d (%s) of echo cannot be converted to string.', $key + 1, $typeResult->getType()->describe(VerbosityLevel::value()), - ))->line($expr->getLine())->build(); + ))->identifier('echo.nonString')->line($expr->getStartLine())->build(); } return $messages; } diff --git a/src/Rules/Cast/InvalidCastRule.php b/src/Rules/Cast/InvalidCastRule.php index 4179390523..ceb074e36c 100644 --- a/src/Rules/Cast/InvalidCastRule.php +++ b/src/Rules/Cast/InvalidCastRule.php @@ -36,15 +36,15 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $castTypeCallback = static function (Type $type) use ($node): ?Type { + $castTypeCallback = static function (Type $type) use ($node): ?array { if ($node instanceof Node\Expr\Cast\Int_) { - return $type->toInteger(); + return [$type->toInteger(), 'int']; } elseif ($node instanceof Node\Expr\Cast\Bool_) { - return $type->toBoolean(); + return [$type->toBoolean(), 'bool']; } elseif ($node instanceof Node\Expr\Cast\Double) { - return $type->toFloat(); + return [$type->toFloat(), 'double']; } elseif ($node instanceof Node\Expr\Cast\String_) { - return $type->toString(); + return [$type->toString(), 'string']; } return null; @@ -55,11 +55,13 @@ public function processNode(Node $node, Scope $scope): array $node->expr, '', static function (Type $type) use ($castTypeCallback): bool { - $castType = $castTypeCallback($type); - if ($castType === null) { + $castResult = $castTypeCallback($type); + if ($castResult === null) { return true; } + [$castType] = $castResult; + return !$castType instanceof ErrorType; }, ); @@ -68,7 +70,13 @@ static function (Type $type) use ($castTypeCallback): bool { return []; } - $castType = $castTypeCallback($type); + $castResult = $castTypeCallback($type); + if ($castResult === null) { + return []; + } + + [$castType, $castIdentifier] = $castResult; + if ($castType instanceof ErrorType) { $classReflection = $this->reflectionProvider->getClass(get_class($node)); $shortName = $classReflection->getNativeReflection()->getShortName(); @@ -84,7 +92,7 @@ static function (Type $type) use ($castTypeCallback): bool { 'Cannot cast %s to %s.', $scope->getType($node->expr)->describe(VerbosityLevel::value()), $shortName, - ))->line($node->getLine())->build(), + ))->identifier(sprintf('cast.%s', $castIdentifier))->line($node->getStartLine())->build(), ]; } diff --git a/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php b/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php index ab0c4a868f..773aabe5d5 100644 --- a/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php +++ b/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php @@ -58,7 +58,7 @@ public function processNode(Node $node, Scope $scope): array 'Part %s (%s) of encapsed string cannot be cast to string.', $this->exprPrinter->printExpr($part), $partType->describe(VerbosityLevel::value()), - ))->line($part->getLine())->build(); + ))->identifier('encapsedStringPart.nonString')->line($part->getStartLine())->build(); } return $messages; diff --git a/src/Rules/Cast/PrintRule.php b/src/Rules/Cast/PrintRule.php index e511f76ebf..3b8ad5f97d 100644 --- a/src/Rules/Cast/PrintRule.php +++ b/src/Rules/Cast/PrintRule.php @@ -42,7 +42,7 @@ public function processNode(Node $node, Scope $scope): array return [RuleErrorBuilder::message(sprintf( 'Parameter %s of print cannot be converted to string.', $typeResult->getType()->describe(VerbosityLevel::value()), - ))->line($node->expr->getLine())->build()]; + ))->identifier('print.nonString')->line($node->expr->getStartLine())->build()]; } return []; diff --git a/src/Rules/Cast/UnsetCastRule.php b/src/Rules/Cast/UnsetCastRule.php index 6260d9805b..be527ffad0 100644 --- a/src/Rules/Cast/UnsetCastRule.php +++ b/src/Rules/Cast/UnsetCastRule.php @@ -30,7 +30,10 @@ public function processNode(Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message('The (unset) cast is no longer supported in PHP 8.0 and later.')->nonIgnorable()->build(), + RuleErrorBuilder::message('The (unset) cast is no longer supported in PHP 8.0 and later.') + ->identifier('cast.unset') + ->nonIgnorable() + ->build(), ]; } diff --git a/src/Rules/ClassCaseSensitivityCheck.php b/src/Rules/ClassCaseSensitivityCheck.php index cb3f572609..f7cdfdaab3 100644 --- a/src/Rules/ClassCaseSensitivityCheck.php +++ b/src/Rules/ClassCaseSensitivityCheck.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules; -use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ReflectionProvider; use function sprintf; use function strtolower; @@ -16,7 +15,7 @@ public function __construct(private ReflectionProvider $reflectionProvider, priv /** * @param ClassNameNodePair[] $pairs - * @return RuleError[] + * @return list */ public function checkClassNames(array $pairs): array { @@ -38,28 +37,19 @@ public function checkClassNames(array $pairs): array continue; } + $typeName = $classReflection->getClassTypeDescription(); $errors[] = RuleErrorBuilder::message(sprintf( '%s %s referenced with incorrect case: %s.', - $this->getTypeName($classReflection), + $typeName, $realClassName, $className, - ))->line($pair->getNode()->getLine())->build(); + )) + ->identifier(sprintf('%s.nameCase', strtolower($typeName))) + ->line($pair->getNode()->getStartLine()) + ->build(); } return $errors; } - private function getTypeName(ClassReflection $classReflection): string - { - if ($classReflection->isInterface()) { - return 'Interface'; - } elseif ($classReflection->isTrait()) { - return 'Trait'; - } elseif ($classReflection->isEnum()) { - return 'Enum'; - } - - return 'Class'; - } - } diff --git a/src/Rules/ClassForbiddenNameCheck.php b/src/Rules/ClassForbiddenNameCheck.php new file mode 100644 index 0000000000..92d9fe2f8c --- /dev/null +++ b/src/Rules/ClassForbiddenNameCheck.php @@ -0,0 +1,92 @@ + '_PHPStan_', + 'Rector' => 'RectorPrefix', + 'PHP-Scoper' => '_PhpScoper', + 'PHPUnit' => 'PHPUnitPHAR', + ]; + + public function __construct(private Container $container) + { + } + + /** + * @param ClassNameNodePair[] $pairs + * @return list + */ + public function checkClassNames(array $pairs): array + { + $extensions = $this->container->getServicesByTag(ForbiddenClassNameExtension::EXTENSION_TAG); + + $classPrefixes = array_merge( + self::INTERNAL_CLASS_PREFIXES, + ...array_map( + static fn (ForbiddenClassNameExtension $extension): array => $extension->getClassPrefixes(), + $extensions, + ), + ); + + $errors = []; + foreach ($pairs as $pair) { + $className = $pair->getClassName(); + + $projectName = null; + $withoutPrefixClassName = null; + foreach ($classPrefixes as $project => $prefix) { + if (!str_starts_with($className, $prefix)) { + continue; + } + + $projectName = $project; + $withoutPrefixClassName = substr($className, strlen($prefix)); + + if (strpos($withoutPrefixClassName, '\\') === false) { + continue; + } + + $withoutPrefixClassName = substr($withoutPrefixClassName, strpos($withoutPrefixClassName, '\\')); + } + + if ($projectName === null) { + continue; + } + + $error = RuleErrorBuilder::message(sprintf( + 'Referencing prefixed %s class: %s.', + $projectName, + $className, + )) + ->line($pair->getNode()->getLine()) + ->identifier('class.prefixed') + ->nonIgnorable(); + + if ($withoutPrefixClassName !== null) { + $error->tip(sprintf( + 'This is most likely unintentional. Did you mean to type %s?', + $withoutPrefixClassName, + )); + } + + $errors[] = $error->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/ClassNameCheck.php b/src/Rules/ClassNameCheck.php new file mode 100644 index 0000000000..c168b74ce7 --- /dev/null +++ b/src/Rules/ClassNameCheck.php @@ -0,0 +1,35 @@ + + */ + public function checkClassNames(array $pairs, bool $checkClassCaseSensitivity = true): array + { + $errors = []; + + if ($checkClassCaseSensitivity) { + foreach ($this->classCaseSensitivityCheck->checkClassNames($pairs) as $error) { + $errors[] = $error; + } + } + foreach ($this->classForbiddenNameCheck->checkClassNames($pairs) as $error) { + $errors[] = $error; + } + + return $errors; + } + +} diff --git a/src/Rules/Classes/AccessPrivateConstantThroughStaticRule.php b/src/Rules/Classes/AccessPrivateConstantThroughStaticRule.php index a2ca28b670..438398a4f9 100644 --- a/src/Rules/Classes/AccessPrivateConstantThroughStaticRule.php +++ b/src/Rules/Classes/AccessPrivateConstantThroughStaticRule.php @@ -54,7 +54,7 @@ public function processNode(Node $node, Scope $scope): array 'Unsafe access to private constant %s::%s through static::.', $constant->getDeclaringClass()->getDisplayName(), $constantName, - ))->build(), + ))->identifier('staticClassAccess.privateConstant')->build(), ]; } diff --git a/src/Rules/Classes/AllowedSubTypesRule.php b/src/Rules/Classes/AllowedSubTypesRule.php new file mode 100644 index 0000000000..164e63d9d7 --- /dev/null +++ b/src/Rules/Classes/AllowedSubTypesRule.php @@ -0,0 +1,68 @@ + + */ +class AllowedSubTypesRule implements Rule +{ + + public function getNodeType(): string + { + return InClassNode::class; + } + + /** + * @param InClassNode $node + */ + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + $className = $classReflection->getName(); + + $parents = array_values($classReflection->getImmediateInterfaces()); + $parentClass = $classReflection->getParentClass(); + if ($parentClass !== null) { + $parents[] = $parentClass; + } + + $messages = []; + + foreach ($parents as $parentReflection) { + $allowedSubTypes = $parentReflection->getAllowedSubTypes(); + if ($allowedSubTypes === null) { + continue; + } + + foreach ($allowedSubTypes as $allowedSubType) { + if (!$allowedSubType->isObject()->yes()) { + continue; + } + + if ($allowedSubType->getObjectClassNames() === [$className]) { + continue 2; + } + } + + $identifierType = strtolower($classReflection->getClassTypeDescription()); + $messages[] = RuleErrorBuilder::message(sprintf( + 'Type %s is not allowed to be a subtype of %s.', + $className, + $parentReflection->getName(), + ))->identifier(sprintf('%s.disallowedSubtype', $identifierType))->build(); + } + + return $messages; + } + +} diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index 41e8c9b7b3..f8425ff295 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -9,7 +9,7 @@ use PHPStan\Internal\SprintfHelper; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -34,7 +34,7 @@ class ClassConstantRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private PhpVersion $phpVersion, ) { @@ -60,7 +60,9 @@ public function processNode(Node $node, Scope $scope): array if (in_array($lowercasedClassName, ['self', 'static'], true)) { if (!$scope->isInClass()) { return [ - RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $className))->build(), + RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $className)) + ->identifier(sprintf('outOfClass.%s', $lowercasedClassName)) + ->build(), ]; } @@ -68,7 +70,9 @@ public function processNode(Node $node, Scope $scope): array } elseif ($lowercasedClassName === 'parent') { if (!$scope->isInClass()) { return [ - RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $className))->build(), + RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $className)) + ->identifier(sprintf('outOfClass.%s', $lowercasedClassName)) + ->build(), ]; } $currentClassReflection = $scope->getClassReflection(); @@ -78,7 +82,7 @@ public function processNode(Node $node, Scope $scope): array 'Access to parent::%s but %s does not extend any class.', $constantName, $currentClassReflection->getDisplayName(), - ))->build(), + ))->identifier('class.noParent')->build(), ]; } $classType = $scope->resolveTypeByName($class); @@ -90,19 +94,25 @@ public function processNode(Node $node, Scope $scope): array if (strtolower($constantName) === 'class') { return [ - RuleErrorBuilder::message(sprintf('Class %s not found.', $className))->discoveringSymbolsTip()->build(), + RuleErrorBuilder::message(sprintf('Class %s not found.', $className)) + ->identifier('class.notFound') + ->discoveringSymbolsTip() + ->build(), ]; } return [ RuleErrorBuilder::message( sprintf('Access to constant %s on an unknown class %s.', $constantName, $className), - )->discoveringSymbolsTip()->build(), + ) + ->identifier('class.notFound') + ->discoveringSymbolsTip() + ->build(), ]; - } else { - $messages = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($className, $class)]); } + $messages = $this->classCheck->checkClassNames([new ClassNameNodePair($className, $class)]); + $classType = $scope->resolveTypeByName($class); } @@ -125,6 +135,7 @@ public function processNode(Node $node, Scope $scope): array if (!$this->phpVersion->supportsClassConstantOnExpression()) { return [ RuleErrorBuilder::message('Accessing ::class constant on an expression is supported only on PHP 8.0 and later.') + ->identifier('classConstant.notSupported') ->nonIgnorable() ->build(), ]; @@ -133,6 +144,7 @@ public function processNode(Node $node, Scope $scope): array if (!$class instanceof Node\Scalar\String_ && $classType->isString()->yes()) { return [ RuleErrorBuilder::message('Accessing ::class constant on a dynamic string is not supported in PHP.') + ->identifier('classConstant.dynamicString') ->nonIgnorable() ->build(), ]; @@ -156,11 +168,11 @@ public function processNode(Node $node, Scope $scope): array 'Cannot access constant %s on %s.', $constantName, $typeForDescribe->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('classConstant.nonObject')->build(), ]); } - if (strtolower($constantName) === 'class') { + if (strtolower($constantName) === 'class' || $scope->hasExpressionType($node)->yes()) { return $messages; } @@ -170,7 +182,7 @@ public function processNode(Node $node, Scope $scope): array 'Access to undefined constant %s::%s.', $typeForDescribe->describe(VerbosityLevel::typeOnly()), $constantName, - ))->build(), + ))->identifier('classConstant.notFound')->build(), ]); } @@ -182,6 +194,9 @@ public function processNode(Node $node, Scope $scope): array $constantReflection->isPrivate() ? 'private' : 'protected', $constantName, $constantReflection->getDeclaringClass()->getDisplayName(), + ))->identifier(sprintf( + 'classConstant.%s', + $constantReflection->isPrivate() ? 'private' : 'protected', ))->build(), ]); } diff --git a/src/Rules/Classes/DuplicateClassDeclarationRule.php b/src/Rules/Classes/DuplicateClassDeclarationRule.php new file mode 100644 index 0000000000..ae2658de79 --- /dev/null +++ b/src/Rules/Classes/DuplicateClassDeclarationRule.php @@ -0,0 +1,66 @@ + + */ +class DuplicateClassDeclarationRule implements Rule +{ + + public function __construct(private Reflector $reflector, private RelativePathHelper $relativePathHelper) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $thisClass = $node->getClassReflection(); + $className = $thisClass->getName(); + $allClasses = $this->reflector->reflectAllClasses(); + $filteredClasses = []; + foreach ($allClasses as $reflectionClass) { + if ($reflectionClass->getName() !== $className) { + continue; + } + + $filteredClasses[] = $reflectionClass; + } + + if (count($filteredClasses) < 2) { + return []; + } + + $filteredClasses = array_filter($filteredClasses, static fn (ReflectionClass $class) => $class->getStartLine() !== $thisClass->getNativeReflection()->getStartLine()); + + $identifierType = strtolower($thisClass->getClassTypeDescription()); + + return [ + RuleErrorBuilder::message(sprintf( + "Class %s declared multiple times:\n%s", + $thisClass->getDisplayName(), + implode("\n", array_map(fn (ReflectionClass $class) => sprintf('- %s:%d', $this->relativePathHelper->getRelativePath($class->getFileName() ?? 'unknown'), $class->getStartLine()), $filteredClasses)), + ))->identifier(sprintf('%s.duplicate', $identifierType))->build(), + ]; + } + +} diff --git a/src/Rules/Classes/DuplicateDeclarationRule.php b/src/Rules/Classes/DuplicateDeclarationRule.php index 257bf2eaf7..b5e122eb0e 100644 --- a/src/Rules/Classes/DuplicateDeclarationRule.php +++ b/src/Rules/Classes/DuplicateDeclarationRule.php @@ -28,10 +28,9 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $classReflection = $scope->getClassReflection(); - if ($classReflection === null) { - throw new ShouldNotHappenException(); - } + $classReflection = $node->getClassReflection(); + + $identifierType = strtolower($classReflection->getClassTypeDescription()); $errors = []; @@ -43,7 +42,10 @@ public function processNode(Node $node, Scope $scope): array 'Cannot redeclare enum case %s::%s.', $classReflection->getDisplayName(), $stmtNode->name->name, - ))->line($stmtNode->getLine())->nonIgnorable()->build(); + ))->identifier(sprintf('%s.duplicateEnumCase', $identifierType)) + ->line($stmtNode->getStartLine()) + ->nonIgnorable() + ->build(); } else { $declaredClassConstantsOrEnumCases[$stmtNode->name->name] = true; } @@ -54,7 +56,10 @@ public function processNode(Node $node, Scope $scope): array 'Cannot redeclare constant %s::%s.', $classReflection->getDisplayName(), $classConstNode->name->name, - ))->line($classConstNode->getLine())->nonIgnorable()->build(); + ))->identifier(sprintf('%s.duplicateConstant', $identifierType)) + ->line($classConstNode->getStartLine()) + ->nonIgnorable() + ->build(); } else { $declaredClassConstantsOrEnumCases[$classConstNode->name->name] = true; } @@ -70,7 +75,10 @@ public function processNode(Node $node, Scope $scope): array 'Cannot redeclare property %s::$%s.', $classReflection->getDisplayName(), $property->name->name, - ))->line($property->getLine())->nonIgnorable()->build(); + ))->identifier(sprintf('%s.duplicateProperty', $identifierType)) + ->line($property->getStartLine()) + ->nonIgnorable() + ->build(); } else { $declaredProperties[$property->name->name] = true; } @@ -96,7 +104,10 @@ public function processNode(Node $node, Scope $scope): array 'Cannot redeclare property %s::$%s.', $classReflection->getDisplayName(), $propertyName, - ))->line($param->getLine())->nonIgnorable()->build(); + ))->identifier(sprintf('%s.duplicateProperty', $identifierType)) + ->line($param->getStartLine()) + ->nonIgnorable() + ->build(); } else { $declaredProperties[$propertyName] = true; } @@ -107,7 +118,10 @@ public function processNode(Node $node, Scope $scope): array 'Cannot redeclare method %s::%s().', $classReflection->getDisplayName(), $method->name->name, - ))->line($method->getStartLine())->nonIgnorable()->build(); + ))->identifier(sprintf('%s.duplicateMethod', $identifierType)) + ->line($method->getStartLine()) + ->nonIgnorable() + ->build(); } else { $declaredFunctions[strtolower($method->name->name)] = true; } diff --git a/src/Rules/Classes/EnumSanityRule.php b/src/Rules/Classes/EnumSanityRule.php index aa94004670..742dbd1842 100644 --- a/src/Rules/Classes/EnumSanityRule.php +++ b/src/Rules/Classes/EnumSanityRule.php @@ -4,16 +4,21 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; +use PHPStan\Type\IntegerType; +use PHPStan\Type\StringType; +use PHPStan\Type\VerbosityLevel; use Serializable; use function array_key_exists; +use function count; +use function implode; +use function in_array; use function sprintf; /** - * @implements Rule + * @implements Rule */ class EnumSanityRule implements Rule { @@ -24,35 +29,34 @@ class EnumSanityRule implements Rule '__invoke' => true, ]; - public function __construct( - private ReflectionProvider $reflectionProvider, - ) - { - } - public function getNodeType(): string { - return Node\Stmt\Enum_::class; + return InClassNode::class; } - /** - * @param Node\Stmt\Enum_ $node - */ public function processNode(Node $node, Scope $scope): array { - $errors = []; - - if ($node->namespacedName === null) { - throw new ShouldNotHappenException(); + $classReflection = $node->getClassReflection(); + if (!$classReflection->isEnum()) { + return []; } - foreach ($node->getMethods() as $methodNode) { + /** @var Node\Stmt\Enum_ $enumNode */ + $enumNode = $node->getOriginalNode(); + + $errors = []; + + foreach ($enumNode->getMethods() as $methodNode) { if ($methodNode->isAbstract()) { $errors[] = RuleErrorBuilder::message(sprintf( 'Enum %s contains abstract method %s().', - $node->namespacedName->toString(), + $classReflection->getDisplayName(), $methodNode->name->name, - ))->line($methodNode->getLine())->nonIgnorable()->build(); + )) + ->identifier('enum.abstractMethod') + ->line($methodNode->getStartLine()) + ->nonIgnorable() + ->build(); } $lowercasedMethodName = $methodNode->name->toLowerString(); @@ -61,65 +65,172 @@ public function processNode(Node $node, Scope $scope): array if ($lowercasedMethodName === '__construct') { $errors[] = RuleErrorBuilder::message(sprintf( 'Enum %s contains constructor.', - $node->namespacedName->toString(), - ))->line($methodNode->getLine())->nonIgnorable()->build(); + $classReflection->getDisplayName(), + )) + ->identifier('enum.constructor') + ->line($methodNode->getStartLine()) + ->nonIgnorable() + ->build(); } elseif ($lowercasedMethodName === '__destruct') { $errors[] = RuleErrorBuilder::message(sprintf( 'Enum %s contains destructor.', - $node->namespacedName->toString(), - ))->line($methodNode->getLine())->nonIgnorable()->build(); + $classReflection->getDisplayName(), + )) + ->identifier('enum.destructor') + ->line($methodNode->getStartLine()) + ->nonIgnorable() + ->build(); } elseif (!array_key_exists($lowercasedMethodName, self::ALLOWED_MAGIC_METHODS)) { $errors[] = RuleErrorBuilder::message(sprintf( 'Enum %s contains magic method %s().', - $node->namespacedName->toString(), + $classReflection->getDisplayName(), $methodNode->name->name, - ))->line($methodNode->getLine())->nonIgnorable()->build(); + )) + ->identifier('enum.magicMethod') + ->line($methodNode->getStartLine()) + ->nonIgnorable() + ->build(); } } if ($lowercasedMethodName === 'cases') { $errors[] = RuleErrorBuilder::message(sprintf( 'Enum %s cannot redeclare native method %s().', - $node->namespacedName->toString(), + $classReflection->getDisplayName(), $methodNode->name->name, - ))->line($methodNode->getLine())->nonIgnorable()->build(); + )) + ->identifier('enum.methodRedeclaration') + ->line($methodNode->getStartLine()) + ->nonIgnorable() + ->build(); } - if ($node->scalarType === null) { + if ($enumNode->scalarType === null) { continue; } - if ($lowercasedMethodName !== 'from' && $lowercasedMethodName !== 'tryfrom') { + if (!in_array($lowercasedMethodName, ['from', 'tryfrom'], true)) { continue; } $errors[] = RuleErrorBuilder::message(sprintf( 'Enum %s cannot redeclare native method %s().', - $node->namespacedName->toString(), + $classReflection->getDisplayName(), $methodNode->name->name, - ))->line($methodNode->getLine())->nonIgnorable()->build(); + )) + ->identifier('enum.methodRedeclaration') + ->line($methodNode->getStartLine()) + ->nonIgnorable() + ->build(); } if ( - $node->scalarType !== null - && $node->scalarType->name !== 'int' - && $node->scalarType->name !== 'string' + $enumNode->scalarType !== null + && !in_array($enumNode->scalarType->name, ['int', 'string'], true) ) { $errors[] = RuleErrorBuilder::message(sprintf( 'Backed enum %s can have only "int" or "string" type.', - $node->namespacedName->toString(), - ))->line($node->scalarType->getLine())->nonIgnorable()->build(); + $classReflection->getDisplayName(), + )) + ->identifier('enum.backingType') + ->line($enumNode->scalarType->getStartLine()) + ->nonIgnorable() + ->build(); } - if ($this->reflectionProvider->hasClass($node->namespacedName->toString())) { - $classReflection = $this->reflectionProvider->getClass($node->namespacedName->toString()); + if ($classReflection->implementsInterface(Serializable::class)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Enum %s cannot implement the Serializable interface.', + $classReflection->getDisplayName(), + )) + ->identifier('enum.serializable') + ->line($enumNode->getStartLine()) + ->nonIgnorable() + ->build(); + } + + $enumCases = []; + foreach ($enumNode->stmts as $stmt) { + if (!$stmt instanceof Node\Stmt\EnumCase) { + continue; + } + $caseName = $stmt->name->name; + + if ($stmt->expr instanceof Node\Scalar\LNumber || $stmt->expr instanceof Node\Scalar\String_) { + if ($enumNode->scalarType === null) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Enum %s is not backed, but case %s has value %s.', + $classReflection->getDisplayName(), + $caseName, + $stmt->expr->value, + )) + ->identifier('enum.caseWithValue') + ->line($stmt->getStartLine()) + ->nonIgnorable() + ->build(); + } else { + $caseValue = $stmt->expr->value; + + if (!isset($enumCases[$caseValue])) { + $enumCases[$caseValue] = []; + } + + $enumCases[$caseValue][] = $caseName; + } + } + + if ($enumNode->scalarType === null) { + continue; + } - if ($classReflection->implementsInterface(Serializable::class)) { + if ($stmt->expr === null) { $errors[] = RuleErrorBuilder::message(sprintf( - 'Enum %s cannot implement the Serializable interface.', - $node->namespacedName->toString(), - ))->line($node->getLine())->nonIgnorable()->build(); + 'Enum case %s::%s does not have a value but the enum is backed with the "%s" type.', + $classReflection->getDisplayName(), + $caseName, + $enumNode->scalarType->name, + )) + ->identifier('enum.missingCase') + ->line($stmt->getStartLine()) + ->nonIgnorable() + ->build(); + continue; + } + + $exprType = $scope->getType($stmt->expr); + $scalarType = $enumNode->scalarType->toLowerString() === 'int' ? new IntegerType() : new StringType(); + if ($scalarType->isSuperTypeOf($exprType)->yes()) { + continue; } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Enum case %s::%s value %s does not match the "%s" type.', + $classReflection->getDisplayName(), + $caseName, + $exprType->describe(VerbosityLevel::value()), + $scalarType->describe(VerbosityLevel::typeOnly()), + )) + ->identifier('enum.caseType') + ->line($stmt->getStartLine()) + ->nonIgnorable() + ->build(); + } + + foreach ($enumCases as $caseValue => $caseNames) { + if (count($caseNames) <= 1) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Enum %s has duplicate value %s for cases %s.', + $classReflection->getDisplayName(), + $caseValue, + implode(', ', $caseNames), + )) + ->identifier('enum.duplicateValue') + ->line($enumNode->getStartLine()) + ->nonIgnorable() + ->build(); } return $errors; diff --git a/src/Rules/Classes/ExistingClassInClassExtendsRule.php b/src/Rules/Classes/ExistingClassInClassExtendsRule.php index 513c7efcd5..b0a6aae26d 100644 --- a/src/Rules/Classes/ExistingClassInClassExtendsRule.php +++ b/src/Rules/Classes/ExistingClassInClassExtendsRule.php @@ -5,7 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -18,7 +18,7 @@ class ExistingClassInClassExtendsRule implements Rule { public function __construct( - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, ) { @@ -35,7 +35,7 @@ public function processNode(Node $node, Scope $scope): array return []; } $extendedClassName = (string) $node->extends; - $messages = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($extendedClassName, $node->extends)]); + $messages = $this->classCheck->checkClassNames([new ClassNameNodePair($extendedClassName, $node->extends)]); $currentClassName = null; if (isset($node->namespacedName)) { $currentClassName = (string) $node->namespacedName; @@ -46,7 +46,11 @@ public function processNode(Node $node, Scope $scope): array '%s extends unknown class %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $extendedClassName, - ))->nonIgnorable()->discoveringSymbolsTip()->build(); + )) + ->identifier('class.notFound') + ->nonIgnorable() + ->discoveringSymbolsTip() + ->build(); } } else { $reflection = $this->reflectionProvider->getClass($extendedClassName); @@ -55,31 +59,69 @@ public function processNode(Node $node, Scope $scope): array '%s extends interface %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('class.extendsInterface') + ->nonIgnorable() + ->build(); } elseif ($reflection->isTrait()) { $messages[] = RuleErrorBuilder::message(sprintf( '%s extends trait %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('class.extendsTrait') + ->nonIgnorable() + ->build(); } elseif ($reflection->isEnum()) { $messages[] = RuleErrorBuilder::message(sprintf( '%s extends enum %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('class.extendsEnum') + ->nonIgnorable() + ->build(); } elseif ($reflection->isFinalByKeyword()) { $messages[] = RuleErrorBuilder::message(sprintf( '%s extends final class %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('class.extendsFinal') + ->nonIgnorable() + ->build(); } elseif ($reflection->isFinal()) { $messages[] = RuleErrorBuilder::message(sprintf( '%s extends @final class %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $reflection->getDisplayName(), - ))->build(); + )) + ->identifier('class.extendsFinalByPhpDoc') + ->build(); + } + + if ($reflection->isClass()) { + if ($node->isReadonly()) { + if (!$reflection->isReadOnly()) { + $messages[] = RuleErrorBuilder::message(sprintf( + '%s extends non-readonly class %s.', + $currentClassName !== null ? sprintf('Readonly class %s', $currentClassName) : 'Anonymous readonly class', + $reflection->getDisplayName(), + )) + ->identifier('class.readOnly') + ->nonIgnorable() + ->build(); + } + } elseif ($reflection->isReadOnly()) { + $messages[] = RuleErrorBuilder::message(sprintf( + '%s extends readonly class %s.', + $currentClassName !== null ? sprintf('Non-readonly class %s', $currentClassName) : 'Anonymous non-readonly class', + $reflection->getDisplayName(), + )) + ->identifier('class.nonReadOnly') + ->nonIgnorable() + ->build(); + } } } diff --git a/src/Rules/Classes/ExistingClassInInstanceOfRule.php b/src/Rules/Classes/ExistingClassInInstanceOfRule.php index c04a3d4b11..7eef8ba523 100644 --- a/src/Rules/Classes/ExistingClassInInstanceOfRule.php +++ b/src/Rules/Classes/ExistingClassInInstanceOfRule.php @@ -6,7 +6,7 @@ use PhpParser\Node\Expr\Instanceof_; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -24,7 +24,7 @@ class ExistingClassInInstanceOfRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $checkClassCaseSensitivity, ) { @@ -52,7 +52,10 @@ public function processNode(Node $node, Scope $scope): array ], true)) { if (!$scope->isInClass()) { return [ - RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $lowercaseName))->line($class->getLine())->build(), + RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $lowercaseName)) + ->identifier(sprintf('outOfClass.%s', $lowercaseName)) + ->line($class->getStartLine()) + ->build(), ]; } @@ -67,24 +70,32 @@ public function processNode(Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message(sprintf('Class %s not found.', $name))->line($class->getLine())->discoveringSymbolsTip()->build(), + RuleErrorBuilder::message(sprintf('Class %s not found.', $name)) + ->identifier('class.notFound') + ->line($class->getStartLine()) + ->discoveringSymbolsTip() + ->build(), ]; - } elseif ($this->checkClassCaseSensitivity) { - $errors = array_merge( - $errors, - $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($name, $class)]), - ); } + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames( + [new ClassNameNodePair($name, $class)], + $this->checkClassCaseSensitivity, + ), + ); + $classReflection = $this->reflectionProvider->getClass($name); - $expressionType = $scope->getType($node->expr); if ($classReflection->isTrait()) { + $expressionType = $scope->getType($node->expr); + $errors[] = RuleErrorBuilder::message(sprintf( 'Instanceof between %s and trait %s will always evaluate to false.', $expressionType->describe(VerbosityLevel::typeOnly()), $name, - ))->build(); + ))->identifier('instanceof.trait')->build(); } return $errors; diff --git a/src/Rules/Classes/ExistingClassInTraitUseRule.php b/src/Rules/Classes/ExistingClassInTraitUseRule.php index 43e84d80aa..e47d9cf5d6 100644 --- a/src/Rules/Classes/ExistingClassInTraitUseRule.php +++ b/src/Rules/Classes/ExistingClassInTraitUseRule.php @@ -5,7 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -20,7 +20,7 @@ class ExistingClassInTraitUseRule implements Rule { public function __construct( - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, ) { @@ -33,7 +33,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $messages = $this->classCaseSensitivityCheck->checkClassNames( + $messages = $this->classCheck->checkClassNames( array_map(static fn (Node\Name $traitName): ClassNameNodePair => new ClassNameNodePair((string) $traitName, $traitName), $node->traits), ); @@ -45,7 +45,10 @@ public function processNode(Node $node, Scope $scope): array if ($classReflection->isInterface()) { if (!$scope->isInTrait()) { foreach ($node->traits as $trait) { - $messages[] = RuleErrorBuilder::message(sprintf('Interface %s uses trait %s.', $classReflection->getName(), (string) $trait))->nonIgnorable()->build(); + $messages[] = RuleErrorBuilder::message(sprintf('Interface %s uses trait %s.', $classReflection->getName(), (string) $trait)) + ->identifier('interface.traitUse') + ->nonIgnorable() + ->build(); } } } else { @@ -61,15 +64,28 @@ public function processNode(Node $node, Scope $scope): array foreach ($node->traits as $trait) { $traitName = (string) $trait; if (!$this->reflectionProvider->hasClass($traitName)) { - $messages[] = RuleErrorBuilder::message(sprintf('%s uses unknown trait %s.', $currentName, $traitName))->nonIgnorable()->discoveringSymbolsTip()->build(); + $messages[] = RuleErrorBuilder::message(sprintf('%s uses unknown trait %s.', $currentName, $traitName)) + ->identifier('trait.notFound') + ->nonIgnorable() + ->discoveringSymbolsTip() + ->build(); } else { $reflection = $this->reflectionProvider->getClass($traitName); if ($reflection->isClass()) { - $messages[] = RuleErrorBuilder::message(sprintf('%s uses class %s.', $currentName, $reflection->getDisplayName()))->nonIgnorable()->build(); + $messages[] = RuleErrorBuilder::message(sprintf('%s uses class %s.', $currentName, $reflection->getDisplayName())) + ->identifier('traitUse.class') + ->nonIgnorable() + ->build(); } elseif ($reflection->isInterface()) { - $messages[] = RuleErrorBuilder::message(sprintf('%s uses interface %s.', $currentName, $reflection->getDisplayName()))->nonIgnorable()->build(); + $messages[] = RuleErrorBuilder::message(sprintf('%s uses interface %s.', $currentName, $reflection->getDisplayName())) + ->identifier('traitUse.interface') + ->nonIgnorable() + ->build(); } elseif ($reflection->isEnum()) { - $messages[] = RuleErrorBuilder::message(sprintf('%s uses enum %s.', $currentName, $reflection->getDisplayName()))->nonIgnorable()->build(); + $messages[] = RuleErrorBuilder::message(sprintf('%s uses enum %s.', $currentName, $reflection->getDisplayName())) + ->identifier('traitUse.enum') + ->nonIgnorable() + ->build(); } } } diff --git a/src/Rules/Classes/ExistingClassesInClassImplementsRule.php b/src/Rules/Classes/ExistingClassesInClassImplementsRule.php index fde6d21979..5aeb3fde46 100644 --- a/src/Rules/Classes/ExistingClassesInClassImplementsRule.php +++ b/src/Rules/Classes/ExistingClassesInClassImplementsRule.php @@ -5,7 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -19,7 +19,7 @@ class ExistingClassesInClassImplementsRule implements Rule { public function __construct( - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, ) { @@ -32,7 +32,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $messages = $this->classCaseSensitivityCheck->checkClassNames( + $messages = $this->classCheck->checkClassNames( array_map(static fn (Node\Name $interfaceName): ClassNameNodePair => new ClassNameNodePair((string) $interfaceName, $interfaceName), $node->implements), ); @@ -49,7 +49,11 @@ public function processNode(Node $node, Scope $scope): array '%s implements unknown interface %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $implementedClassName, - ))->nonIgnorable()->discoveringSymbolsTip()->build(); + )) + ->identifier('interface.notFound') + ->nonIgnorable() + ->discoveringSymbolsTip() + ->build(); } } else { $reflection = $this->reflectionProvider->getClass($implementedClassName); @@ -58,19 +62,28 @@ public function processNode(Node $node, Scope $scope): array '%s implements class %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('classImplements.class') + ->nonIgnorable() + ->build(); } elseif ($reflection->isTrait()) { $messages[] = RuleErrorBuilder::message(sprintf( '%s implements trait %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('classImplements.trait') + ->nonIgnorable() + ->build(); } elseif ($reflection->isEnum()) { $messages[] = RuleErrorBuilder::message(sprintf( '%s implements enum %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('classImplements.enum') + ->nonIgnorable() + ->build(); } } } diff --git a/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php b/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php index 81f6168eba..413f32fcd8 100644 --- a/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php +++ b/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php @@ -5,7 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -19,7 +19,7 @@ class ExistingClassesInEnumImplementsRule implements Rule { public function __construct( - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, ) { @@ -32,7 +32,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $messages = $this->classCaseSensitivityCheck->checkClassNames( + $messages = $this->classCheck->checkClassNames( array_map(static fn (Node\Name $interfaceName): ClassNameNodePair => new ClassNameNodePair((string) $interfaceName, $interfaceName), $node->implements), ); @@ -46,7 +46,11 @@ public function processNode(Node $node, Scope $scope): array 'Enum %s implements unknown interface %s.', $currentEnumName, $implementedClassName, - ))->nonIgnorable()->discoveringSymbolsTip()->build(); + )) + ->identifier('interface.notFound') + ->nonIgnorable() + ->discoveringSymbolsTip() + ->build(); } } else { $reflection = $this->reflectionProvider->getClass($implementedClassName); @@ -55,19 +59,28 @@ public function processNode(Node $node, Scope $scope): array 'Enum %s implements class %s.', $currentEnumName, $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('enumImplements.class') + ->nonIgnorable() + ->build(); } elseif ($reflection->isTrait()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Enum %s implements trait %s.', $currentEnumName, $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('enumImplements.trait') + ->nonIgnorable() + ->build(); } elseif ($reflection->isEnum()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Enum %s implements enum %s.', $currentEnumName, $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('enumImplements.enum') + ->nonIgnorable() + ->build(); } } } diff --git a/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php b/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php index f267550104..e25a2ac974 100644 --- a/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php +++ b/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php @@ -5,7 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -19,7 +19,7 @@ class ExistingClassesInInterfaceExtendsRule implements Rule { public function __construct( - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private ReflectionProvider $reflectionProvider, ) { @@ -32,7 +32,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $messages = $this->classCaseSensitivityCheck->checkClassNames( + $messages = $this->classCheck->checkClassNames( array_map(static fn (Node\Name $interfaceName): ClassNameNodePair => new ClassNameNodePair((string) $interfaceName, $interfaceName), $node->extends), ); @@ -45,7 +45,11 @@ public function processNode(Node $node, Scope $scope): array 'Interface %s extends unknown interface %s.', $currentInterfaceName, $extendedInterfaceName, - ))->nonIgnorable()->discoveringSymbolsTip()->build(); + )) + ->identifier('interface.notFound') + ->nonIgnorable() + ->discoveringSymbolsTip() + ->build(); } } else { $reflection = $this->reflectionProvider->getClass($extendedInterfaceName); @@ -54,19 +58,28 @@ public function processNode(Node $node, Scope $scope): array 'Interface %s extends class %s.', $currentInterfaceName, $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('interfaceExtends.class') + ->nonIgnorable() + ->build(); } elseif ($reflection->isTrait()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Interface %s extends trait %s.', $currentInterfaceName, $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('interfaceExtends.trait') + ->nonIgnorable() + ->build(); } elseif ($reflection->isEnum()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Interface %s extends enum %s.', $currentInterfaceName, $reflection->getDisplayName(), - ))->nonIgnorable()->build(); + )) + ->identifier('interfaceExtends.enum') + ->nonIgnorable() + ->build(); } } diff --git a/src/Rules/Classes/ImpossibleInstanceOfRule.php b/src/Rules/Classes/ImpossibleInstanceOfRule.php index 5558b8fba5..c84092f810 100644 --- a/src/Rules/Classes/ImpossibleInstanceOfRule.php +++ b/src/Rules/Classes/ImpossibleInstanceOfRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; @@ -23,6 +24,7 @@ class ImpossibleInstanceOfRule implements Rule public function __construct( private bool $checkAlwaysTrueInstanceof, private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, ) { } @@ -34,62 +36,73 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $instanceofType = $scope->getType($node); - $expressionType = $scope->getType($node->expr); + $instanceofType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); + if (!$instanceofType instanceof ConstantBooleanType) { + return []; + } if ($node->class instanceof Node\Name) { $className = $scope->resolveName($node->class); $classType = new ObjectType($className); } else { - $classType = $scope->getType($node->class); + $classType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->class) : $scope->getNativeType($node->class); $allowed = TypeCombinator::union( new StringType(), new ObjectWithoutClassType(), ); - if (!$allowed->accepts($classType, true)->yes()) { + if (!$allowed->isSuperTypeOf($classType)->yes()) { return [ RuleErrorBuilder::message(sprintf( 'Instanceof between %s and %s results in an error.', - $expressionType->describe(VerbosityLevel::typeOnly()), + $scope->getType($node->expr)->describe(VerbosityLevel::typeOnly()), $classType->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('instanceof.invalidExprType')->build(), ]; } } - if (!$instanceofType instanceof ConstantBooleanType) { - return []; - } - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } - $instanceofTypeWithoutPhpDocs = $scope->doNotTreatPhpDocTypesAsCertain()->getType($node); + $instanceofTypeWithoutPhpDocs = $scope->getNativeType($node); if ($instanceofTypeWithoutPhpDocs instanceof ConstantBooleanType) { return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; if (!$instanceofType->getValue()) { + $exprType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->expr) : $scope->getNativeType($node->expr); + return [ $addTip(RuleErrorBuilder::message(sprintf( 'Instanceof between %s and %s will always evaluate to false.', - $expressionType->describe(VerbosityLevel::typeOnly()), + $exprType->describe(VerbosityLevel::typeOnly()), $classType->describe(VerbosityLevel::getRecommendedLevelByType($classType)), - )))->build(), + )))->identifier('instanceof.alwaysFalse')->build(), ]; } elseif ($this->checkAlwaysTrueInstanceof) { - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Instanceof between %s and %s will always evaluate to true.', - $expressionType->describe(VerbosityLevel::typeOnly()), - $classType->describe(VerbosityLevel::getRecommendedLevelByType($classType)), - )))->build(), - ]; + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + return []; + } + + $exprType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->expr) : $scope->getNativeType($node->expr); + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Instanceof between %s and %s will always evaluate to true.', + $exprType->describe(VerbosityLevel::typeOnly()), + $classType->describe(VerbosityLevel::getRecommendedLevelByType($classType)), + ))); + if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier('instanceof.alwaysTrue'); + + return [$errorBuilder->build()]; } return []; diff --git a/src/Rules/Classes/InstantiationCallableRule.php b/src/Rules/Classes/InstantiationCallableRule.php index ed9eae3be8..d36c15a33a 100644 --- a/src/Rules/Classes/InstantiationCallableRule.php +++ b/src/Rules/Classes/InstantiationCallableRule.php @@ -22,7 +22,10 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { return [ - RuleErrorBuilder::message('Cannot create callable from the new operator.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Cannot create callable from the new operator.') + ->identifier('callable.notSupported') + ->nonIgnorable() + ->build(), ]; } diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index ce2ff2920b..7e31e5adcd 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -9,16 +9,14 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use function array_map; use function array_merge; use function count; @@ -34,7 +32,7 @@ class InstantiationRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, private FunctionCallParametersCheck $check, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, ) { } @@ -55,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array /** * @param Node\Expr\New_ $node - * @return RuleError[] + * @return list */ private function checkClassName(string $class, bool $isName, Node $node, Scope $scope): array { @@ -65,7 +63,9 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ if ($lowercasedClass === 'static') { if (!$scope->isInClass()) { return [ - RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $class))->build(), + RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $class)) + ->identifier('outOfClass.static') + ->build(), ]; } @@ -89,14 +89,18 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ } elseif ($lowercasedClass === 'self') { if (!$scope->isInClass()) { return [ - RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $class))->build(), + RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $class)) + ->identifier('outOfClass.self') + ->build(), ]; } $classReflection = $scope->getClassReflection(); } elseif ($lowercasedClass === 'parent') { if (!$scope->isInClass()) { return [ - RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $class))->build(), + RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $class)) + ->identifier('outOfClass.parent') + ->build(), ]; } if ($scope->getClassReflection()->getParentClass() === null) { @@ -106,7 +110,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ $scope->getClassReflection()->getDisplayName(), $scope->getFunctionName(), $scope->getClassReflection()->getDisplayName(), - ))->build(), + ))->identifier('class.noParent')->build(), ]; } $classReflection = $scope->getClassReflection()->getParentClass(); @@ -117,14 +121,17 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ } return [ - RuleErrorBuilder::message(sprintf('Instantiated class %s not found.', $class))->discoveringSymbolsTip()->build(), + RuleErrorBuilder::message(sprintf('Instantiated class %s not found.', $class)) + ->identifier('class.notFound') + ->discoveringSymbolsTip() + ->build(), ]; - } else { - $messages = $this->classCaseSensitivityCheck->checkClassNames([ - new ClassNameNodePair($class, $node->class), - ]); } + $messages = $this->classCheck->checkClassNames([ + new ClassNameNodePair($class, $node->class), + ]); + $classReflection = $this->reflectionProvider->getClass($class); } @@ -132,7 +139,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ return [ RuleErrorBuilder::message( sprintf('Cannot instantiate enum %s.', $classReflection->getDisplayName()), - )->build(), + )->identifier('new.enum')->build(), ]; } @@ -140,7 +147,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ return [ RuleErrorBuilder::message( sprintf('Cannot instantiate interface %s.', $classReflection->getDisplayName()), - )->build(), + )->identifier('new.interface')->build(), ]; } @@ -148,7 +155,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ return [ RuleErrorBuilder::message( sprintf('Instantiated class %s is abstract.', $classReflection->getDisplayName()), - )->build(), + )->identifier('new.abstract')->build(), ]; } @@ -162,7 +169,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ RuleErrorBuilder::message(sprintf( 'Class %s does not have a constructor and must be instantiated without any parameters.', $classReflection->getDisplayName(), - ))->build(), + ))->identifier('new.noConstructor')->build(), ]); } @@ -177,7 +184,9 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ $constructorReflection->isPrivate() ? 'private' : 'protected', $constructorReflection->getDeclaringClass()->getDisplayName(), $constructorReflection->getName(), - ))->build(); + )) + ->identifier(sprintf('new.%sConstructor', $constructorReflection->isPrivate() ? 'private' : 'protected')) + ->build(); } $classDisplayName = SprintfHelper::escapeFormatString($classReflection->getDisplayName()); @@ -187,6 +196,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ $scope, $node->getArgs(), $constructorReflection->getVariants(), + $constructorReflection->getNamedArgumentsVariants(), ), $scope, $constructorReflection->getDeclaringClass()->isBuiltin(), @@ -207,11 +217,12 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ 'Return type of call to ' . $classDisplayName . ' constructor contains unresolvable type.', 'Parameter %s of class ' . $classDisplayName . ' constructor contains unresolvable type.', ], + 'new', )); } /** - * @param Node\Expr\New_ $node $node + * @param Node\Expr\New_ $node * @return array */ private function getClassNames(Node $node, Scope $scope): array @@ -221,12 +232,15 @@ private function getClassNames(Node $node, Scope $scope): array } if ($node->class instanceof Node\Stmt\Class_) { - $anonymousClassType = $scope->getType($node); - if (!$anonymousClassType instanceof TypeWithClassName) { + $classNames = $scope->getType($node)->getObjectClassNames(); + if ($classNames === []) { throw new ShouldNotHappenException(); } - return [[$anonymousClassType->getClassName(), true]]; + return array_map( + static fn (string $className) => [$className, true], + $classNames, + ); } $type = $scope->getType($node->class); @@ -234,11 +248,11 @@ private function getClassNames(Node $node, Scope $scope): array return array_merge( array_map( static fn (ConstantStringType $type): array => [$type->getValue(), true], - TypeUtils::getConstantStrings($type), + $type->getConstantStrings(), ), array_map( static fn (string $name): array => [$name, false], - TypeUtils::getDirectClassNames($type), + $type->getObjectClassNames(), ), ); } diff --git a/src/Rules/Classes/InvalidPromotedPropertiesRule.php b/src/Rules/Classes/InvalidPromotedPropertiesRule.php index b81e387b60..5376f6b17e 100644 --- a/src/Rules/Classes/InvalidPromotedPropertiesRule.php +++ b/src/Rules/Classes/InvalidPromotedPropertiesRule.php @@ -12,7 +12,7 @@ use function sprintf; /** - * @implements Rule + * @implements Rule */ class InvalidPromotedPropertiesRule implements Rule { @@ -23,22 +23,14 @@ public function __construct(private PhpVersion $phpVersion) public function getNodeType(): string { - return Node::class; + return Node\FunctionLike::class; } public function processNode(Node $node, Scope $scope): array { - if ( - !$node instanceof Node\Expr\ArrowFunction - && !$node instanceof Node\Stmt\ClassMethod - && !$node instanceof Node\Expr\Closure - && !$node instanceof Node\Stmt\Function_ - ) { - return []; - } - $hasPromotedProperties = false; - foreach ($node->params as $param) { + + foreach ($node->getParams() as $param) { if ($param->flags === 0) { continue; } @@ -55,31 +47,33 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( 'Promoted properties are supported only on PHP 8.0 and later.', - )->nonIgnorable()->build(), + )->identifier('property.promotedNotSupported')->nonIgnorable()->build(), ]; } if ( !$node instanceof Node\Stmt\ClassMethod - || $node->name->toLowerString() !== '__construct' + || ( + $node->name->toLowerString() !== '__construct' + && $node->getAttribute('originalTraitMethodName') !== '__construct') ) { return [ RuleErrorBuilder::message( 'Promoted properties can be in constructor only.', - )->nonIgnorable()->build(), + )->identifier('property.invalidPromoted')->nonIgnorable()->build(), ]; } - if ($node->stmts === null) { + if ($node->getStmts() === null) { return [ RuleErrorBuilder::message( 'Promoted properties are not allowed in abstract constructors.', - )->nonIgnorable()->build(), + )->identifier('property.invalidPromoted')->nonIgnorable()->build(), ]; } $errors = []; - foreach ($node->params as $param) { + foreach ($node->getParams() as $param) { if ($param->flags === 0) { continue; } @@ -95,8 +89,7 @@ public function processNode(Node $node, Scope $scope): array $propertyName = $param->var->name; $errors[] = RuleErrorBuilder::message( sprintf('Promoted property parameter $%s can not be variadic.', $propertyName), - )->nonIgnorable()->line($param->getLine())->build(); - continue; + )->identifier('property.invalidPromoted')->nonIgnorable()->line($param->getStartLine())->build(); } return $errors; diff --git a/src/Rules/Classes/LocalTypeAliasesCheck.php b/src/Rules/Classes/LocalTypeAliasesCheck.php new file mode 100644 index 0000000000..78940cac12 --- /dev/null +++ b/src/Rules/Classes/LocalTypeAliasesCheck.php @@ -0,0 +1,188 @@ + $globalTypeAliases + */ + public function __construct( + private array $globalTypeAliases, + private ReflectionProvider $reflectionProvider, + private TypeNodeResolver $typeNodeResolver, + ) + { + } + + /** + * @return list + */ + public function check(ClassReflection $reflection): array + { + $phpDoc = $reflection->getResolvedPhpDoc(); + if ($phpDoc === null) { + return []; + } + + $nameScope = $phpDoc->getNullableNameScope(); + $resolveName = static function (string $name) use ($nameScope): string { + if ($nameScope === null) { + return $name; + } + + return $nameScope->resolveStringName($name); + }; + + $errors = []; + $className = $reflection->getName(); + + $importedAliases = []; + + foreach ($phpDoc->getTypeAliasImportTags() as $typeAliasImportTag) { + $aliasName = $typeAliasImportTag->getImportedAs() ?? $typeAliasImportTag->getImportedAlias(); + $importedAlias = $typeAliasImportTag->getImportedAlias(); + $importedFromClassName = $typeAliasImportTag->getImportedFrom(); + + if (!$this->reflectionProvider->hasClass($importedFromClassName)) { + $errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: class %s does not exist.', $importedAlias, $importedFromClassName)) + ->identifier('class.notFound') + ->build(); + continue; + } + + $importedFromReflection = $this->reflectionProvider->getClass($importedFromClassName); + $typeAliases = $importedFromReflection->getTypeAliases(); + + if (!array_key_exists($importedAlias, $typeAliases)) { + $errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: type alias does not exist in %s.', $importedAlias, $importedFromClassName)) + ->identifier('typeAlias.notFound') + ->build(); + continue; + } + + $resolvedName = $resolveName($aliasName); + if ($this->reflectionProvider->hasClass($resolveName($aliasName))) { + $classReflection = $this->reflectionProvider->getClass($resolvedName); + $classLikeDescription = 'a class'; + if ($classReflection->isInterface()) { + $classLikeDescription = 'an interface'; + } elseif ($classReflection->isTrait()) { + $classLikeDescription = 'a trait'; + } elseif ($classReflection->isEnum()) { + $classLikeDescription = 'an enum'; + } + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as %s in scope of %s.', $aliasName, $classLikeDescription, $className)) + ->identifier('typeAlias.duplicate') + ->build(); + continue; + } + + if (array_key_exists($aliasName, $this->globalTypeAliases)) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->identifier('typeAlias.duplicate')->build(); + continue; + } + + $importedAs = $typeAliasImportTag->getImportedAs(); + if ($importedAs !== null && !$this->isAliasNameValid($importedAs, $nameScope)) { + $errors[] = RuleErrorBuilder::message(sprintf('Imported type alias %s has an invalid name: %s.', $importedAlias, $importedAs))->identifier('typeAlias.invalidName')->build(); + continue; + } + + $importedAliases[] = $aliasName; + } + + foreach ($phpDoc->getTypeAliasTags() as $typeAliasTag) { + $aliasName = $typeAliasTag->getAliasName(); + + if (in_array($aliasName, $importedAliases, true)) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s overwrites an imported type alias of the same name.', $aliasName))->identifier('typeAlias.duplicate')->build(); + continue; + } + + $resolvedName = $resolveName($aliasName); + if ($this->reflectionProvider->hasClass($resolvedName)) { + $classReflection = $this->reflectionProvider->getClass($resolvedName); + $classLikeDescription = 'a class'; + if ($classReflection->isInterface()) { + $classLikeDescription = 'an interface'; + } elseif ($classReflection->isTrait()) { + $classLikeDescription = 'a trait'; + } elseif ($classReflection->isEnum()) { + $classLikeDescription = 'an enum'; + } + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as %s in scope of %s.', $aliasName, $classLikeDescription, $className))->identifier('typeAlias.duplicate')->build(); + continue; + } + + if (array_key_exists($aliasName, $this->globalTypeAliases)) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->identifier('typeAlias.duplicate')->build(); + continue; + } + + if (!$this->isAliasNameValid($aliasName, $nameScope)) { + $errors[] = RuleErrorBuilder::message(sprintf('Type alias has an invalid name: %s.', $aliasName)) + ->identifier('typeAlias.invalidName') + ->build(); + continue; + } + + $resolvedType = $typeAliasTag->getTypeAlias()->resolve($this->typeNodeResolver); + $foundError = false; + TypeTraverser::map($resolvedType, static function (Type $type, callable $traverse) use (&$errors, &$foundError, $aliasName): Type { + if ($foundError) { + return $type; + } + + if ($type instanceof CircularTypeAliasErrorType) { + $errors[] = RuleErrorBuilder::message(sprintf('Circular definition detected in type alias %s.', $aliasName)) + ->identifier('typeAlias.circular') + ->build(); + $foundError = true; + return $type; + } + + if ($type instanceof ErrorType) { + $errors[] = RuleErrorBuilder::message(sprintf('Invalid type definition detected in type alias %s.', $aliasName)) + ->identifier('typeAlias.invalidType') + ->build(); + $foundError = true; + return $type; + } + + return $traverse($type); + }); + } + + return $errors; + } + + private function isAliasNameValid(string $aliasName, ?NameScope $nameScope): bool + { + if ($nameScope === null) { + return true; + } + + $aliasNameResolvedType = $this->typeNodeResolver->resolve(new IdentifierTypeNode($aliasName), $nameScope->bypassTypeAliases()); + return ($aliasNameResolvedType->isObject()->yes() && !in_array($aliasName, ['self', 'parent'], true)) + || $aliasNameResolvedType instanceof TemplateType; // aliases take precedence over type parameters, this is reported by other rules using TemplateTypeCheck + } + +} diff --git a/src/Rules/Classes/LocalTypeAliasesRule.php b/src/Rules/Classes/LocalTypeAliasesRule.php index cdc76cdb61..86697971b8 100644 --- a/src/Rules/Classes/LocalTypeAliasesRule.php +++ b/src/Rules/Classes/LocalTypeAliasesRule.php @@ -3,23 +3,9 @@ namespace PHPStan\Rules\Classes; use PhpParser\Node; -use PHPStan\Analyser\NameScope; use PHPStan\Analyser\Scope; use PHPStan\Node\InClassNode; -use PHPStan\PhpDoc\TypeNodeResolver; -use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; -use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\CircularTypeAliasErrorType; -use PHPStan\Type\ErrorType; -use PHPStan\Type\Generic\TemplateType; -use PHPStan\Type\ObjectType; -use PHPStan\Type\Type; -use PHPStan\Type\TypeTraverser; -use function array_key_exists; -use function in_array; -use function sprintf; /** * @implements Rule @@ -27,14 +13,7 @@ class LocalTypeAliasesRule implements Rule { - /** - * @param array $globalTypeAliases - */ - public function __construct( - private array $globalTypeAliases, - private ReflectionProvider $reflectionProvider, - private TypeNodeResolver $typeNodeResolver, - ) + public function __construct(private LocalTypeAliasesCheck $check) { } @@ -45,141 +24,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $reflection = $node->getClassReflection(); - $phpDoc = $reflection->getResolvedPhpDoc(); - if ($phpDoc === null) { - return []; - } - - $nameScope = $phpDoc->getNullableNameScope(); - $resolveName = static function (string $name) use ($nameScope): string { - if ($nameScope === null) { - return $name; - } - - return $nameScope->resolveStringName($name); - }; - - $errors = []; - $className = $reflection->getName(); - - $importedAliases = []; - - foreach ($phpDoc->getTypeAliasImportTags() as $typeAliasImportTag) { - $aliasName = $typeAliasImportTag->getImportedAs() ?? $typeAliasImportTag->getImportedAlias(); - $importedAlias = $typeAliasImportTag->getImportedAlias(); - $importedFromClassName = $typeAliasImportTag->getImportedFrom(); - - if (!$this->reflectionProvider->hasClass($importedFromClassName)) { - $errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: class %s does not exist.', $importedAlias, $importedFromClassName))->build(); - continue; - } - - $importedFromReflection = $this->reflectionProvider->getClass($importedFromClassName); - $typeAliases = $importedFromReflection->getTypeAliases(); - - if (!array_key_exists($importedAlias, $typeAliases)) { - $errors[] = RuleErrorBuilder::message(sprintf('Cannot import type alias %s: type alias does not exist in %s.', $importedAlias, $importedFromClassName))->build(); - continue; - } - - $resolvedName = $resolveName($aliasName); - if ($this->reflectionProvider->hasClass($resolveName($aliasName))) { - $classReflection = $this->reflectionProvider->getClass($resolvedName); - $classLikeDescription = 'a class'; - if ($classReflection->isInterface()) { - $classLikeDescription = 'an interface'; - } elseif ($classReflection->isTrait()) { - $classLikeDescription = 'a trait'; - } elseif ($classReflection->isEnum()) { - $classLikeDescription = 'an enum'; - } - $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as %s in scope of %s.', $aliasName, $classLikeDescription, $className))->build(); - continue; - } - - if (array_key_exists($aliasName, $this->globalTypeAliases)) { - $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->build(); - continue; - } - - $importedAs = $typeAliasImportTag->getImportedAs(); - if ($importedAs !== null && !$this->isAliasNameValid($importedAs, $nameScope)) { - $errors[] = RuleErrorBuilder::message(sprintf('Imported type alias %s has an invalid name: %s.', $importedAlias, $importedAs))->build(); - continue; - } - - $importedAliases[] = $aliasName; - } - - foreach ($phpDoc->getTypeAliasTags() as $typeAliasTag) { - $aliasName = $typeAliasTag->getAliasName(); - - if (in_array($aliasName, $importedAliases, true)) { - $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s overwrites an imported type alias of the same name.', $aliasName))->build(); - continue; - } - - $resolvedName = $resolveName($aliasName); - if ($this->reflectionProvider->hasClass($resolvedName)) { - $classReflection = $this->reflectionProvider->getClass($resolvedName); - $classLikeDescription = 'a class'; - if ($classReflection->isInterface()) { - $classLikeDescription = 'an interface'; - } elseif ($classReflection->isTrait()) { - $classLikeDescription = 'a trait'; - } elseif ($classReflection->isEnum()) { - $classLikeDescription = 'an enum'; - } - $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as %s in scope of %s.', $aliasName, $classLikeDescription, $className))->build(); - continue; - } - - if (array_key_exists($aliasName, $this->globalTypeAliases)) { - $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a global type alias.', $aliasName))->build(); - continue; - } - - if (!$this->isAliasNameValid($aliasName, $nameScope)) { - $errors[] = RuleErrorBuilder::message(sprintf('Type alias has an invalid name: %s.', $aliasName))->build(); - continue; - } - - $resolvedType = $typeAliasTag->getTypeAlias()->resolve($this->typeNodeResolver); - $foundError = false; - TypeTraverser::map($resolvedType, static function (Type $type, callable $traverse) use (&$errors, &$foundError, $aliasName): Type { - if ($foundError) { - return $type; - } - - if ($type instanceof CircularTypeAliasErrorType) { - $errors[] = RuleErrorBuilder::message(sprintf('Circular definition detected in type alias %s.', $aliasName))->build(); - $foundError = true; - return $type; - } - - if ($type instanceof ErrorType) { - $errors[] = RuleErrorBuilder::message(sprintf('Invalid type definition detected in type alias %s.', $aliasName))->build(); - $foundError = true; - return $type; - } - - return $traverse($type); - }); - } - - return $errors; - } - - private function isAliasNameValid(string $aliasName, ?NameScope $nameScope): bool - { - if ($nameScope === null) { - return true; - } - - $aliasNameResolvedType = $this->typeNodeResolver->resolve(new IdentifierTypeNode($aliasName), $nameScope->bypassTypeAliases()); - return ($aliasNameResolvedType instanceof ObjectType && !in_array($aliasName, ['self', 'parent'], true)) - || $aliasNameResolvedType instanceof TemplateType; // aliases take precedence over type parameters, this is reported by other rules using TemplateTypeCheck + return $this->check->check($node->getClassReflection()); } } diff --git a/src/Rules/Classes/LocalTypeTraitAliasesRule.php b/src/Rules/Classes/LocalTypeTraitAliasesRule.php new file mode 100644 index 0000000000..406108db1b --- /dev/null +++ b/src/Rules/Classes/LocalTypeTraitAliasesRule.php @@ -0,0 +1,39 @@ + + */ +class LocalTypeTraitAliasesRule implements Rule +{ + + public function __construct(private LocalTypeAliasesCheck $check, private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Trait_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $traitName = $node->namespacedName; + if ($traitName === null) { + return []; + } + + if (!$this->reflectionProvider->hasClass($traitName->toString())) { + return []; + } + + return $this->check->check($this->reflectionProvider->getClass($traitName->toString())); + } + +} diff --git a/src/Rules/Classes/MixinRule.php b/src/Rules/Classes/MixinRule.php index 93d0f4bfc4..0bf9fa24da 100644 --- a/src/Rules/Classes/MixinRule.php +++ b/src/Rules/Classes/MixinRule.php @@ -6,7 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InClassNode; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\MissingTypehintCheck; @@ -26,7 +26,7 @@ class MixinRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private GenericObjectTypeCheck $genericObjectTypeCheck, private MissingTypehintCheck $missingTypehintCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, @@ -42,23 +42,24 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - return []; - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); $mixinTags = $classReflection->getMixinTags(); $errors = []; foreach ($mixinTags as $mixinTag) { $type = $mixinTag->getType(); if (!$type->canCallMethods()->yes() || !$type->canAccessProperties()->yes()) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly())))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly()))) + ->identifier('mixin.nonObject') + ->build(); continue; } if ( $this->unresolvableTypeHelper->containsUnresolvableType($type) ) { - $errors[] = RuleErrorBuilder::message('PHPDoc tag @mixin contains unresolvable type.')->build(); + $errors[] = RuleErrorBuilder::message('PHPDoc tag @mixin contains unresolvable type.') + ->identifier('mixin.unresolvableType') + ->build(); continue; } @@ -68,6 +69,8 @@ public function processNode(Node $node, Scope $scope): array 'Generic type %s in PHPDoc tag @mixin does not specify all template types of %s %s: %s', 'Generic type %s in PHPDoc tag @mixin specifies %d template types, but %s %s supports only %d: %s', 'Type %s in generic type %s in PHPDoc tag @mixin is not subtype of template type %s of %s %s.', + 'Call-site variance of %s in generic type %s in PHPDoc tag @mixin is in conflict with %s template type %s of %s %s.', + 'Call-site variance of %s in generic type %s in PHPDoc tag @mixin is redundant, template type %s of %s %s has the same variance.', )); foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($type) as [$innerName, $genericTypeNames]) { @@ -75,20 +78,28 @@ public function processNode(Node $node, Scope $scope): array 'PHPDoc tag @mixin contains generic %s but does not specify its types: %s', $innerName, implode(', ', $genericTypeNames), - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + )) + ->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP) + ->identifier('missingType.generics') + ->build(); } foreach ($type->getReferencedClasses() as $class) { if (!$this->reflectionProvider->hasClass($class)) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains unknown class %s.', $class))->discoveringSymbolsTip()->build(); + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains unknown class %s.', $class)) + ->identifier('class.notFound') + ->discoveringSymbolsTip() + ->build(); } elseif ($this->reflectionProvider->getClass($class)->isTrait()) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains invalid type %s.', $class))->build(); - } elseif ($this->checkClassCaseSensitivity) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains invalid type %s.', $class)) + ->identifier('mixin.trait') + ->build(); + } else { $errors = array_merge( $errors, - $this->classCaseSensitivityCheck->checkClassNames([ + $this->classCheck->checkClassNames([ new ClassNameNodePair($class, $node), - ]), + ], $this->checkClassCaseSensitivity), ); } } diff --git a/src/Rules/Classes/NewStaticRule.php b/src/Rules/Classes/NewStaticRule.php index 588fccf527..b675469bac 100644 --- a/src/Rules/Classes/NewStaticRule.php +++ b/src/Rules/Classes/NewStaticRule.php @@ -41,6 +41,7 @@ public function processNode(Node $node, Scope $scope): array $messages = [ RuleErrorBuilder::message('Unsafe usage of new static().') + ->identifier('new.static') ->tip('See: https://phpstan.org/blog/solving-phpstan-error-unsafe-usage-of-new-static') ->build(), ]; diff --git a/src/Rules/Classes/NonClassAttributeClassRule.php b/src/Rules/Classes/NonClassAttributeClassRule.php index 0138692224..c6318d07a4 100644 --- a/src/Rules/Classes/NonClassAttributeClassRule.php +++ b/src/Rules/Classes/NonClassAttributeClassRule.php @@ -5,11 +5,12 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\InClassNode; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; use function sprintf; +use function strtolower; /** * @implements Rule @@ -38,7 +39,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ private function check(Scope $scope): array { @@ -50,13 +51,17 @@ private function check(Scope $scope): array return [ RuleErrorBuilder::message(sprintf( '%s cannot be an Attribute class.', - $classReflection->isInterface() ? 'Interface' : 'Enum', - ))->build(), + $classReflection->getClassTypeDescription(), + )) + ->identifier(sprintf('attribute.%s', strtolower($classReflection->getClassTypeDescription()))) + ->build(), ]; } if ($classReflection->isAbstract()) { return [ - RuleErrorBuilder::message(sprintf('Abstract class %s cannot be an Attribute class.', $classReflection->getDisplayName()))->build(), + RuleErrorBuilder::message(sprintf('Abstract class %s cannot be an Attribute class.', $classReflection->getDisplayName())) + ->identifier('attribute.abstract') + ->build(), ]; } @@ -66,7 +71,9 @@ private function check(Scope $scope): array if (!$classReflection->getConstructor()->isPublic()) { return [ - RuleErrorBuilder::message(sprintf('Attribute class %s constructor must be public.', $classReflection->getDisplayName()))->build(), + RuleErrorBuilder::message(sprintf('Attribute class %s constructor must be public.', $classReflection->getDisplayName())) + ->identifier('attribute.constructorNotPublic') + ->build(), ]; } diff --git a/src/Rules/Classes/ReadOnlyClassRule.php b/src/Rules/Classes/ReadOnlyClassRule.php new file mode 100644 index 0000000000..21755c623c --- /dev/null +++ b/src/Rules/Classes/ReadOnlyClassRule.php @@ -0,0 +1,58 @@ + + */ +class ReadOnlyClassRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + if (!$classReflection->isReadOnly()) { + return []; + } + if ($classReflection->isAnonymous()) { + if ($this->phpVersion->supportsReadOnlyAnonymousClasses()) { + return []; + } + + return [ + RuleErrorBuilder::message('Anonymous readonly classes are supported only on PHP 8.3 and later.') + ->identifier('classConstant.nativeTypeNotSupported') + ->nonIgnorable() + ->build(), + ]; + } + + if ($this->phpVersion->supportsReadOnlyClasses()) { + return []; + } + + return [ + RuleErrorBuilder::message('Readonly classes are supported only on PHP 8.2 and later.') + ->identifier('classConstant.nativeTypeNotSupported') + ->nonIgnorable() + ->build(), + ]; + } + +} diff --git a/src/Rules/Classes/RequireExtendsRule.php b/src/Rules/Classes/RequireExtendsRule.php new file mode 100644 index 0000000000..34a35ab11f --- /dev/null +++ b/src/Rules/Classes/RequireExtendsRule.php @@ -0,0 +1,87 @@ + + */ +class RequireExtendsRule implements Rule +{ + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + + if ($classReflection->isInterface()) { + return []; + } + + $errors = []; + foreach ($classReflection->getInterfaces() as $interface) { + $extendsTags = $interface->getRequireExtendsTags(); + foreach ($extendsTags as $extendsTag) { + $type = $extendsTag->getType(); + if (!$type instanceof ObjectType) { + continue; + } + + if ($classReflection->is($type->getClassName())) { + continue; + } + + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Interface %s requires implementing class to extend %s, but %s does not.', + $interface->getDisplayName(), + $type->describe(VerbosityLevel::typeOnly()), + $classReflection->getDisplayName(), + ), + ) + ->identifier('class.missingExtends') + ->build(); + } + } + + foreach ($classReflection->getTraits(true) as $trait) { + $extendsTags = $trait->getRequireExtendsTags(); + foreach ($extendsTags as $extendsTag) { + $type = $extendsTag->getType(); + if (!$type instanceof ObjectType) { + continue; + } + + if ($classReflection->is($type->getClassName())) { + continue; + } + + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Trait %s requires using class to extend %s, but %s does not.', + $trait->getDisplayName(), + $type->describe(VerbosityLevel::typeOnly()), + $classReflection->getDisplayName(), + ), + ) + ->identifier('class.missingExtends') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Classes/RequireImplementsRule.php b/src/Rules/Classes/RequireImplementsRule.php new file mode 100644 index 0000000000..32c9361d65 --- /dev/null +++ b/src/Rules/Classes/RequireImplementsRule.php @@ -0,0 +1,58 @@ + + */ +class RequireImplementsRule implements Rule +{ + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + + $errors = []; + foreach ($classReflection->getTraits(true) as $trait) { + $implementsTags = $trait->getRequireImplementsTags(); + foreach ($implementsTags as $implementsTag) { + $type = $implementsTag->getType(); + if (!$type instanceof ObjectType) { + continue; + } + + if ($classReflection->implementsInterface($type->getClassName())) { + continue; + } + + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Trait %s requires using class to implement %s, but %s does not.', + $trait->getDisplayName(), + $type->describe(VerbosityLevel::typeOnly()), + $classReflection->getDisplayName(), + ), + ) + ->identifier('class.missingImplements') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Classes/TraitAttributeClassRule.php b/src/Rules/Classes/TraitAttributeClassRule.php index 454d063a3a..4d69f3a7ba 100644 --- a/src/Rules/Classes/TraitAttributeClassRule.php +++ b/src/Rules/Classes/TraitAttributeClassRule.php @@ -25,7 +25,9 @@ public function processNode(Node $node, Scope $scope): array $name = $attr->name->toLowerString(); if ($name === 'attribute') { return [ - RuleErrorBuilder::message('Trait cannot be an Attribute class.')->build(), + RuleErrorBuilder::message('Trait cannot be an Attribute class.') + ->identifier('attribute.trait') + ->build(), ]; } } diff --git a/src/Rules/Classes/UnusedConstructorParametersRule.php b/src/Rules/Classes/UnusedConstructorParametersRule.php index a6383e9f23..2850e2bafe 100644 --- a/src/Rules/Classes/UnusedConstructorParametersRule.php +++ b/src/Rules/Classes/UnusedConstructorParametersRule.php @@ -8,7 +8,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassMethodNode; -use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\UnusedFunctionParametersCheck; use PHPStan\ShouldNotHappenException; @@ -37,15 +36,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - - $method = $scope->getFunction(); - if (!$method instanceof MethodReflection) { - return []; - } - + $method = $node->getMethodReflection(); $originalNode = $node->getOriginalNode(); if (strtolower($method->getName()) !== '__construct' || $originalNode->stmts === null) { return []; @@ -57,9 +48,9 @@ public function processNode(Node $node, Scope $scope): array $message = sprintf( 'Constructor of class %s has an unused parameter $%%s.', - SprintfHelper::escapeFormatString($scope->getClassReflection()->getDisplayName()), + SprintfHelper::escapeFormatString($node->getClassReflection()->getDisplayName()), ); - if ($scope->getClassReflection()->isAnonymous()) { + if ($node->getClassReflection()->isAnonymous()) { $message = 'Constructor of an anonymous class has an unused parameter $%s.'; } @@ -74,7 +65,6 @@ public function processNode(Node $node, Scope $scope): array $originalNode->stmts, $message, 'constructor.unusedParameter', - [], ); } diff --git a/src/Rules/Comparison/BooleanAndConstantConditionRule.php b/src/Rules/Comparison/BooleanAndConstantConditionRule.php index adfe60e25c..44c574fb68 100644 --- a/src/Rules/Comparison/BooleanAndConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanAndConstantConditionRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\BooleanAndNode; +use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; @@ -20,6 +21,8 @@ class BooleanAndConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private bool $treatPhpDocTypesAsCertain, + private bool $bleedingEdge, + private bool $reportAlwaysTrueInLastCondition, ) { } @@ -36,10 +39,11 @@ public function processNode( { $errors = []; $originalNode = $node->getOriginalNode(); + $nodeText = $this->bleedingEdge ? $originalNode->getOperatorSigil() : '&&'; $leftType = $this->helper->getBooleanType($scope, $originalNode->left); - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $identifierType = $originalNode instanceof Node\Expr\BinaryOp\BooleanAnd ? 'booleanAnd' : 'logicalAnd'; if ($leftType instanceof ConstantBooleanType) { - $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $tipText, $originalNode): RuleErrorBuilder { + $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } @@ -49,12 +53,23 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - $errors[] = $addTipLeft(RuleErrorBuilder::message(sprintf( - 'Left side of && is always %s.', - $leftType->getValue() ? 'true' : 'false', - )))->line($originalNode->left->getLine())->build(); + + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$leftType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTipLeft(RuleErrorBuilder::message(sprintf( + 'Left side of %s is always %s.', + $nodeText, + $leftType->getValue() ? 'true' : 'false', + ))) + ->identifier(sprintf('%s.leftAlways%s', $identifierType, $leftType->getValue() ? 'True' : 'False')) + ->line($originalNode->left->getStartLine()); + if ($leftType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $errors[] = $errorBuilder->build(); + } } $rightScope = $node->getRightScope(); @@ -62,52 +77,69 @@ public function processNode( $rightScope, $originalNode->right, ); - if ($rightType instanceof ConstantBooleanType) { - $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($rightScope, $originalNode, $tipText): RuleErrorBuilder { + if ($rightType instanceof ConstantBooleanType && !$scope->isInFirstLevelStatement()) { + $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($rightScope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } $booleanNativeType = $this->helper->getNativeBooleanType( - $rightScope->doNotTreatPhpDocTypesAsCertain(), + $rightScope, $originalNode->right, ); if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - if (!$scope->isInFirstLevelStatement()) { - $errors[] = $addTipRight(RuleErrorBuilder::message(sprintf( - 'Right side of && is always %s.', + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$rightType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTipRight(RuleErrorBuilder::message(sprintf( + 'Right side of %s is always %s.', + $nodeText, $rightType->getValue() ? 'true' : 'false', - )))->line($originalNode->right->getLine())->build(); + ))) + ->identifier(sprintf('%s.rightAlways%s', $identifierType, $rightType->getValue() ? 'True' : 'False')) + ->line($originalNode->right->getStartLine()); + if ($rightType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $errors[] = $errorBuilder->build(); } } - if (count($errors) === 0) { - $nodeType = $scope->getType($originalNode); + if (count($errors) === 0 && !$scope->isInFirstLevelStatement()) { + $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($originalNode) : $scope->getNativeType($originalNode); if ($nodeType instanceof ConstantBooleanType) { - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode, $tipText): RuleErrorBuilder { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } - $booleanNativeType = $scope->doNotTreatPhpDocTypesAsCertain()->getType($originalNode); + $booleanNativeType = $scope->getNativeType($originalNode); if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - if (!$scope->isInFirstLevelStatement()) { - $errors[] = $addTip(RuleErrorBuilder::message(sprintf( - 'Result of && is always %s.', + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$nodeType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Result of %s is always %s.', + $nodeText, $nodeType->getValue() ? 'true' : 'false', - )))->build(); + ))); + if ($nodeType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier(sprintf('%s.always%s', $identifierType, $nodeType->getValue() ? 'True' : 'False')); + + $errors[] = $errorBuilder->build(); } } } diff --git a/src/Rules/Comparison/BooleanNotConstantConditionRule.php b/src/Rules/Comparison/BooleanNotConstantConditionRule.php index 9a8ca4ae24..d41cb743ca 100644 --- a/src/Rules/Comparison/BooleanNotConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanNotConstantConditionRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; @@ -18,6 +19,7 @@ class BooleanNotConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, ) { } @@ -44,15 +46,25 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - return [ - $addTip(RuleErrorBuilder::message(sprintf( + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($exprType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( 'Negated boolean expression is always %s.', $exprType->getValue() ? 'false' : 'true', - )))->line($node->expr->getLine())->build(), - ]; + )))->line($node->expr->getStartLine()); + if (!$exprType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier(sprintf('booleanNot.always%s', $exprType->getValue() ? 'False' : 'True')); + + return [ + $errorBuilder->build(), + ]; + } } return []; diff --git a/src/Rules/Comparison/BooleanOrConstantConditionRule.php b/src/Rules/Comparison/BooleanOrConstantConditionRule.php index 2720b2b553..831829355c 100644 --- a/src/Rules/Comparison/BooleanOrConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanOrConstantConditionRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\BooleanOrNode; +use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; @@ -20,6 +21,8 @@ class BooleanOrConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private bool $treatPhpDocTypesAsCertain, + private bool $bleedingEdge, + private bool $reportAlwaysTrueInLastCondition, ) { } @@ -35,11 +38,12 @@ public function processNode( ): array { $originalNode = $node->getOriginalNode(); + $nodeText = $this->bleedingEdge ? $originalNode->getOperatorSigil() : '||'; $messages = []; $leftType = $this->helper->getBooleanType($scope, $originalNode->left); - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $identifierType = $originalNode instanceof Node\Expr\BinaryOp\BooleanOr ? 'booleanOr' : 'logicalOr'; if ($leftType instanceof ConstantBooleanType) { - $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode, $tipText): RuleErrorBuilder { + $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } @@ -49,12 +53,23 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - $messages[] = $addTipLeft(RuleErrorBuilder::message(sprintf( - 'Left side of || is always %s.', - $leftType->getValue() ? 'true' : 'false', - )))->line($originalNode->left->getLine())->build(); + + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$leftType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTipLeft(RuleErrorBuilder::message(sprintf( + 'Left side of %s is always %s.', + $nodeText, + $leftType->getValue() ? 'true' : 'false', + ))) + ->identifier(sprintf('%s.leftAlways%s', $identifierType, $leftType->getValue() ? 'True' : 'False')) + ->line($originalNode->left->getStartLine()); + if ($leftType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $messages[] = $errorBuilder->build(); + } } $rightScope = $node->getRightScope(); @@ -62,52 +77,69 @@ public function processNode( $rightScope, $originalNode->right, ); - if ($rightType instanceof ConstantBooleanType) { - $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($rightScope, $originalNode, $tipText): RuleErrorBuilder { + if ($rightType instanceof ConstantBooleanType && !$scope->isInFirstLevelStatement()) { + $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($rightScope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } $booleanNativeType = $this->helper->getNativeBooleanType( - $rightScope->doNotTreatPhpDocTypesAsCertain(), + $rightScope, $originalNode->right, ); if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - if (!$scope->isInFirstLevelStatement()) { - $messages[] = $addTipRight(RuleErrorBuilder::message(sprintf( - 'Right side of || is always %s.', + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$rightType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTipRight(RuleErrorBuilder::message(sprintf( + 'Right side of %s is always %s.', + $nodeText, $rightType->getValue() ? 'true' : 'false', - )))->line($originalNode->right->getLine())->build(); + ))) + ->identifier(sprintf('%s.rightAlways%s', $identifierType, $rightType->getValue() ? 'True' : 'False')) + ->line($originalNode->right->getStartLine()); + if ($rightType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $messages[] = $errorBuilder->build(); } } - if (count($messages) === 0) { - $nodeType = $scope->getType($originalNode); + if (count($messages) === 0 && !$scope->isInFirstLevelStatement()) { + $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($originalNode) : $scope->getNativeType($originalNode); if ($nodeType instanceof ConstantBooleanType) { - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode, $tipText): RuleErrorBuilder { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } - $booleanNativeType = $scope->doNotTreatPhpDocTypesAsCertain()->getType($originalNode); + $booleanNativeType = $scope->getNativeType($originalNode); if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip($tipText); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - if (!$scope->isInFirstLevelStatement()) { - $messages[] = $addTip(RuleErrorBuilder::message(sprintf( - 'Result of || is always %s.', + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$nodeType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Result of %s is always %s.', + $nodeText, $nodeType->getValue() ? 'true' : 'false', - )))->build(); + ))); + if ($nodeType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier(sprintf('%s.always%s', $identifierType, $nodeType->getValue() ? 'True' : 'False')); + + $messages[] = $errorBuilder->build(); } } } diff --git a/src/Rules/Comparison/ConstantConditionRuleHelper.php b/src/Rules/Comparison/ConstantConditionRuleHelper.php index fd22403af1..60da2b56ff 100644 --- a/src/Rules/Comparison/ConstantConditionRuleHelper.php +++ b/src/Rules/Comparison/ConstantConditionRuleHelper.php @@ -14,6 +14,7 @@ class ConstantConditionRuleHelper public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, private bool $treatPhpDocTypesAsCertain, + private bool $looseComparisonRuleEnabled, ) { } @@ -30,6 +31,15 @@ public function shouldReportAlwaysTrueByDefault(Expr $expr): bool public function shouldSkip(Scope $scope, Expr $expr): bool { + if ( + $this->looseComparisonRuleEnabled + && ($expr instanceof Expr\BinaryOp\Equal + || $expr instanceof Expr\BinaryOp\NotEqual + ) + ) { + return true; + } + if ( $expr instanceof Expr\Instanceof_ || $expr instanceof Expr\BinaryOp\Identical diff --git a/src/Rules/Comparison/ConstantLooseComparisonRule.php b/src/Rules/Comparison/ConstantLooseComparisonRule.php index 78766f823f..5222647d17 100644 --- a/src/Rules/Comparison/ConstantLooseComparisonRule.php +++ b/src/Rules/Comparison/ConstantLooseComparisonRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; @@ -16,7 +17,11 @@ class ConstantLooseComparisonRule implements Rule { - public function __construct(private bool $checkAlwaysTrueLooseComparison) + public function __construct( + private bool $checkAlwaysTrueLooseComparison, + private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + ) { } @@ -31,32 +36,52 @@ public function processNode(Node $node, Scope $scope): array return []; } - $nodeType = $scope->getType($node); + $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); if (!$nodeType instanceof ConstantBooleanType) { return []; } - $leftType = $scope->getType($node->left); - $rightType = $scope->getType($node->right); + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $instanceofTypeWithoutPhpDocs = $scope->getNativeType($node); + if ($instanceofTypeWithoutPhpDocs instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + }; if (!$nodeType->getValue()) { return [ - RuleErrorBuilder::message(sprintf( + $addTip(RuleErrorBuilder::message(sprintf( 'Loose comparison using %s between %s and %s will always evaluate to false.', - $node instanceof Node\Expr\BinaryOp\Equal ? '==' : '!=', - $leftType->describe(VerbosityLevel::value()), - $rightType->describe(VerbosityLevel::value()), - ))->build(), + $node->getOperatorSigil(), + $scope->getType($node->left)->describe(VerbosityLevel::value()), + $scope->getType($node->right)->describe(VerbosityLevel::value()), + )))->identifier(sprintf('%s.alwaysFalse', $node instanceof Node\Expr\BinaryOp\Equal ? 'equal' : 'notEqual'))->build(), ]; } elseif ($this->checkAlwaysTrueLooseComparison) { - return [ - RuleErrorBuilder::message(sprintf( - 'Loose comparison using %s between %s and %s will always evaluate to true.', - $node instanceof Node\Expr\BinaryOp\Equal ? '==' : '!=', - $leftType->describe(VerbosityLevel::value()), - $rightType->describe(VerbosityLevel::value()), - ))->build(), - ]; + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + return []; + } + + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Loose comparison using %s between %s and %s will always evaluate to true.', + $node->getOperatorSigil(), + $scope->getType($node->left)->describe(VerbosityLevel::value()), + $scope->getType($node->right)->describe(VerbosityLevel::value()), + ))); + if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier(sprintf('%s.alwaysTrue', $node instanceof Node\Expr\BinaryOp\Equal ? 'equal' : 'notEqual')); + + return [$errorBuilder->build()]; } return []; diff --git a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php index 9683f4c608..6074f8d20c 100644 --- a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php +++ b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php @@ -71,14 +71,17 @@ public function processNode(Node $node, Scope $scope): array return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ $addTip(RuleErrorBuilder::message(sprintf( 'Do-while loop condition is always %s.', $exprType->getValue() ? 'true' : 'false', - )))->line($node->getCond()->getLine())->build(), + ))) + ->line($node->getCond()->getStartLine()) + ->identifier(sprintf('doWhile.always%s', $exprType->getValue() ? 'True' : 'False')) + ->build(), ]; } diff --git a/src/Rules/Comparison/ElseIfConstantConditionRule.php b/src/Rules/Comparison/ElseIfConstantConditionRule.php index 31df891540..d1bf93a6db 100644 --- a/src/Rules/Comparison/ElseIfConstantConditionRule.php +++ b/src/Rules/Comparison/ElseIfConstantConditionRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; @@ -18,6 +19,7 @@ class ElseIfConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, ) { } @@ -44,21 +46,24 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; - return [ - $addTip(RuleErrorBuilder::message(sprintf( + + $isLast = $node->cond->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$exprType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( 'Elseif condition is always %s.', $exprType->getValue() ? 'true' : 'false', - )))->line($node->cond->getLine()) - ->identifier('deadCode.elseifConstantCondition') - ->metadata([ - 'depth' => $node->getAttribute('statementDepth'), - 'order' => $node->getAttribute('statementOrder'), - 'value' => $exprType->getValue(), - ]) - ->build(), - ]; + )))->line($node->cond->getStartLine()); + + if ($exprType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier(sprintf('elseif.always%s', $exprType->getValue() ? 'True' : 'False')); + + return [$errorBuilder->build()]; + } } return []; diff --git a/src/Rules/Comparison/IfConstantConditionRule.php b/src/Rules/Comparison/IfConstantConditionRule.php index 0de33d796b..8bf57ebcd4 100644 --- a/src/Rules/Comparison/IfConstantConditionRule.php +++ b/src/Rules/Comparison/IfConstantConditionRule.php @@ -44,21 +44,16 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ $addTip(RuleErrorBuilder::message(sprintf( 'If condition is always %s.', $exprType->getValue() ? 'true' : 'false', - )))->line($node->cond->getLine()) - ->identifier('deadCode.ifConstantCondition') - ->metadata([ - 'depth' => $node->getAttribute('statementDepth'), - 'order' => $node->getAttribute('statementOrder'), - 'value' => $exprType->getValue(), - ]) - ->build(), + ))) + ->identifier(sprintf('if.always%s', $exprType->getValue() ? 'True' : 'False')) + ->line($node->cond->getStartLine())->build(), ]; } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php index e60656e751..55423dbf50 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; @@ -19,6 +20,7 @@ public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, private bool $checkAlwaysTrueCheckTypeFunctionCall, private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, ) { } @@ -53,7 +55,7 @@ public function processNode(Node $node, Scope $scope): array return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; if (!$isAlways) { @@ -62,16 +64,26 @@ public function processNode(Node $node, Scope $scope): array 'Call to function %s()%s will always evaluate to false.', $functionName, $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), - )))->build(), + )))->identifier('function.impossibleType')->build(), ]; } elseif ($this->checkAlwaysTrueCheckTypeFunctionCall) { - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Call to function %s()%s will always evaluate to true.', - $functionName, - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), - )))->build(), - ]; + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + return []; + } + + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Call to function %s()%s will always evaluate to true.', + $functionName, + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + ))); + if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier('function.alreadyNarrowedType'); + + return [$errorBuilder->build()]; } return []; diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 9bd7c0ec6a..9463072340 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -7,20 +7,26 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; +use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function array_map; use function array_pop; @@ -42,6 +48,7 @@ public function __construct( private TypeSpecifier $typeSpecifier, private array $universalObjectCratesClasses, private bool $treatPhpDocTypesAsCertain, + private bool $nullContextForVoidReturningFunctions, ) { } @@ -52,11 +59,15 @@ public function findSpecifiedType( ): ?bool { if ($node instanceof FuncCall) { + if ($node->isFirstClassCallable()) { + return null; + } $argsCount = count($node->getArgs()); if ($node->name instanceof Node\Name) { $functionName = strtolower((string) $node->name); if ($functionName === 'assert' && $argsCount >= 1) { - $assertValue = $scope->getType($node->getArgs()[0]->value)->toBoolean(); + $arg = $node->getArgs()[0]->value; + $assertValue = ($this->treatPhpDocTypesAsCertain ? $scope->getType($arg) : $scope->getNativeType($arg))->toBoolean(); if (!$assertValue instanceof ConstantBooleanType) { return null; } @@ -78,7 +89,8 @@ public function findSpecifiedType( } elseif ($functionName === 'array_search') { return null; } elseif ($functionName === 'in_array' && $argsCount >= 3) { - $haystackType = $scope->getType($node->getArgs()[1]->value); + $haystackArg = $node->getArgs()[1]->value; + $haystackType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($haystackArg) : $scope->getNativeType($haystackArg)); if ($haystackType instanceof MixedType) { return null; } @@ -87,14 +99,15 @@ public function findSpecifiedType( return null; } - $constantArrays = TypeUtils::getOldConstantArrays($haystackType); - $needleType = $scope->getType($node->getArgs()[0]->value); + $needleArg = $node->getArgs()[0]->value; + $needleType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($needleArg) : $scope->getNativeType($needleArg)); $valueType = $haystackType->getIterableValueType(); - $constantNeedleTypesCount = count(TypeUtils::getConstantScalars($needleType)); - $constantHaystackTypesCount = count(TypeUtils::getConstantScalars($valueType)); + $constantNeedleTypesCount = count($needleType->getFiniteTypes()); + $constantHaystackTypesCount = count($valueType->getFiniteTypes()); $isNeedleSupertype = $needleType->isSuperTypeOf($valueType); - if (count($constantArrays) === 0) { + if ($haystackType->isConstantArray()->no()) { if ($haystackType->isIterableAtLeastOnce()->yes()) { + // In this case the generic implementation via typeSpecifier fails, because the argument types cannot be narrowed down. if ($constantNeedleTypesCount === 1 && $constantHaystackTypesCount === 1) { if ($isNeedleSupertype->yes()) { return true; @@ -103,21 +116,36 @@ public function findSpecifiedType( return false; } } + + return null; } - return null; } if (!$haystackType instanceof ConstantArrayType || count($haystackType->getValueTypes()) > 0) { - $haystackArrayTypes = TypeUtils::getArrays($haystackType); + $haystackArrayTypes = $haystackType->getArrays(); if (count($haystackArrayTypes) === 1 && $haystackArrayTypes[0]->getIterableValueType() instanceof NeverType) { return null; } if ($isNeedleSupertype->maybe() || $isNeedleSupertype->yes()) { foreach ($haystackArrayTypes as $haystackArrayType) { - foreach (TypeUtils::getConstantScalars($haystackArrayType->getIterableValueType()) as $constantScalarType) { - if ($constantScalarType->isSuperTypeOf($needleType)->yes()) { - continue 2; + if ($haystackArrayType instanceof ConstantArrayType) { + foreach ($haystackArrayType->getValueTypes() as $i => $haystackArrayValueType) { + if ($haystackArrayType->isOptionalKey($i)) { + continue; + } + + foreach ($haystackArrayValueType->getConstantScalarTypes() as $constantScalarType) { + if ($constantScalarType->isSuperTypeOf($needleType)->yes()) { + continue 3; + } + } + } + } else { + foreach ($haystackArrayType->getIterableValueType()->getConstantScalarTypes() as $constantScalarType) { + if ($constantScalarType->isSuperTypeOf($needleType)->yes()) { + continue 2; + } } } @@ -137,8 +165,8 @@ public function findSpecifiedType( } } } elseif ($functionName === 'method_exists' && $argsCount >= 2) { - $objectType = $scope->getType($node->getArgs()[0]->value); - $methodType = $scope->getType($node->getArgs()[1]->value); + $objectArg = $node->getArgs()[0]->value; + $objectType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($objectArg) : $scope->getNativeType($objectArg)); if ($objectType instanceof ConstantStringType && !$this->reflectionProvider->hasClass($objectType->getValue()) @@ -146,12 +174,15 @@ public function findSpecifiedType( return false; } + $methodArg = $node->getArgs()[1]->value; + $methodType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($methodArg) : $scope->getNativeType($methodArg)); + if ($methodType instanceof ConstantStringType) { if ($objectType instanceof ConstantStringType) { $objectType = new ObjectType($objectType->getValue()); } - if ($objectType instanceof TypeWithClassName) { + if ($objectType->getObjectClassNames() !== []) { if ($objectType->hasMethod($methodType->getValue())->yes()) { return true; } @@ -160,12 +191,37 @@ public function findSpecifiedType( return false; } } + + $genericType = TypeTraverser::map($objectType, static function (Type $type, callable $traverse): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + if ($type instanceof GenericClassStringType) { + return $type->getGenericType(); + } + return new MixedType(); + }); + + if ($genericType instanceof TypeWithClassName) { + if ($genericType->hasMethod($methodType->getValue())->yes()) { + return true; + } + + $classReflection = $genericType->getClassReflection(); + if ( + $classReflection !== null + && $classReflection->isFinal() + && $genericType->hasMethod($methodType->getValue())->no()) { + return false; + } + } } } } } - $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $node, TypeSpecifierContext::createTruthy()); + $typeSpecifierScope = $this->treatPhpDocTypesAsCertain ? $scope : $scope->doNotTreatPhpDocTypesAsCertain(); + $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($typeSpecifierScope, $node, $this->determineContext($typeSpecifierScope, $node)); // don't validate types on overwrite if ($specifiedTypes->shouldOverwrite()) { @@ -177,11 +233,11 @@ public function findSpecifiedType( $rootExpr = $specifiedTypes->getRootExpr(); if ($rootExpr !== null) { - if (self::isSpecified($scope, $node, $rootExpr)) { + if (self::isSpecified($typeSpecifierScope, $node, $rootExpr)) { return null; } - $rootExprType = $scope->getType($rootExpr); + $rootExprType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($rootExpr) : $scope->getNativeType($rootExpr)); if ($rootExprType instanceof ConstantBooleanType) { return $rootExprType->getValue(); } @@ -192,7 +248,7 @@ public function findSpecifiedType( $results = []; foreach ($sureTypes as $sureType) { - if (self::isSpecified($scope, $node, $sureType[0])) { + if (self::isSpecified($typeSpecifierScope, $node, $sureType[0])) { $results[] = TrinaryLogic::createMaybe(); continue; } @@ -210,7 +266,7 @@ public function findSpecifiedType( } foreach ($sureNotTypes as $sureNotType) { - if (self::isSpecified($scope, $node, $sureNotType[0])) { + if (self::isSpecified($typeSpecifierScope, $node, $sureNotType[0])) { $results[] = TrinaryLogic::createMaybe(); continue; } @@ -241,8 +297,8 @@ private static function isSpecified(Scope $scope, Expr $node, Expr $expr): bool return true; } - if ($expr instanceof Expr\Variable && is_string($expr->name) && !$scope->hasVariableType($expr->name)->yes()) { - return true; + if ($expr instanceof Expr\Variable) { + return is_string($expr->name) && !$scope->hasVariableType($expr->name)->yes(); } if ($expr instanceof Expr\BooleanNot) { @@ -257,7 +313,7 @@ private static function isSpecified(Scope $scope, Expr $node, Expr $expr): bool $node instanceof FuncCall || $node instanceof MethodCall || $node instanceof Expr\StaticCall - ) && $scope->isSpecified($expr); + ) && $scope->hasExpressionType($expr)->yes(); } /** @@ -272,7 +328,7 @@ public function getArgumentsDescription( return ''; } - $descriptions = array_map(static fn (Arg $arg): string => $scope->getType($arg->value)->describe(VerbosityLevel::value()), $args); + $descriptions = array_map(fn (Arg $arg): string => ($this->treatPhpDocTypesAsCertain ? $scope->getType($arg->value) : $scope->getNativeType($arg->value))->describe(VerbosityLevel::value()), $args); if (count($descriptions) < 3) { return sprintf(' with %s', implode(' and ', $descriptions)); @@ -298,7 +354,54 @@ public function doNotTreatPhpDocTypesAsCertain(): self $this->typeSpecifier, $this->universalObjectCratesClasses, false, + $this->nullContextForVoidReturningFunctions, ); } + private function determineContext(Scope $scope, Expr $node): TypeSpecifierContext + { + if (!$this->nullContextForVoidReturningFunctions) { + return TypeSpecifierContext::createTruthy(); + } + + if ($node instanceof Expr\CallLike && $node->isFirstClassCallable()) { + return TypeSpecifierContext::createTruthy(); + } + + if ($node instanceof FuncCall && $node->name instanceof Node\Name) { + if ($this->reflectionProvider->hasFunction($node->name, $scope)) { + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $node->getArgs(), $functionReflection->getVariants(), $functionReflection->getNamedArgumentsVariants()); + $returnType = TypeUtils::resolveLateResolvableTypes($parametersAcceptor->getReturnType()); + + return $returnType->isVoid()->yes() ? TypeSpecifierContext::createNull() : TypeSpecifierContext::createTruthy(); + } + } elseif ($node instanceof MethodCall && $node->name instanceof Node\Identifier) { + $methodCalledOnType = $scope->getType($node->var); + $methodReflection = $scope->getMethodReflection($methodCalledOnType, $node->name->name); + if ($methodReflection !== null) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $node->getArgs(), $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); + $returnType = TypeUtils::resolveLateResolvableTypes($parametersAcceptor->getReturnType()); + + return $returnType->isVoid()->yes() ? TypeSpecifierContext::createNull() : TypeSpecifierContext::createTruthy(); + } + } elseif ($node instanceof StaticCall && $node->name instanceof Node\Identifier) { + if ($node->class instanceof Node\Name) { + $calleeType = $scope->resolveTypeByName($node->class); + } else { + $calleeType = $scope->getType($node->class); + } + + $staticMethodReflection = $scope->getMethodReflection($calleeType, $node->name->name); + if ($staticMethodReflection !== null) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $node->getArgs(), $staticMethodReflection->getVariants(), $staticMethodReflection->getNamedArgumentsVariants()); + $returnType = TypeUtils::resolveLateResolvableTypes($parametersAcceptor->getReturnType()); + + return $returnType->isVoid()->yes() ? TypeSpecifierContext::createNull() : TypeSpecifierContext::createTruthy(); + } + } + + return TypeSpecifierContext::createTruthy(); + } + } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php index 6208b0eddf..9fc4e4067d 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; +use PHPStan\Parser\LastConditionVisitor; use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -21,6 +22,7 @@ public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, private bool $checkAlwaysTrueCheckTypeFunctionCall, private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, ) { } @@ -51,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; if (!$isAlways) { @@ -62,18 +64,28 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), - )))->build(), + )))->identifier('method.impossibleType')->build(), ]; } elseif ($this->checkAlwaysTrueCheckTypeFunctionCall) { + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + return []; + } + $method = $this->getMethod($node->var, $node->name->name, $scope); - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Call to method %s::%s()%s will always evaluate to true.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), - )))->build(), - ]; + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Call to method %s::%s()%s will always evaluate to true.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + ))); + if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier('method.alreadyNarrowedType'); + + return [$errorBuilder->build()]; } return []; diff --git a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php index af7bd57afa..5d35d1be1a 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; +use PHPStan\Parser\LastConditionVisitor; use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -21,6 +22,7 @@ public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, private bool $checkAlwaysTrueCheckTypeFunctionCall, private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, ) { } @@ -51,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; if (!$isAlways) { @@ -63,19 +65,28 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), - )))->build(), + )))->identifier('staticMethod.impossibleType')->build(), ]; } elseif ($this->checkAlwaysTrueCheckTypeFunctionCall) { + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + return []; + } + $method = $this->getMethod($node->class, $node->name->name, $scope); + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Call to static method %s::%s()%s will always evaluate to true.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + ))); + if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Call to static method %s::%s()%s will always evaluate to true.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), - )))->build(), - ]; + $errorBuilder->identifier('staticMethod.alreadyNarrowedType'); + + return [$errorBuilder->build()]; } return []; diff --git a/src/Rules/Comparison/LogicalXorConstantConditionRule.php b/src/Rules/Comparison/LogicalXorConstantConditionRule.php new file mode 100644 index 0000000000..250b55bd6e --- /dev/null +++ b/src/Rules/Comparison/LogicalXorConstantConditionRule.php @@ -0,0 +1,102 @@ + + */ +class LogicalXorConstantConditionRule implements Rule +{ + + public function __construct( + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + ) + { + } + + public function getNodeType(): string + { + return LogicalXor::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $errors = []; + $leftType = $this->helper->getBooleanType($scope, $node->left); + if ($leftType instanceof ConstantBooleanType) { + $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $booleanNativeType = $this->helper->getNativeBooleanType($scope, $node->left); + if ($booleanNativeType instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + }; + + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$leftType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTipLeft(RuleErrorBuilder::message(sprintf( + 'Left side of xor is always %s.', + $leftType->getValue() ? 'true' : 'false', + ))) + ->identifier(sprintf('logicalXor.leftAlways%s', $leftType->getValue() ? 'True' : 'False')) + ->line($node->left->getStartLine()); + if ($leftType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $errors[] = $errorBuilder->build(); + } + } + + $rightType = $this->helper->getBooleanType($scope, $node->right); + if ($rightType instanceof ConstantBooleanType) { + $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $booleanNativeType = $this->helper->getNativeBooleanType( + $scope, + $node->right, + ); + if ($booleanNativeType instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + }; + + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if (!$rightType->getValue() || $isLast !== true || $this->reportAlwaysTrueInLastCondition) { + $errorBuilder = $addTipRight(RuleErrorBuilder::message(sprintf( + 'Right side of xor is always %s.', + $rightType->getValue() ? 'true' : 'false', + ))) + ->identifier(sprintf('logicalXor.rightAlways%s', $rightType->getValue() ? 'True' : 'False')) + ->line($node->right->getStartLine()); + if ($rightType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $errors[] = $errorBuilder->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Comparison/MatchExpressionRule.php b/src/Rules/Comparison/MatchExpressionRule.php index e9081d8e7b..d7d318322e 100644 --- a/src/Rules/Comparison/MatchExpressionRule.php +++ b/src/Rules/Comparison/MatchExpressionRule.php @@ -9,19 +9,13 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; -use PHPStan\Type\SubtractableType; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use UnhandledMatchError; -use function array_keys; use function array_map; -use function array_values; use function count; use function sprintf; @@ -31,7 +25,13 @@ class MatchExpressionRule implements Rule { - public function __construct(private bool $checkAlwaysTrueStrictComparison) + public function __construct( + private ConstantConditionRuleHelper $constantConditionRuleHelper, + private bool $checkAlwaysTrueStrictComparison, + private bool $disableUnreachable, + private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertain, + ) { } @@ -43,13 +43,23 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $matchCondition = $node->getCondition(); - $nextArmIsDead = false; + $matchConditionType = $scope->getType($matchCondition); + $nextArmIsDeadForType = false; + $nextArmIsDeadForNativeType = false; $errors = []; $armsCount = count($node->getArms()); $hasDefault = false; foreach ($node->getArms() as $i => $arm) { - if ($nextArmIsDead) { - $errors[] = RuleErrorBuilder::message('Match arm is unreachable because previous comparison is always true.')->line($arm->getLine())->build(); + if ( + $nextArmIsDeadForNativeType + || ($nextArmIsDeadForType && $this->treatPhpDocTypesAsCertain) + ) { + if (!$this->disableUnreachable) { + $errors[] = RuleErrorBuilder::message('Match arm is unreachable because previous comparison is always true.') + ->identifier('match.unreachable') + ->line($arm->getLine()) + ->build(); + } continue; } $armConditions = $arm->getConditions(); @@ -62,10 +72,31 @@ public function processNode(Node $node, Scope $scope): array $matchCondition, $armCondition->getCondition(), ); + $armConditionResult = $armConditionScope->getType($armConditionExpr); if (!$armConditionResult instanceof ConstantBooleanType) { continue; } + if ($armConditionResult->getValue()) { + $nextArmIsDeadForType = true; + } + + if (!$this->treatPhpDocTypesAsCertain) { + $armConditionNativeResult = $armConditionScope->getNativeType($armConditionExpr); + if (!$armConditionNativeResult instanceof ConstantBooleanType) { + continue; + } + if ($armConditionNativeResult->getValue()) { + $nextArmIsDeadForNativeType = true; + } + } + + if ($matchConditionType instanceof ConstantBooleanType) { + $armConditionStandaloneResult = $this->constantConditionRuleHelper->getBooleanType($armConditionScope, $armCondition->getCondition()); + if (!$armConditionStandaloneResult instanceof ConstantBooleanType) { + continue; + } + } $armLine = $armCondition->getLine(); if (!$armConditionResult->getValue()) { @@ -73,63 +104,38 @@ public function processNode(Node $node, Scope $scope): array 'Match arm comparison between %s and %s is always false.', $armConditionScope->getType($matchCondition)->describe(VerbosityLevel::value()), $armConditionScope->getType($armCondition->getCondition())->describe(VerbosityLevel::value()), - ))->line($armLine)->build(); + ))->line($armLine)->identifier('match.alwaysFalse')->build(); } else { - $nextArmIsDead = true; - if ( - $this->checkAlwaysTrueStrictComparison - && ($i !== $armsCount - 1 || $i === 0) - ) { - $errors[] = RuleErrorBuilder::message(sprintf( + if ($this->checkAlwaysTrueStrictComparison) { + if ($i === $armsCount - 1 && !$this->reportAlwaysTrueInLastCondition) { + continue; + } + $errorBuilder = RuleErrorBuilder::message(sprintf( 'Match arm comparison between %s and %s is always true.', $armConditionScope->getType($matchCondition)->describe(VerbosityLevel::value()), $armConditionScope->getType($armCondition->getCondition())->describe(VerbosityLevel::value()), - ))->line($armLine)->build(); + ))->line($armLine); + if ($i !== $armsCount - 1 && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + $errorBuilder->identifier('match.alwaysTrue'); + + $errors[] = $errorBuilder->build(); } } } } - if (!$hasDefault && !$nextArmIsDead) { + if (!$hasDefault && !$nextArmIsDeadForType) { $remainingType = $node->getEndScope()->getType($matchCondition); - if ($remainingType instanceof TypeWithClassName && $remainingType instanceof SubtractableType) { - $subtractedType = $remainingType->getSubtractedType(); - if ($subtractedType !== null && $remainingType->getClassReflection() !== null) { - $classReflection = $remainingType->getClassReflection(); - if ($classReflection->isEnum()) { - $cases = []; - foreach (array_keys($classReflection->getEnumCases()) as $name) { - $cases[$name] = new EnumCaseObjectType($classReflection->getName(), $name); - } - - $subtractedTypes = TypeUtils::flattenTypes($subtractedType); - $set = true; - foreach ($subtractedTypes as $subType) { - if (!$subType instanceof EnumCaseObjectType) { - $set = false; - break; - } - - if ($subType->getClassName() !== $classReflection->getName()) { - $set = false; - break; - } - - unset($cases[$subType->getEnumCaseName()]); - } - - $cases = array_values($cases); - $casesCount = count($cases); - if ($set) { - if ($casesCount > 1) { - $remainingType = new UnionType($cases); - } - if ($casesCount === 1) { - $remainingType = $cases[0]; - } - } - } - } + $cases = $remainingType->getEnumCases(); + $casesCount = count($cases); + if ($casesCount > 1) { + $remainingType = new UnionType($cases); + } + if ($casesCount === 1) { + $remainingType = $cases[0]; } if ( !$remainingType instanceof NeverType @@ -140,7 +146,7 @@ public function processNode(Node $node, Scope $scope): array 'Match expression does not handle remaining %s: %s', $remainingType instanceof UnionType ? 'values' : 'value', $remainingType->describe(VerbosityLevel::value()), - ))->build(); + ))->identifier('match.unhandled')->build(); } } diff --git a/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php b/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php index bf8feaaec6..081cdef1e0 100644 --- a/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php +++ b/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php @@ -7,8 +7,10 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\VerbosityLevel; +use function get_class; use function sprintf; /** @@ -17,6 +19,12 @@ class NumberComparisonOperatorsConstantConditionRule implements Rule { + public function __construct( + private bool $treatPhpDocTypesAsCertain, + ) + { + } + public function getNodeType(): string { return BinaryOp::class; @@ -36,16 +44,46 @@ public function processNode( return []; } - $exprType = $scope->getType($node); + $exprType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); if ($exprType instanceof ConstantBooleanType) { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $booleanNativeType = $scope->getNativeType($node); + if ($booleanNativeType instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + }; + + switch (get_class($node)) { + case BinaryOp\Greater::class: + $nodeType = 'greater'; + break; + case BinaryOp\GreaterOrEqual::class: + $nodeType = 'greaterOrEqual'; + break; + case BinaryOp\Smaller::class: + $nodeType = 'smaller'; + break; + case BinaryOp\SmallerOrEqual::class: + $nodeType = 'smallerOrEqual'; + break; + default: + throw new ShouldNotHappenException(); + } + return [ - RuleErrorBuilder::message(sprintf( + $addTip(RuleErrorBuilder::message(sprintf( 'Comparison operation "%s" between %s and %s is always %s.', $node->getOperatorSigil(), $scope->getType($node->left)->describe(VerbosityLevel::value()), $scope->getType($node->right)->describe(VerbosityLevel::value()), $exprType->getValue() ? 'true' : 'false', - ))->build(), + )))->identifier(sprintf('%s.always%s', $nodeType, $exprType->getValue() ? 'True' : 'False'))->build(), ]; } diff --git a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php index 18200745a0..66f447db3b 100644 --- a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php +++ b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; @@ -16,7 +17,11 @@ class StrictComparisonOfDifferentTypesRule implements Rule { - public function __construct(private bool $checkAlwaysTrueStrictComparison) + public function __construct( + private bool $checkAlwaysTrueStrictComparison, + private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, + ) { } @@ -31,31 +36,64 @@ public function processNode(Node $node, Scope $scope): array return []; } - $nodeType = $scope->getType($node); + $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); if (!$nodeType instanceof ConstantBooleanType) { return []; } - $leftType = $scope->getType($node->left); - $rightType = $scope->getType($node->right); + $leftType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->left) : $scope->getNativeType($node->left); + $rightType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->right) : $scope->getNativeType($node->right); + + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $instanceofTypeWithoutPhpDocs = $scope->getNativeType($node); + if ($instanceofTypeWithoutPhpDocs instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + }; if (!$nodeType->getValue()) { return [ - RuleErrorBuilder::message(sprintf( + $addTip(RuleErrorBuilder::message(sprintf( 'Strict comparison using %s between %s and %s will always evaluate to false.', - $node instanceof Node\Expr\BinaryOp\Identical ? '===' : '!==', + $node->getOperatorSigil(), $leftType->describe(VerbosityLevel::value()), $rightType->describe(VerbosityLevel::value()), - ))->build(), + )))->identifier(sprintf('%s.alwaysFalse', $node instanceof Node\Expr\BinaryOp\Identical ? 'identical' : 'notIdentical'))->build(), ]; } elseif ($this->checkAlwaysTrueStrictComparison) { + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + return []; + } + + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Strict comparison using %s between %s and %s will always evaluate to true.', + $node->getOperatorSigil(), + $leftType->describe(VerbosityLevel::value()), + $rightType->describe(VerbosityLevel::value()), + ))); + if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->addTip('Remove remaining cases below this one and this error will disappear too.'); + } + + if ( + $leftType->isEnum()->yes() + && $rightType->isEnum()->yes() + && $node->getAttribute(LastConditionVisitor::ATTRIBUTE_IS_MATCH_NAME, false) !== true + ) { + $errorBuilder->addTip('Use match expression instead. PHPStan will report unhandled enum cases.'); + } + + $errorBuilder->identifier(sprintf('%s.alwaysTrue', $node instanceof Node\Expr\BinaryOp\Identical ? 'identical' : 'notIdentical')); + return [ - RuleErrorBuilder::message(sprintf( - 'Strict comparison using %s between %s and %s will always evaluate to true.', - $node instanceof Node\Expr\BinaryOp\Identical ? '===' : '!==', - $leftType->describe(VerbosityLevel::value()), - $rightType->describe(VerbosityLevel::value()), - ))->build(), + $errorBuilder->build(), ]; } diff --git a/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php b/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php index e608d7bfbd..9513924e6b 100644 --- a/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php +++ b/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php @@ -44,22 +44,13 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ $addTip(RuleErrorBuilder::message(sprintf( 'Ternary operator condition is always %s.', $exprType->getValue() ? 'true' : 'false', - ))) - ->identifier('deadCode.ternaryConstantCondition') - ->metadata([ - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - 'depth' => $node->getAttribute('expressionDepth'), - 'order' => $node->getAttribute('expressionOrder'), - 'value' => $exprType->getValue(), - ]) - ->build(), + )))->identifier(sprintf('ternary.always%s', $exprType->getValue() ? 'True' : 'False'))->build(), ]; } diff --git a/src/Rules/Comparison/UnreachableIfBranchesRule.php b/src/Rules/Comparison/UnreachableIfBranchesRule.php index ce9eb2328f..4918a320d6 100644 --- a/src/Rules/Comparison/UnreachableIfBranchesRule.php +++ b/src/Rules/Comparison/UnreachableIfBranchesRule.php @@ -17,6 +17,7 @@ class UnreachableIfBranchesRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private bool $treatPhpDocTypesAsCertain, + private bool $disable, ) { } @@ -28,49 +29,48 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { + if ($this->disable) { + return []; + } + $errors = []; $condition = $node->cond; - $conditionType = $scope->getType($condition)->toBoolean(); - $nextBranchIsDead = $conditionType instanceof ConstantBooleanType && $conditionType->getValue() && $this->helper->shouldSkip($scope, $node->cond) && !$this->helper->shouldReportAlwaysTrueByDefault($node->cond); + $conditionType = $this->treatPhpDocTypesAsCertain ? $scope->getType($condition) : $scope->getNativeType($condition); + $conditionBooleanType = $conditionType->toBoolean(); + $nextBranchIsDead = $conditionBooleanType->isTrue()->yes() && $this->helper->shouldSkip($scope, $node->cond) && !$this->helper->shouldReportAlwaysTrueByDefault($node->cond); + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, &$condition): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; } - $booleanNativeType = $scope->doNotTreatPhpDocTypesAsCertain()->getType($condition)->toBoolean(); + $booleanNativeType = $scope->getNativeType($condition)->toBoolean(); if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; foreach ($node->elseifs as $elseif) { if ($nextBranchIsDead) { - $errors[] = $addTip(RuleErrorBuilder::message('Elseif branch is unreachable because previous condition is always true.')->line($elseif->getLine())) - ->identifier('deadCode.unreachableElseif') - ->metadata([ - 'ifDepth' => $node->getAttribute('statementDepth'), - 'ifOrder' => $node->getAttribute('statementOrder'), - 'depth' => $elseif->getAttribute('statementDepth'), - 'order' => $elseif->getAttribute('statementOrder'), - ]) + $errors[] = $addTip(RuleErrorBuilder::message('Elseif branch is unreachable because previous condition is always true.')) + ->identifier('elseif.unreachable') + ->line($elseif->getStartLine()) ->build(); continue; } $condition = $elseif->cond; - $conditionType = $scope->getType($condition)->toBoolean(); - $nextBranchIsDead = $conditionType instanceof ConstantBooleanType && $conditionType->getValue() && $this->helper->shouldSkip($scope, $elseif->cond) && !$this->helper->shouldReportAlwaysTrueByDefault($elseif->cond); + $conditionType = $this->treatPhpDocTypesAsCertain ? $scope->getType($condition) : $scope->getNativeType($condition); + $conditionBooleanType = $conditionType->toBoolean(); + $nextBranchIsDead = $conditionBooleanType->isTrue()->yes() && $this->helper->shouldSkip($scope, $elseif->cond) && !$this->helper->shouldReportAlwaysTrueByDefault($elseif->cond); } if ($node->else !== null && $nextBranchIsDead) { - $errors[] = $addTip(RuleErrorBuilder::message('Else branch is unreachable because previous condition is always true.'))->line($node->else->getLine()) - ->identifier('deadCode.unreachableElse') - ->metadata([ - 'ifDepth' => $node->getAttribute('statementDepth'), - 'ifOrder' => $node->getAttribute('statementOrder'), - ]) + $errors[] = $addTip(RuleErrorBuilder::message('Else branch is unreachable because previous condition is always true.')) + ->identifier('else.unreachable') + ->line($node->else->getStartLine()) ->build(); } diff --git a/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php b/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php index 551f0e63e9..32ef874c5f 100644 --- a/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php +++ b/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php @@ -17,6 +17,7 @@ class UnreachableTernaryElseBranchRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private bool $treatPhpDocTypesAsCertain, + private bool $disable, ) { } @@ -28,10 +29,14 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $conditionType = $scope->getType($node->cond)->toBoolean(); + if ($this->disable) { + return []; + } + + $conditionType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->cond) : $scope->getNativeType($node->cond); + $conditionBooleanType = $conditionType->toBoolean(); if ( - $conditionType instanceof ConstantBooleanType - && $conditionType->getValue() + $conditionBooleanType->isTrue()->yes() && $this->helper->shouldSkip($scope, $node->cond) && !$this->helper->shouldReportAlwaysTrueByDefault($node->cond) ) { @@ -40,23 +45,17 @@ public function processNode(Node $node, Scope $scope): array return $ruleErrorBuilder; } - $booleanNativeType = $scope->doNotTreatPhpDocTypesAsCertain()->getType($node->cond); + $booleanNativeType = $scope->getNativeType($node->cond); if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ $addTip(RuleErrorBuilder::message('Else branch is unreachable because ternary operator condition is always true.')) - ->line($node->else->getLine()) - ->identifier('deadCode.unreachableTernaryElse') - ->metadata([ - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - 'depth' => $node->getAttribute('expressionDepth'), - 'order' => $node->getAttribute('expressionOrder'), - ]) + ->identifier('ternary.elseUnreachable') + ->line($node->else->getStartLine()) ->build(), ]; } diff --git a/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php b/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php index 8494d0c2b2..80a473b713 100644 --- a/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php +++ b/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\VoidType; /** * @implements Rule @@ -21,12 +20,11 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $matchResultType = $scope->getType($node); - if ( - $matchResultType instanceof VoidType - && !$scope->isInFirstLevelStatement() - ) { - return [RuleErrorBuilder::message('Result of match expression (void) is used.')->build()]; + if (!$scope->isInFirstLevelStatement()) { + $matchResultType = $scope->getKeepVoidType($node); + if ($matchResultType->isVoid()->yes()) { + return [RuleErrorBuilder::message('Result of match expression (void) is used.')->identifier('match.void')->build()]; + } } return []; diff --git a/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php index 0c485c7329..01ae378030 100644 --- a/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php +++ b/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php @@ -33,7 +33,7 @@ public function processNode( ): array { $exprType = $this->helper->getBooleanType($scope, $node->cond); - if ($exprType instanceof ConstantBooleanType && !$exprType->getValue()) { + if ($exprType->isFalse()->yes()) { $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; @@ -44,11 +44,12 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ - $addTip(RuleErrorBuilder::message('While loop condition is always false.'))->line($node->cond->getLine()) + $addTip(RuleErrorBuilder::message('While loop condition is always false.'))->line($node->cond->getStartLine()) + ->identifier('while.alwaysFalse') ->build(), ]; } diff --git a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php index 3723019229..42a885b6ad 100644 --- a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php +++ b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php @@ -60,7 +60,7 @@ public function processNode( } $originalNode = $node->getOriginalNode(); $exprType = $this->helper->getBooleanType($scope, $originalNode->cond); - if ($exprType instanceof ConstantBooleanType && $exprType->getValue()) { + if ($exprType->isTrue()->yes()) { $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { return $ruleErrorBuilder; @@ -71,11 +71,12 @@ public function processNode( return $ruleErrorBuilder; } - return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; return [ - $addTip(RuleErrorBuilder::message('While loop condition is always true.'))->line($originalNode->cond->getLine()) + $addTip(RuleErrorBuilder::message('While loop condition is always true.'))->line($originalNode->cond->getStartLine()) + ->identifier('while.alwaysTrue') ->build(), ]; } diff --git a/src/Rules/Constants/AlwaysUsedClassConstantsExtension.php b/src/Rules/Constants/AlwaysUsedClassConstantsExtension.php index 756cd130bf..4e3bdcc92d 100644 --- a/src/Rules/Constants/AlwaysUsedClassConstantsExtension.php +++ b/src/Rules/Constants/AlwaysUsedClassConstantsExtension.php @@ -4,7 +4,24 @@ use PHPStan\Reflection\ConstantReflection; -/** @api */ +/** + * This is the extension interface to implement if you want to describe + * always-used class constant. + * + * To register it in the configuration file use the `phpstan.constants.alwaysUsedClassConstantsExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.constants.alwaysUsedClassConstantsExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/always-used-class-constants + * + * @api + */ interface AlwaysUsedClassConstantsExtension { diff --git a/src/Rules/Constants/ConstantRule.php b/src/Rules/Constants/ConstantRule.php index 1eb08445dd..27003ecade 100644 --- a/src/Rules/Constants/ConstantRule.php +++ b/src/Rules/Constants/ConstantRule.php @@ -26,7 +26,10 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Constant %s not found.', (string) $node->name, - ))->discoveringSymbolsTip()->build(), + )) + ->identifier('constant.notFound') + ->discoveringSymbolsTip() + ->build(), ]; } diff --git a/src/Rules/Constants/DynamicClassConstantFetchRule.php b/src/Rules/Constants/DynamicClassConstantFetchRule.php new file mode 100644 index 0000000000..ce1295ffc4 --- /dev/null +++ b/src/Rules/Constants/DynamicClassConstantFetchRule.php @@ -0,0 +1,69 @@ + + */ +class DynamicClassConstantFetchRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion, private RuleLevelHelper $ruleLevelHelper) + { + } + + public function getNodeType(): string + { + return ClassConstFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Expr) { + return []; + } + + if (!$this->phpVersion->supportsDynamicClassConstantFetch()) { + return [ + RuleErrorBuilder::message('Fetching class constants with a dynamic name is supported only on PHP 8.3 and later.') + ->identifier('classConstant.dynamicFetch') + ->nonIgnorable() + ->build(), + ]; + } + + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->name, + '', + static fn (Type $type): bool => $type->isString()->yes(), + ); + $type = $typeResult->getType(); + if ($type instanceof ErrorType) { + return []; + } + if ($type->isString()->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Class constant name in dynamic fetch can only be a string, %s given.', + $type->describe(VerbosityLevel::typeOnly()), + ))->identifier('classConstant.nameType')->build(), + ]; + } + +} diff --git a/src/Rules/Constants/FinalConstantRule.php b/src/Rules/Constants/FinalConstantRule.php index 23fa67ab65..d6b891f465 100644 --- a/src/Rules/Constants/FinalConstantRule.php +++ b/src/Rules/Constants/FinalConstantRule.php @@ -33,7 +33,10 @@ public function processNode(Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message('Final class constants are supported only on PHP 8.1 and later.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Final class constants are supported only on PHP 8.1 and later.') + ->identifier('classConstant.finalNotSupported') + ->nonIgnorable() + ->build(), ]; } diff --git a/src/Rules/Constants/MagicConstantContextRule.php b/src/Rules/Constants/MagicConstantContextRule.php new file mode 100644 index 0000000000..5cfb38f074 --- /dev/null +++ b/src/Rules/Constants/MagicConstantContextRule.php @@ -0,0 +1,75 @@ + */ +class MagicConstantContextRule implements Rule +{ + + public function getNodeType(): string + { + return MagicConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // test cases https://3v4l.org/ZUvvr + + if ($node instanceof MagicConst\Class_) { + if ($scope->isInClass()) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf('Magic constant %s is always empty outside a class.', $node->getName()), + )->identifier('magicConstant.outOfClass')->build(), + ]; + } elseif ($node instanceof MagicConst\Trait_) { + if ($scope->isInTrait()) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf('Magic constant %s is always empty outside a trait.', $node->getName()), + )->identifier('magicConstant.outOfTrait')->build(), + ]; + } elseif ($node instanceof MagicConst\Method || $node instanceof MagicConst\Function_) { + if ($scope->getFunctionName() !== null) { + return []; + } + if ($scope->isInAnonymousFunction()) { + return []; + } + + if ((bool) $node->getAttribute(MagicConstantParamDefaultVisitor::ATTRIBUTE_NAME)) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf('Magic constant %s is always empty outside a function.', $node->getName()), + )->identifier('magicConstant.outOfFunction')->build(), + ]; + } elseif ($node instanceof MagicConst\Namespace_) { + if ($scope->getNamespace() === null) { + return [ + RuleErrorBuilder::message( + sprintf('Magic constant %s is always empty in global namespace.', $node->getName()), + )->identifier('magicConstant.outOfNamespace')->build(), + ]; + } + } + return []; + } + +} diff --git a/src/Rules/Constants/MissingClassConstantTypehintRule.php b/src/Rules/Constants/MissingClassConstantTypehintRule.php index 91a3679d3c..7de4b9fec5 100644 --- a/src/Rules/Constants/MissingClassConstantTypehintRule.php +++ b/src/Rules/Constants/MissingClassConstantTypehintRule.php @@ -5,9 +5,9 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassReflection; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; use PHPStan\Type\VerbosityLevel; @@ -46,12 +46,15 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ private function processSingleConstant(ClassReflection $classReflection, string $constantName): array { $constantReflection = $classReflection->getConstant($constantName); - $constantType = $constantReflection->getValueType(); + $constantType = $constantReflection->getPhpDocType(); + if ($constantType === null) { + return []; + } $errors = []; foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($constantType) as $iterableType) { @@ -61,7 +64,10 @@ private function processSingleConstant(ClassReflection $classReflection, string $constantReflection->getDeclaringClass()->getDisplayName(), $constantName, $iterableTypeDescription, - ))->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); } foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($constantType) as [$name, $genericTypeNames]) { @@ -71,7 +77,10 @@ private function processSingleConstant(ClassReflection $classReflection, string $constantName, $name, implode(', ', $genericTypeNames), - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + )) + ->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP) + ->identifier('missingType.generics') + ->build(); } foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($constantType) as $callableType) { @@ -80,7 +89,7 @@ private function processSingleConstant(ClassReflection $classReflection, string $constantReflection->getDeclaringClass()->getDisplayName(), $constantName, $callableType->describe(VerbosityLevel::typeOnly()), - ))->build(); + ))->identifier('missingType.callable')->build(); } return $errors; diff --git a/src/Rules/Constants/NativeTypedClassConstantRule.php b/src/Rules/Constants/NativeTypedClassConstantRule.php new file mode 100644 index 0000000000..ce2ea802d8 --- /dev/null +++ b/src/Rules/Constants/NativeTypedClassConstantRule.php @@ -0,0 +1,44 @@ + + */ +class NativeTypedClassConstantRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node->type === null) { + return []; + } + + if ($this->phpVersion->supportsNativeTypesInClassConstants()) { + return []; + } + + return [ + RuleErrorBuilder::message('Class constants with native types are supported only on PHP 8.3 and later.') + ->identifier('classConstant.nativeTypeNotSupported') + ->nonIgnorable() + ->build(), + ]; + } + +} diff --git a/src/Rules/Constants/OverridingConstantRule.php b/src/Rules/Constants/OverridingConstantRule.php index ca60d2c9b8..555cbfc0dd 100644 --- a/src/Rules/Constants/OverridingConstantRule.php +++ b/src/Rules/Constants/OverridingConstantRule.php @@ -7,8 +7,8 @@ use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; use PHPStan\Type\VerbosityLevel; @@ -48,7 +48,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ private function processSingleConstant(ClassReflection $classReflection, string $constantName): array { @@ -58,10 +58,6 @@ private function processSingleConstant(ClassReflection $classReflection, string } $constantReflection = $classReflection->getConstant($constantName); - if (!$constantReflection instanceof ClassConstantReflection) { - return []; - } - $errors = []; if ($prototype->isFinal()) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -70,7 +66,7 @@ private function processSingleConstant(ClassReflection $classReflection, string $constantReflection->getName(), $prototype->getDeclaringClass()->getDisplayName(), $prototype->getName(), - ))->nonIgnorable()->build(); + ))->identifier('classConstant.final')->nonIgnorable()->build(); } if ($prototype->isPublic()) { @@ -82,7 +78,7 @@ private function processSingleConstant(ClassReflection $classReflection, string $constantReflection->getName(), $prototype->getDeclaringClass()->getDisplayName(), $prototype->getName(), - ))->nonIgnorable()->build(); + ))->identifier('classConstant.visibility')->nonIgnorable()->build(); } } elseif ($constantReflection->isPrivate()) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -91,13 +87,41 @@ private function processSingleConstant(ClassReflection $classReflection, string $constantReflection->getName(), $prototype->getDeclaringClass()->getDisplayName(), $prototype->getName(), - ))->nonIgnorable()->build(); + ))->identifier('classConstant.visibility')->nonIgnorable()->build(); } if (!$this->checkPhpDocMethodSignatures) { return $errors; } + $prototypeNativeType = $prototype->getNativeType(); + $constantNativeType = $constantReflection->getNativeType(); + if ($prototypeNativeType !== null) { + if ($constantNativeType !== null) { + if (!$prototypeNativeType->isSuperTypeOf($constantNativeType)->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Native type %s of constant %s::%s is not covariant with native type %s of constant %s::%s.', + $constantNativeType->describe(VerbosityLevel::typeOnly()), + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantReflection->getName(), + $prototypeNativeType->describe(VerbosityLevel::typeOnly()), + $prototype->getDeclaringClass()->getDisplayName(), + $prototype->getName(), + ))->identifier('classConstant.nativeType')->nonIgnorable()->build(); + } + } else { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Constant %s::%s overriding constant %s::%s (%s) should also have native type %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantReflection->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $prototype->getName(), + $prototypeNativeType->describe(VerbosityLevel::typeOnly()), + $prototypeNativeType->describe(VerbosityLevel::typeOnly()), + ))->identifier('classConstant.missingNativeType')->nonIgnorable()->build(); + } + } + if (!$prototype->hasPhpDocType()) { return $errors; } @@ -115,7 +139,7 @@ private function processSingleConstant(ClassReflection $classReflection, string $prototype->getValueType()->describe(VerbosityLevel::value()), $prototype->getDeclaringClass()->getDisplayName(), $prototype->getName(), - ))->build(); + ))->identifier('classConstant.type')->build(); } return $errors; diff --git a/src/Rules/Constants/ValueAssignedToClassConstantRule.php b/src/Rules/Constants/ValueAssignedToClassConstantRule.php new file mode 100644 index 0000000000..37b72e1c72 --- /dev/null +++ b/src/Rules/Constants/ValueAssignedToClassConstantRule.php @@ -0,0 +1,128 @@ + + */ +class ValueAssignedToClassConstantRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + + $nativeType = null; + if ($node->type !== null) { + $nativeType = ParserNodeTypeToPHPStanType::resolve($node->type, $scope->getClassReflection()); + } + + $errors = []; + foreach ($node->consts as $const) { + $constantName = $const->name->toString(); + $errors = array_merge($errors, $this->processSingleConstant( + $scope->getClassReflection(), + $constantName, + $scope->getType($const->value), + $nativeType, + )); + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleConstant(ClassReflection $classReflection, string $constantName, Type $valueExprType, ?Type $nativeType): array + { + $constantReflection = $classReflection->getConstant($constantName); + $phpDocType = $constantReflection->getPhpDocType(); + if ($phpDocType === null) { + if ($nativeType === null) { + return []; + } + + $accepts = $nativeType->acceptsWithReason($valueExprType, true); + if ($accepts->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Constant %s::%s (%s) does not accept value %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $nativeType->describe(VerbosityLevel::typeOnly()), + $valueExprType->describe(VerbosityLevel::value()), + ))->acceptsReasonsTip($accepts->reasons)->nonIgnorable()->identifier('classConstant.value')->build(), + ]; + } elseif ($nativeType === null) { + $isSuperType = $phpDocType->isSuperTypeOf($valueExprType); + $verbosity = VerbosityLevel::getRecommendedLevelByType($phpDocType, $valueExprType); + if ($isSuperType->no()) { + return [ + RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @var for constant %s::%s with type %s is incompatible with value %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $phpDocType->describe($verbosity), + $valueExprType->describe(VerbosityLevel::value()), + ))->identifier('classConstant.phpDocType')->build(), + ]; + + } elseif ($isSuperType->maybe()) { + return [ + RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @var for constant %s::%s with type %s is not subtype of value %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $phpDocType->describe($verbosity), + $valueExprType->describe(VerbosityLevel::value()), + ))->identifier('classConstant.phpDocType')->build(), + ]; + } + + return []; + } + + $type = $constantReflection->getValueType(); + $accepts = $type->acceptsWithReason($valueExprType, true); + if ($accepts->yes()) { + return []; + } + + $verbosity = VerbosityLevel::getRecommendedLevelByType($type, $valueExprType); + + return [ + RuleErrorBuilder::message(sprintf( + 'Constant %s::%s (%s) does not accept value %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $type->describe(VerbosityLevel::typeOnly()), + $valueExprType->describe($verbosity), + ))->acceptsReasonsTip($accepts->reasons)->identifier('classConstant.value')->build(), + ]; + } + +} diff --git a/src/Rules/DateTimeInstantiationRule.php b/src/Rules/DateTimeInstantiationRule.php index 3b09ccef61..b8cd1684dd 100644 --- a/src/Rules/DateTimeInstantiationRule.php +++ b/src/Rules/DateTimeInstantiationRule.php @@ -6,7 +6,6 @@ use PhpParser\Node; use PhpParser\Node\Expr\New_; use PHPStan\Analyser\Scope; -use PHPStan\Type\Constant\ConstantStringType; use Throwable; use function count; use function in_array; @@ -29,35 +28,40 @@ public function getNodeType(): string */ public function processNode(Node $node, Scope $scope): array { - if ( - !($node->class instanceof Node\Name) - || count($node->getArgs()) === 0 - || !in_array(strtolower((string) $node->class), ['datetime', 'datetimeimmutable'], true) - ) { + if (!$node->class instanceof Node\Name) { return []; } - $arg = $scope->getType($node->getArgs()[0]->value); - if (!($arg instanceof ConstantStringType)) { + $lowerClassName = strtolower((string) $node->class); + if ( + count($node->getArgs()) === 0 + || !in_array($lowerClassName, ['datetime', 'datetimeimmutable'], true) + ) { return []; } + $arg = $scope->getType($node->getArgs()[0]->value); $errors = []; - $dateString = $arg->getValue(); - try { - new DateTime($dateString); - } catch (Throwable) { - // an exception is thrown for errors only but we want to catch warnings too - } - $lastErrors = DateTime::getLastErrors(); - if ($lastErrors !== false) { + + foreach ($arg->getConstantStrings() as $constantString) { + $dateString = $constantString->getValue(); + try { + new DateTime($dateString); + } catch (Throwable) { + // an exception is thrown for errors only but we want to catch warnings too + } + $lastErrors = DateTime::getLastErrors(); + if ($lastErrors === false) { + continue; + } + foreach ($lastErrors['errors'] as $error) { $errors[] = RuleErrorBuilder::message(sprintf( 'Instantiating %s with %s produces an error: %s', - (string) $node->class, + $lowerClassName === 'datetime' ? 'DateTime' : 'DateTimeImmutable', $dateString, $error, - ))->build(); + ))->identifier(sprintf('new.%s', $lowerClassName === 'datetime' ? 'dateTime' : 'dateTimeImmutable'))->build(); } } diff --git a/src/Rules/DeadCode/BetterNoopRule.php b/src/Rules/DeadCode/BetterNoopRule.php new file mode 100644 index 0000000000..22ce47032f --- /dev/null +++ b/src/Rules/DeadCode/BetterNoopRule.php @@ -0,0 +1,138 @@ + + */ +class BetterNoopRule implements Rule +{ + + public function __construct(private ExprPrinter $exprPrinter) + { + } + + public function getNodeType(): string + { + return NoopExpressionNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $expr = $node->getOriginalExpr(); + if ($expr instanceof Node\Expr\BinaryOp\LogicalXor) { + return [ + RuleErrorBuilder::message( + 'Unused result of "xor" operator.', + )->line($expr->getStartLine()) + ->tip('This operator has unexpected precedence, try disambiguating the logic with parentheses ().') + ->identifier('logicalXor.resultUnused') + ->build(), + ]; + } + + if ($expr instanceof Node\Expr\BinaryOp\LogicalAnd || $expr instanceof Node\Expr\BinaryOp\LogicalOr) { + $identifierType = $expr instanceof Node\Expr\BinaryOp\LogicalAnd ? 'logicalAnd' : 'logicalOr'; + + return [ + RuleErrorBuilder::message(sprintf( + 'Unused result of "%s" operator.', + $expr->getOperatorSigil(), + ))->line($expr->getStartLine()) + ->tip('This operator has unexpected precedence, try disambiguating the logic with parentheses ().') + ->identifier(sprintf('%s.resultUnused', $identifierType)) + ->build(), + ]; + } + + if ($node->hasAssign()) { + return []; + } + + if ($expr instanceof Node\Expr\BinaryOp\BooleanAnd || $expr instanceof Node\Expr\BinaryOp\BooleanOr) { + $identifierType = $expr instanceof Node\Expr\BinaryOp\BooleanAnd ? 'booleanAnd' : 'booleanOr'; + + return [ + RuleErrorBuilder::message(sprintf( + 'Unused result of "%s" operator.', + $expr->getOperatorSigil(), + ))->line($expr->getStartLine()) + ->identifier(sprintf('%s.resultUnused', $identifierType)) + ->build(), + ]; + } + + if ($expr instanceof Node\Expr\Ternary) { + return [ + RuleErrorBuilder::message('Unused result of ternary operator.') + ->line($expr->getStartLine()) + ->identifier('ternary.resultUnused') + ->build(), + ]; + } + + if ($expr instanceof Node\Expr\FuncCall) { + if ($expr->name instanceof Node\Name) { + // handled by CallToFunctionStatementWithoutSideEffectsRule + return []; + } + + $nameType = $scope->getType($expr->name); + if (!$nameType->isCallable()->yes()) { + return []; + } + } + + if ($expr instanceof Node\Expr\New_ && $expr->class instanceof Node\Name) { + // handled by CallToConstructorStatementWithoutSideEffectsRule + return []; + } + + if ( + $expr instanceof Node\Expr\NullsafeMethodCall + || $expr instanceof Node\Expr\MethodCall + || $expr instanceof Node\Expr\StaticCall + ) { + // handled by *WithoutSideEffectsRule rules + return []; + } + + if ( + $expr instanceof Node\Expr\Assign + || $expr instanceof Node\Expr\AssignOp + || $expr instanceof Node\Expr\AssignRef + ) { + return []; + } + + if ($expr instanceof Node\Expr\Closure) { + return []; + } + + $exprString = $this->exprPrinter->printExpr($expr); + $exprStringLines = preg_split('~\R~', $exprString, 2); + if ($exprStringLines !== false && count($exprStringLines) > 1) { + $exprString = $exprStringLines[0] . '…'; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Expression "%s" on a separate line does not do anything.', + $exprString, + ))->line($expr->getStartLine()) + ->identifier('expr.resultUnused') + ->build(), + ]; + } + +} diff --git a/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php new file mode 100644 index 0000000000..ab14e74d36 --- /dev/null +++ b/src/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRule.php @@ -0,0 +1,54 @@ + + */ +class CallToConstructorStatementWithoutImpurePointsRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classesWithConstructors = []; + foreach ($node->get(ConstructorWithoutImpurePointsCollector::class) as [$class]) { + $classesWithConstructors[strtolower($class)] = $class; + } + + $errors = []; + foreach ($node->get(PossiblyPureNewCollector::class) as $filePath => $data) { + foreach ($data as [$class, $line]) { + $lowerClass = strtolower($class); + if (!array_key_exists($lowerClass, $classesWithConstructors)) { + continue; + } + + $originalClassName = $classesWithConstructors[$lowerClass]; + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to new %s() on a separate line has no effect.', + $originalClassName, + ))->file($filePath) + ->line($line) + ->identifier('new.resultUnused') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php new file mode 100644 index 0000000000..1635b12050 --- /dev/null +++ b/src/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRule.php @@ -0,0 +1,54 @@ + + */ +class CallToFunctionStatementWithoutImpurePointsRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $functions = []; + foreach ($node->get(FunctionWithoutImpurePointsCollector::class) as [$functionName]) { + $functions[strtolower($functionName)] = $functionName; + } + + $errors = []; + foreach ($node->get(PossiblyPureFuncCallCollector::class) as $filePath => $data) { + foreach ($data as [$func, $line]) { + $lowerFunc = strtolower($func); + if (!array_key_exists($lowerFunc, $functions)) { + continue; + } + + $originalFunctionName = $functions[$lowerFunc]; + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to function %s() on a separate line has no effect.', + $originalFunctionName, + ))->file($filePath) + ->line($line) + ->identifier('function.resultUnused') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRule.php new file mode 100644 index 0000000000..5653e1449b --- /dev/null +++ b/src/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRule.php @@ -0,0 +1,68 @@ + + */ +class CallToMethodStatementWithoutImpurePointsRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $methods = []; + foreach ($node->get(MethodWithoutImpurePointsCollector::class) as $collected) { + foreach ($collected as [$className, $methodName, $classDisplayName]) { + $className = strtolower($className); + + if (!array_key_exists($className, $methods)) { + $methods[$className] = []; + } + $methods[$className][strtolower($methodName)] = $classDisplayName . '::' . $methodName; + } + } + + $errors = []; + foreach ($node->get(PossiblyPureMethodCallCollector::class) as $filePath => $data) { + foreach ($data as [$className, $method, $line]) { + $className = strtolower($className); + + if (!array_key_exists($className, $methods)) { + continue; + } + + $lowerMethod = strtolower($method); + if (!array_key_exists($lowerMethod, $methods[$className])) { + continue; + } + + $originalMethodName = $methods[$className][$lowerMethod]; + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to method %s() on a separate line has no effect.', + $originalMethodName, + ))->file($filePath) + ->line($line) + ->identifier('method.resultUnused') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php b/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php new file mode 100644 index 0000000000..c64b29f8bf --- /dev/null +++ b/src/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRule.php @@ -0,0 +1,68 @@ + + */ +class CallToStaticMethodStatementWithoutImpurePointsRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $methods = []; + foreach ($node->get(MethodWithoutImpurePointsCollector::class) as $collected) { + foreach ($collected as [$className, $methodName, $classDisplayName]) { + $lowerClassName = strtolower($className); + + if (!array_key_exists($lowerClassName, $methods)) { + $methods[$lowerClassName] = []; + } + $methods[$lowerClassName][strtolower($methodName)] = $classDisplayName . '::' . $methodName; + } + } + + $errors = []; + foreach ($node->get(PossiblyPureStaticCallCollector::class) as $filePath => $data) { + foreach ($data as [$className, $method, $line]) { + $lowerClassName = strtolower($className); + + if (!array_key_exists($lowerClassName, $methods)) { + continue; + } + + $lowerMethod = strtolower($method); + if (!array_key_exists($lowerMethod, $methods[$lowerClassName])) { + continue; + } + + $originalMethodName = $methods[$lowerClassName][$lowerMethod]; + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to %s() on a separate line has no effect.', + $originalMethodName, + ))->file($filePath) + ->line($line) + ->identifier('staticMethod.resultUnused') + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php b/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php new file mode 100644 index 0000000000..dae162500a --- /dev/null +++ b/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php @@ -0,0 +1,59 @@ + + */ +class ConstructorWithoutImpurePointsCollector implements Collector +{ + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope) + { + $method = $node->getMethodReflection(); + if (strtolower($method->getName()) !== '__construct') { + return null; + } + + if (!$method->isPure()->maybe()) { + return null; + } + + if (count($node->getImpurePoints()) !== 0) { + return null; + } + + if (count($node->getStatementResult()->getThrowPoints()) !== 0) { + return null; + } + + $variant = ParametersAcceptorSelector::selectSingle($method->getVariants()); + foreach ($variant->getParameters() as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + return null; + } + + if (count($method->getAsserts()->getAll()) !== 0) { + return null; + } + + return $method->getDeclaringClass()->getName(); + } + +} diff --git a/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php b/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php new file mode 100644 index 0000000000..528e5c76e6 --- /dev/null +++ b/src/Rules/DeadCode/FunctionWithoutImpurePointsCollector.php @@ -0,0 +1,57 @@ + + */ +class FunctionWithoutImpurePointsCollector implements Collector +{ + + public function getNodeType(): string + { + return FunctionReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope) + { + $function = $node->getFunctionReflection(); + if (!$function->isPure()->maybe()) { + return null; + } + if (!$function->hasSideEffects()->maybe()) { + return null; + } + + if (count($node->getImpurePoints()) !== 0) { + return null; + } + + if (count($node->getStatementResult()->getThrowPoints()) !== 0) { + return null; + } + + $variant = ParametersAcceptorSelector::selectSingle($function->getVariants()); + foreach ($variant->getParameters() as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + return null; + } + + if (count($function->getAsserts()->getAll()) !== 0) { + return null; + } + + return $function->getName(); + } + +} diff --git a/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php b/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php new file mode 100644 index 0000000000..b6bdb3ca34 --- /dev/null +++ b/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php @@ -0,0 +1,65 @@ + + */ +class MethodWithoutImpurePointsCollector implements Collector +{ + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope) + { + $method = $node->getMethodReflection(); + if (!$method->isPure()->maybe()) { + return null; + } + if (!$method->hasSideEffects()->maybe()) { + return null; + } + + if (count($node->getImpurePoints()) !== 0) { + return null; + } + + if (count($node->getStatementResult()->getThrowPoints()) !== 0) { + return null; + } + + $variant = ParametersAcceptorSelector::selectSingle($method->getVariants()); + foreach ($variant->getParameters() as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + return null; + } + + if (count($method->getAsserts()->getAll()) !== 0) { + return null; + } + + $declaringClass = $method->getDeclaringClass(); + if ( + $declaringClass->hasConstructor() + && $declaringClass->getConstructor()->getName() === $method->getName() + ) { + return null; + } + + return [$method->getDeclaringClass()->getName(), $method->getName(), $method->getDeclaringClass()->getDisplayName()]; + } + +} diff --git a/src/Rules/DeadCode/NoopRule.php b/src/Rules/DeadCode/NoopRule.php index cd18fda0ca..b05eb401f9 100644 --- a/src/Rules/DeadCode/NoopRule.php +++ b/src/Rules/DeadCode/NoopRule.php @@ -15,7 +15,7 @@ class NoopRule implements Rule { - public function __construct(private ExprPrinter $exprPrinter) + public function __construct(private ExprPrinter $exprPrinter, private bool $better) { } @@ -26,6 +26,10 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { + if ($this->better) { + // disabled in bleeding edge + return []; + } $originalExpr = $node->expr; $expr = $originalExpr; if ( @@ -36,18 +40,8 @@ public function processNode(Node $node, Scope $scope): array ) { $expr = $expr->expr; } - if ( - !$expr instanceof Node\Expr\Variable - && !$expr instanceof Node\Expr\PropertyFetch - && !$expr instanceof Node\Expr\StaticPropertyFetch - && !$expr instanceof Node\Expr\NullsafePropertyFetch - && !$expr instanceof Node\Expr\ArrayDimFetch - && !$expr instanceof Node\Scalar - && !$expr instanceof Node\Expr\Isset_ - && !$expr instanceof Node\Expr\Empty_ - && !$expr instanceof Node\Expr\ConstFetch - && !$expr instanceof Node\Expr\ClassConstFetch - ) { + + if (!$this->isNoopExpr($expr)) { return []; } @@ -55,14 +49,24 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Expression "%s" on a separate line does not do anything.', $this->exprPrinter->printExpr($originalExpr), - ))->line($expr->getLine()) - ->identifier('deadCode.noopExpression') - ->metadata([ - 'depth' => $node->getAttribute('statementDepth'), - 'order' => $node->getAttribute('statementOrder'), - ]) + ))->line($expr->getStartLine()) + ->identifier('expr.resultUnused') ->build(), ]; } + public function isNoopExpr(Node\Expr $expr): bool + { + return $expr instanceof Node\Expr\Variable + || $expr instanceof Node\Expr\PropertyFetch + || $expr instanceof Node\Expr\StaticPropertyFetch + || $expr instanceof Node\Expr\NullsafePropertyFetch + || $expr instanceof Node\Expr\ArrayDimFetch + || $expr instanceof Node\Scalar + || $expr instanceof Node\Expr\Isset_ + || $expr instanceof Node\Expr\Empty_ + || $expr instanceof Node\Expr\ConstFetch + || $expr instanceof Node\Expr\ClassConstFetch; + } + } diff --git a/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php b/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php new file mode 100644 index 0000000000..952da73ba2 --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php @@ -0,0 +1,50 @@ + + */ +class PossiblyPureFuncCallCollector implements Collector +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Expression::class; + } + + public function processNode(Node $node, Scope $scope) + { + if (!$node->expr instanceof Node\Expr\FuncCall) { + return null; + } + if (!$node->expr->name instanceof Node\Name) { + return null; + } + + if (!$this->reflectionProvider->hasFunction($node->expr->name, $scope)) { + return null; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->expr->name, $scope); + if (!$functionReflection->isPure()->maybe()) { + return null; + } + if (!$functionReflection->hasSideEffects()->maybe()) { + return null; + } + + return [$functionReflection->getName(), $node->getStartLine()]; + } + +} diff --git a/src/Rules/DeadCode/PossiblyPureMethodCallCollector.php b/src/Rules/DeadCode/PossiblyPureMethodCallCollector.php new file mode 100644 index 0000000000..d63f25d263 --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureMethodCallCollector.php @@ -0,0 +1,65 @@ + + */ +class PossiblyPureMethodCallCollector implements Collector +{ + + public function __construct() + { + } + + public function getNodeType(): string + { + return Expression::class; + } + + public function processNode(Node $node, Scope $scope) + { + if (!$node->expr instanceof Node\Expr\MethodCall) { + return null; + } + if (!$node->expr->name instanceof Node\Identifier) { + return null; + } + + $methodName = $node->expr->name->toString(); + $calledOnType = $scope->getType($node->expr->var); + $methodReflection = $scope->getMethodReflection($calledOnType, $methodName); + if ($methodReflection === null) { + return null; + } + if ( + !$methodReflection->isPrivate() + && !$methodReflection->isFinal()->yes() + && !$methodReflection->getDeclaringClass()->isFinal() + ) { + $typeClassReflections = $calledOnType->getObjectClassReflections(); + if (count($typeClassReflections) !== 1) { + return null; + } + + if (!$typeClassReflections[0]->isFinal()) { + return null; + } + } + if (!$methodReflection->isPure()->maybe()) { + return null; + } + if (!$methodReflection->hasSideEffects()->maybe()) { + return null; + } + + return [$methodReflection->getDeclaringClass()->getName(), $methodReflection->getName(), $node->getStartLine()]; + } + +} diff --git a/src/Rules/DeadCode/PossiblyPureNewCollector.php b/src/Rules/DeadCode/PossiblyPureNewCollector.php new file mode 100644 index 0000000000..e2fabe49ca --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureNewCollector.php @@ -0,0 +1,60 @@ + + */ +class PossiblyPureNewCollector implements Collector +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Expression::class; + } + + public function processNode(Node $node, Scope $scope) + { + if (!$node->expr instanceof Node\Expr\New_) { + return null; + } + + if (!$node->expr->class instanceof Node\Name) { + return null; + } + + $className = $node->expr->class->toString(); + + if (!$this->reflectionProvider->hasClass($className)) { + return null; + } + + $classReflection = $this->reflectionProvider->getClass($className); + if (!$classReflection->hasConstructor()) { + return null; + } + + $constructor = $classReflection->getConstructor(); + if (strtolower($constructor->getName()) !== '__construct') { + return null; + } + + if (!$constructor->isPure()->maybe()) { + return null; + } + + return [$constructor->getDeclaringClass()->getName(), $node->getStartLine()]; + } + +} diff --git a/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php b/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php new file mode 100644 index 0000000000..d934b57a6d --- /dev/null +++ b/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php @@ -0,0 +1,55 @@ + + */ +class PossiblyPureStaticCallCollector implements Collector +{ + + public function __construct() + { + } + + public function getNodeType(): string + { + return Expression::class; + } + + public function processNode(Node $node, Scope $scope) + { + if (!$node->expr instanceof Node\Expr\StaticCall) { + return null; + } + if (!$node->expr->name instanceof Node\Identifier) { + return null; + } + + if (!$node->expr->class instanceof Node\Name) { + return null; + } + + $methodName = $node->expr->name->toString(); + $calledOnType = $scope->resolveTypeByName($node->expr->class); + $methodReflection = $scope->getMethodReflection($calledOnType, $methodName); + + if ($methodReflection === null) { + return null; + } + if (!$methodReflection->isPure()->maybe()) { + return null; + } + if (!$methodReflection->hasSideEffects()->maybe()) { + return null; + } + + return [$methodReflection->getDeclaringClass()->getName(), $methodReflection->getName(), $node->getStartLine()]; + } + +} diff --git a/src/Rules/DeadCode/UnreachableStatementRule.php b/src/Rules/DeadCode/UnreachableStatementRule.php index 8f4793267a..f1b226ad57 100644 --- a/src/Rules/DeadCode/UnreachableStatementRule.php +++ b/src/Rules/DeadCode/UnreachableStatementRule.php @@ -21,17 +21,9 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if ($node->getOriginalStatement() instanceof Node\Stmt\Nop) { - return []; - } - return [ RuleErrorBuilder::message('Unreachable statement - code above always terminates.') - ->identifier('deadCode.unreachableStatement') - ->metadata([ - 'depth' => $node->getAttribute('statementDepth'), - 'order' => $node->getAttribute('statementOrder'), - ]) + ->identifier('deadCode.unreachable') ->build(), ]; } diff --git a/src/Rules/DeadCode/UnusedPrivateConstantRule.php b/src/Rules/DeadCode/UnusedPrivateConstantRule.php index c41e3d66eb..ccb160f60f 100644 --- a/src/Rules/DeadCode/UnusedPrivateConstantRule.php +++ b/src/Rules/DeadCode/UnusedPrivateConstantRule.php @@ -8,8 +8,7 @@ use PHPStan\Rules\Constants\AlwaysUsedClassConstantsExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; -use PHPStan\Type\TypeWithClassName; +use PHPStan\Type\ObjectType; use function sprintf; /** @@ -32,11 +31,9 @@ public function processNode(Node $node, Scope $scope): array if (!$node->getClass() instanceof Node\Stmt\Class_ && !$node->getClass() instanceof Node\Stmt\Enum_) { return []; } - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); + $classType = new ObjectType($classReflection->getName(), null, $classReflection); $constants = []; foreach ($node->getConstants() as $constant) { @@ -60,24 +57,35 @@ public function processNode(Node $node, Scope $scope): array foreach ($node->getFetches() as $fetch) { $fetchNode = $fetch->getNode(); + + $fetchScope = $fetch->getScope(); + if ($fetchNode->class instanceof Node\Name) { + $fetchedOnClass = $fetchScope->resolveTypeByName($fetchNode->class); + } else { + $fetchedOnClass = $fetchScope->getType($fetchNode->class); + } + if (!$fetchNode->name instanceof Node\Identifier) { + if (!$classType->isSuperTypeOf($fetchedOnClass)->no()) { + $constants = []; + break; + } continue; } - if ($fetchNode->class instanceof Node\Name) { - $fetchScope = $fetch->getScope(); - $fetchedOnClass = $fetchScope->resolveName($fetchNode->class); - if ($fetchedOnClass !== $classReflection->getName()) { - continue; - } - } else { - $classExprType = $fetch->getScope()->getType($fetchNode->class); - if (!$classExprType instanceof TypeWithClassName) { - continue; + $constantReflection = $fetchScope->getConstantReflection($fetchedOnClass, $fetchNode->name->toString()); + if ($constantReflection === null) { + if (!$classType->isSuperTypeOf($fetchedOnClass)->no()) { + unset($constants[$fetchNode->name->toString()]); } - if ($classExprType->getClassName() !== $classReflection->getName()) { - continue; + continue; + } + + if ($constantReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { + if (!$classType->isSuperTypeOf($fetchedOnClass)->no()) { + unset($constants[$fetchNode->name->toString()]); } + continue; } unset($constants[$fetchNode->name->toString()]); @@ -86,14 +94,8 @@ public function processNode(Node $node, Scope $scope): array $errors = []; foreach ($constants as $constantName => $constantNode) { $errors[] = RuleErrorBuilder::message(sprintf('Constant %s::%s is unused.', $classReflection->getDisplayName(), $constantName)) - ->line($constantNode->getLine()) - ->identifier('deadCode.unusedClassConstant') - ->metadata([ - 'classOrder' => $node->getClass()->getAttribute('statementOrder'), - 'classDepth' => $node->getClass()->getAttribute('statementDepth'), - 'classStartLine' => $node->getClass()->getStartLine(), - 'constantName' => $constantName, - ]) + ->line($constantNode->getStartLine()) + ->identifier('classConstant.unused') ->tip(sprintf('See: %s', 'https://phpstan.org/developing-extensions/always-used-class-constants')) ->build(); } diff --git a/src/Rules/DeadCode/UnusedPrivateMethodRule.php b/src/Rules/DeadCode/UnusedPrivateMethodRule.php index 84709f06a8..aa868808d4 100644 --- a/src/Rules/DeadCode/UnusedPrivateMethodRule.php +++ b/src/Rules/DeadCode/UnusedPrivateMethodRule.php @@ -7,14 +7,11 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\ClassMethodsNode; use PHPStan\Reflection\MethodReflection; +use PHPStan\Rules\Methods\AlwaysUsedMethodExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; -use PHPStan\Type\TypeUtils; use function array_map; use function count; use function sprintf; @@ -26,6 +23,10 @@ class UnusedPrivateMethodRule implements Rule { + public function __construct(private AlwaysUsedMethodExtensionProvider $extensionProvider) + { + } + public function getNodeType(): string { return ClassMethodsNode::class; @@ -36,15 +37,12 @@ public function processNode(Node $node, Scope $scope): array if (!$node->getClass() instanceof Node\Stmt\Class_ && !$node->getClass() instanceof Node\Stmt\Enum_) { return []; } - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); + $classType = new ObjectType($classReflection->getName(), null, $classReflection); $constructor = null; if ($classReflection->hasConstructor()) { $constructor = $classReflection->getConstructor(); } - $classType = new ObjectType($classReflection->getName()); $methods = []; foreach ($node->getMethods() as $method) { @@ -61,7 +59,15 @@ public function processNode(Node $node, Scope $scope): array if (strtolower($methodName) === '__clone') { continue; } - $methods[$methodName] = $method; + + $methodReflection = $classReflection->getNativeMethod($methodName); + foreach ($this->extensionProvider->getExtensions() as $extension) { + if ($extension->isAlwaysUsed($methodReflection)) { + continue 2; + } + } + + $methods[strtolower($methodName)] = $method; } $arrayCalls = []; @@ -76,7 +82,7 @@ public function processNode(Node $node, Scope $scope): array $methodNames = [$methodCallNode->name->toString()]; } else { $methodNameType = $callScope->getType($methodCallNode->name); - $strings = TypeUtils::getConstantStrings($methodNameType); + $strings = $methodNameType->getConstantStrings(); if (count($strings) === 0) { return []; } @@ -87,27 +93,36 @@ public function processNode(Node $node, Scope $scope): array if ($methodCallNode instanceof Node\Expr\MethodCall) { $calledOnType = $callScope->getType($methodCallNode->var); } else { - if (!$methodCallNode->class instanceof Node\Name) { - continue; + if ($methodCallNode->class instanceof Node\Name) { + $calledOnType = $callScope->resolveTypeByName($methodCallNode->class); + } else { + $calledOnType = $callScope->getType($methodCallNode->class); } - $calledOnType = $scope->resolveTypeByName($methodCallNode->class); - } - if ($classType->isSuperTypeOf($calledOnType)->no()) { - continue; - } - if ($calledOnType instanceof MixedType) { - continue; } + $inMethod = $callScope->getFunction(); if (!$inMethod instanceof MethodReflection) { continue; } foreach ($methodNames as $methodName) { + $methodReflection = $callScope->getMethodReflection($calledOnType, $methodName); + if ($methodReflection === null) { + if (!$classType->isSuperTypeOf($calledOnType)->no()) { + unset($methods[strtolower($methodName)]); + } + continue; + } + if ($methodReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { + if (!$classType->isSuperTypeOf($calledOnType)->no()) { + unset($methods[strtolower($methodName)]); + } + continue; + } if ($inMethod->getName() === $methodName) { continue; } - unset($methods[$methodName]); + unset($methods[strtolower($methodName)]); } } @@ -116,51 +131,52 @@ public function processNode(Node $node, Scope $scope): array /** @var Node\Expr\Array_ $array */ $array = $arrayCall->getNode(); $arrayScope = $arrayCall->getScope(); - $arrayType = $scope->getType($array); - if (!$arrayType instanceof ConstantArrayType) { + $arrayType = $arrayScope->getType($array); + if (!$arrayType->isCallable()->yes()) { continue; } - foreach ($arrayType->findTypeAndMethodNames() as $typeAndMethod) { - if ($typeAndMethod->isUnknown()) { - return []; - } - if (!$typeAndMethod->getCertainty()->yes()) { - return []; - } - $calledOnType = $typeAndMethod->getType(); - if ($classType->isSuperTypeOf($calledOnType)->no()) { - continue; - } - if ($calledOnType instanceof MixedType) { - continue; - } - $inMethod = $arrayScope->getFunction(); - if (!$inMethod instanceof MethodReflection) { - continue; - } - if ($inMethod->getName() === $typeAndMethod->getMethod()) { - continue; + foreach ($arrayType->getConstantArrays() as $constantArray) { + foreach ($constantArray->findTypeAndMethodNames() as $typeAndMethod) { + if ($typeAndMethod->isUnknown()) { + return []; + } + if (!$typeAndMethod->getCertainty()->yes()) { + return []; + } + + $calledOnType = $typeAndMethod->getType(); + $methodReflection = $arrayScope->getMethodReflection($calledOnType, $typeAndMethod->getMethod()); + if ($methodReflection === null) { + continue; + } + + if ($methodReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { + continue; + } + + $inMethod = $arrayScope->getFunction(); + if (!$inMethod instanceof MethodReflection) { + continue; + } + if ($inMethod->getName() === $typeAndMethod->getMethod()) { + continue; + } + unset($methods[strtolower($typeAndMethod->getMethod())]); } - unset($methods[$typeAndMethod->getMethod()]); } } } $errors = []; - foreach ($methods as $methodName => $method) { + foreach ($methods as $method) { + $originalMethodName = $method->getNode()->name->toString(); $methodType = 'Method'; if ($method->getNode()->isStatic()) { $methodType = 'Static method'; } - $errors[] = RuleErrorBuilder::message(sprintf('%s %s::%s() is unused.', $methodType, $classReflection->getDisplayName(), $methodName)) - ->line($method->getNode()->getLine()) - ->identifier('deadCode.unusedMethod') - ->metadata([ - 'classOrder' => $node->getClass()->getAttribute('statementOrder'), - 'classDepth' => $node->getClass()->getAttribute('statementDepth'), - 'classStartLine' => $node->getClass()->getStartLine(), - 'methodName' => $methodName, - ]) + $errors[] = RuleErrorBuilder::message(sprintf('%s %s::%s() is unused.', $methodType, $classReflection->getDisplayName(), $originalMethodName)) + ->line($method->getNode()->getStartLine()) + ->identifier('method.unused') ->build(); } diff --git a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php index a829883b14..6af054659d 100644 --- a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php +++ b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php @@ -9,16 +9,13 @@ use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; -use PHPStan\Type\TypeUtils; use function array_key_exists; use function array_map; use function count; use function sprintf; -use function strpos; +use function str_contains; /** * @implements Rule @@ -49,12 +46,8 @@ public function processNode(Node $node, Scope $scope): array if (!$node->getClass() instanceof Node\Stmt\Class_) { return []; } - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - $classReflection = $scope->getClassReflection(); - $classType = new ObjectType($classReflection->getName()); - + $classReflection = $node->getClassReflection(); + $classType = new ObjectType($classReflection->getName(), null, $classReflection); $properties = []; foreach ($node->getProperties() as $property) { if (!$property->isPrivate()) { @@ -69,7 +62,7 @@ public function processNode(Node $node, Scope $scope): array if ($property->getPhpDoc() !== null) { $text = $property->getPhpDoc(); foreach ($this->alwaysReadTags as $tag) { - if (strpos($text, $tag) === false) { + if (!str_contains($text, $tag)) { continue; } @@ -78,7 +71,7 @@ public function processNode(Node $node, Scope $scope): array } foreach ($this->alwaysWrittenTags as $tag) { - if (strpos($text, $tag) === false) { + if (!str_contains($text, $tag)) { continue; } @@ -125,7 +118,7 @@ public function processNode(Node $node, Scope $scope): array $propertyNames = [$fetch->name->toString()]; } else { $propertyNameType = $usage->getScope()->getType($fetch->name); - $strings = TypeUtils::getConstantStrings($propertyNameType); + $strings = $propertyNameType->getConstantStrings(); if (count($strings) === 0) { return []; } @@ -135,24 +128,39 @@ public function processNode(Node $node, Scope $scope): array if ($fetch instanceof Node\Expr\PropertyFetch) { $fetchedOnType = $usage->getScope()->getType($fetch->var); } else { - if (!$fetch->class instanceof Node\Name) { - continue; + if ($fetch->class instanceof Node\Name) { + $fetchedOnType = $usage->getScope()->resolveTypeByName($fetch->class); + } else { + $fetchedOnType = $usage->getScope()->getType($fetch->class); } - - $fetchedOnType = $usage->getScope()->resolveTypeByName($fetch->class); - } - - if ($classType->isSuperTypeOf($fetchedOnType)->no()) { - continue; - } - if ($fetchedOnType instanceof MixedType) { - continue; } foreach ($propertyNames as $propertyName) { if (!array_key_exists($propertyName, $properties)) { continue; } + $propertyReflection = $usage->getScope()->getPropertyReflection($fetchedOnType, $propertyName); + if ($propertyReflection === null) { + if (!$classType->isSuperTypeOf($fetchedOnType)->no()) { + if ($usage instanceof PropertyRead) { + $properties[$propertyName]['read'] = true; + } else { + $properties[$propertyName]['written'] = true; + } + } + continue; + } + if ($propertyReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { + if (!$classType->isSuperTypeOf($fetchedOnType)->no()) { + if ($usage instanceof PropertyRead) { + $properties[$propertyName]['read'] = true; + } else { + $properties[$propertyName]['written'] = true; + } + } + continue; + } + if ($usage instanceof PropertyRead) { $properties[$propertyName]['read'] = true; } else { @@ -167,29 +175,31 @@ public function processNode(Node $node, Scope $scope): array foreach ($properties as $name => $data) { $propertyNode = $data['node']; if ($propertyNode->isStatic()) { - $propertyName = sprintf('Static property %s::$%s', $scope->getClassReflection()->getDisplayName(), $name); + $propertyName = sprintf('Static property %s::$%s', $classReflection->getDisplayName(), $name); } else { - $propertyName = sprintf('Property %s::$%s', $scope->getClassReflection()->getDisplayName(), $name); + $propertyName = sprintf('Property %s::$%s', $classReflection->getDisplayName(), $name); } $tip = sprintf('See: %s', 'https://phpstan.org/developing-extensions/always-read-written-properties'); if (!$data['read']) { if (!$data['written']) { $errors[] = RuleErrorBuilder::message(sprintf('%s is unused.', $propertyName)) ->line($propertyNode->getStartLine()) - ->identifier('deadCode.unusedProperty') - ->metadata([ - 'classOrder' => $node->getClass()->getAttribute('statementOrder'), - 'classDepth' => $node->getClass()->getAttribute('statementDepth'), - 'classStartLine' => $node->getClass()->getStartLine(), - 'propertyName' => $name, - ]) ->tip($tip) + ->identifier('property.unused') ->build(); } else { - $errors[] = RuleErrorBuilder::message(sprintf('%s is never read, only written.', $propertyName))->line($propertyNode->getStartLine())->tip($tip)->build(); + $errors[] = RuleErrorBuilder::message(sprintf('%s is never read, only written.', $propertyName)) + ->line($propertyNode->getStartLine()) + ->identifier('property.onlyWritten') + ->tip($tip) + ->build(); } } elseif (!$data['written'] && (!array_key_exists($name, $uninitializedProperties) || !$this->checkUninitializedProperties)) { - $errors[] = RuleErrorBuilder::message(sprintf('%s is never written, only read.', $propertyName))->line($propertyNode->getStartLine())->tip($tip)->build(); + $errors[] = RuleErrorBuilder::message(sprintf('%s is never written, only read.', $propertyName)) + ->line($propertyNode->getStartLine()) + ->identifier('property.onlyRead') + ->tip($tip) + ->build(); } } diff --git a/src/Rules/Debug/DumpTypeRule.php b/src/Rules/Debug/DumpTypeRule.php index ceeeefa0e5..db1020b18a 100644 --- a/src/Rules/Debug/DumpTypeRule.php +++ b/src/Rules/Debug/DumpTypeRule.php @@ -43,11 +43,7 @@ public function processNode(Node $node, Scope $scope): array } if (count($node->getArgs()) === 0) { - return [ - RuleErrorBuilder::message(sprintf('Missing argument for %s() function call.', $functionName)) - ->nonIgnorable() - ->build(), - ]; + return []; } return [ @@ -56,7 +52,7 @@ public function processNode(Node $node, Scope $scope): array 'Dumped type: %s', $scope->getType($node->getArgs()[0]->value)->describe(VerbosityLevel::precise()), ), - )->nonIgnorable()->build(), + )->nonIgnorable()->identifier('phpstan.dumpType')->build(), ]; } diff --git a/src/Rules/Debug/FileAssertRule.php b/src/Rules/Debug/FileAssertRule.php index d8ebdbfa4a..e0899850c3 100644 --- a/src/Rules/Debug/FileAssertRule.php +++ b/src/Rules/Debug/FileAssertRule.php @@ -6,11 +6,10 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\TrinaryLogic; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\VerbosityLevel; use function count; use function is_string; @@ -59,7 +58,7 @@ public function processNode(Node $node, Scope $scope): array /** * @param Node\Arg[] $args - * @return RuleError[] + * @return list */ private function processAssertType(array $args, Scope $scope): array { @@ -67,26 +66,32 @@ private function processAssertType(array $args, Scope $scope): array return []; } - $expectedTypeString = $scope->getType($args[0]->value); - if (!$expectedTypeString instanceof ConstantStringType) { + $expectedTypeStrings = $scope->getType($args[0]->value)->getConstantStrings(); + if (count($expectedTypeStrings) !== 1) { return [ - RuleErrorBuilder::message('Expected type must be a literal string.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Expected type must be a literal string.') + ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') + ->build(), ]; } $expressionType = $scope->getType($args[1]->value)->describe(VerbosityLevel::precise()); - if ($expectedTypeString->getValue() === $expressionType) { + if ($expectedTypeStrings[0]->getValue() === $expressionType) { return []; } return [ - RuleErrorBuilder::message(sprintf('Expected type %s, actual: %s', $expectedTypeString->getValue(), $expressionType))->nonIgnorable()->build(), + RuleErrorBuilder::message(sprintf('Expected type %s, actual: %s', $expectedTypeStrings[0]->getValue(), $expressionType)) + ->nonIgnorable() + ->identifier('phpstan.type') + ->build(), ]; } /** * @param Node\Arg[] $args - * @return RuleError[] + * @return list */ private function processAssertNativeType(array $args, Scope $scope): array { @@ -94,27 +99,32 @@ private function processAssertNativeType(array $args, Scope $scope): array return []; } - $scope = $scope->doNotTreatPhpDocTypesAsCertain(); - $expectedTypeString = $scope->getNativeType($args[0]->value); - if (!$expectedTypeString instanceof ConstantStringType) { + $expectedTypeStrings = $scope->getNativeType($args[0]->value)->getConstantStrings(); + if (count($expectedTypeStrings) !== 1) { return [ - RuleErrorBuilder::message('Expected native type must be a literal string.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Expected native type must be a literal string.') + ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') + ->build(), ]; } $expressionType = $scope->getNativeType($args[1]->value)->describe(VerbosityLevel::precise()); - if ($expectedTypeString->getValue() === $expressionType) { + if ($expectedTypeStrings[0]->getValue() === $expressionType) { return []; } return [ - RuleErrorBuilder::message(sprintf('Expected native type %s, actual: %s', $expectedTypeString->getValue(), $expressionType))->nonIgnorable()->build(), + RuleErrorBuilder::message(sprintf('Expected native type %s, actual: %s', $expectedTypeStrings[0]->getValue(), $expressionType)) + ->nonIgnorable() + ->identifier('phpstan.nativeType') + ->build(), ]; } /** * @param Node\Arg[] $args - * @return RuleError[] + * @return list */ private function processAssertVariableCertainty(array $args, Scope $scope): array { @@ -127,6 +137,7 @@ private function processAssertVariableCertainty(array $args, Scope $scope): arra return [ RuleErrorBuilder::message('First argument of %s() must be TrinaryLogic call') ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') ->build(), ]; } @@ -134,6 +145,7 @@ private function processAssertVariableCertainty(array $args, Scope $scope): arra return [ RuleErrorBuilder::message('Invalid TrinaryLogic call.') ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') ->build(), ]; } @@ -142,6 +154,7 @@ private function processAssertVariableCertainty(array $args, Scope $scope): arra return [ RuleErrorBuilder::message('Invalid TrinaryLogic call.') ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') ->build(), ]; } @@ -150,17 +163,19 @@ private function processAssertVariableCertainty(array $args, Scope $scope): arra return [ RuleErrorBuilder::message('Invalid TrinaryLogic call.') ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') ->build(), ]; } - // @phpstan-ignore-next-line + // @phpstan-ignore staticMethod.dynamicName $expectedCertaintyValue = TrinaryLogic::{$certainty->name->toString()}(); $variable = $args[1]->value; if (!$variable instanceof Node\Expr\Variable) { return [ RuleErrorBuilder::message('Invalid assertVariableCertainty call.') ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') ->build(), ]; } @@ -168,6 +183,7 @@ private function processAssertVariableCertainty(array $args, Scope $scope): arra return [ RuleErrorBuilder::message('Invalid assertVariableCertainty call.') ->nonIgnorable() + ->identifier('phpstan.unknownExpectation') ->build(), ]; } @@ -178,7 +194,10 @@ private function processAssertVariableCertainty(array $args, Scope $scope): arra } return [ - RuleErrorBuilder::message(sprintf('Expected variable certainty %s, actual: %s', $expectedCertaintyValue->describe(), $actualCertaintyValue->describe()))->nonIgnorable()->build(), + RuleErrorBuilder::message(sprintf('Expected variable certainty %s, actual: %s', $expectedCertaintyValue->describe(), $actualCertaintyValue->describe())) + ->nonIgnorable() + ->identifier('phpstan.variable') + ->build(), ]; } diff --git a/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php b/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php index 4cbf0a7d92..267d8c8200 100644 --- a/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php +++ b/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php @@ -17,6 +17,13 @@ class CatchWithUnthrownExceptionRule implements Rule { + public function __construct( + private ExceptionTypeResolver $exceptionTypeResolver, + private bool $reportUncheckedExceptionDeadCatch, + ) + { + } + public function getNodeType(): string { return CatchWithUnthrownExceptionNode::class; @@ -28,14 +35,34 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( sprintf('Dead catch - %s is already caught above.', $node->getOriginalCaughtType()->describe(VerbosityLevel::typeOnly())), - )->line($node->getLine())->build(), + ) + ->line($node->getStartLine()) + ->identifier('catch.alreadyCaught') + ->build(), ]; } + if (!$this->reportUncheckedExceptionDeadCatch) { + $isCheckedException = false; + foreach ($node->getCaughtType()->getObjectClassNames() as $objectClassName) { + if ($this->exceptionTypeResolver->isCheckedException($objectClassName, $scope)) { + $isCheckedException = true; + break; + } + } + + if (!$isCheckedException) { + return []; + } + } + return [ RuleErrorBuilder::message( sprintf('Dead catch - %s is never thrown in the try block.', $node->getCaughtType()->describe(VerbosityLevel::typeOnly())), - )->line($node->getLine())->build(), + ) + ->line($node->getStartLine()) + ->identifier('catch.neverThrown') + ->build(), ]; } diff --git a/src/Rules/Exceptions/CaughtExceptionExistenceRule.php b/src/Rules/Exceptions/CaughtExceptionExistenceRule.php index bbd4b6be44..1231a0d5e2 100644 --- a/src/Rules/Exceptions/CaughtExceptionExistenceRule.php +++ b/src/Rules/Exceptions/CaughtExceptionExistenceRule.php @@ -6,7 +6,7 @@ use PhpParser\Node\Stmt\Catch_; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -22,7 +22,7 @@ class CaughtExceptionExistenceRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $checkClassCaseSensitivity, ) { @@ -42,22 +42,28 @@ public function processNode(Node $node, Scope $scope): array if ($scope->isInClassExists($className)) { continue; } - $errors[] = RuleErrorBuilder::message(sprintf('Caught class %s not found.', $className))->line($class->getLine())->discoveringSymbolsTip()->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Caught class %s not found.', $className)) + ->line($class->getStartLine()) + ->identifier('class.notFound') + ->discoveringSymbolsTip() + ->build(); continue; } $classReflection = $this->reflectionProvider->getClass($className); if (!$classReflection->isInterface() && !$classReflection->implementsInterface(Throwable::class)) { - $errors[] = RuleErrorBuilder::message(sprintf('Caught class %s is not an exception.', $classReflection->getDisplayName()))->line($class->getLine())->build(); - } - - if (!$this->checkClassCaseSensitivity) { - continue; + $errors[] = RuleErrorBuilder::message(sprintf('Caught class %s is not an exception.', $classReflection->getDisplayName())) + ->line($class->getStartLine()) + ->identifier('catch.notThrowable') + ->build(); } $errors = array_merge( $errors, - $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($className, $class)]), + $this->classCheck->checkClassNames( + [new ClassNameNodePair($className, $class)], + $this->checkClassCaseSensitivity, + ), ); } diff --git a/src/Rules/Exceptions/ExceptionTypeResolver.php b/src/Rules/Exceptions/ExceptionTypeResolver.php index 83af9366d3..5b7ac7e965 100644 --- a/src/Rules/Exceptions/ExceptionTypeResolver.php +++ b/src/Rules/Exceptions/ExceptionTypeResolver.php @@ -4,7 +4,33 @@ use PHPStan\Analyser\Scope; -/** @api */ +/** + * @api + * + * This interface allows you to write custom logic that can dynamically decide + * whether an exception is checked or unchecked type. + * + * Because the interface accepts a Scope, you can ask about the place in the code where + * it's being decided - a file, a namespace or a class name. + * + * There can only be a single ExceptionTypeResolver per project, and you can register it + * in your configuration file like this: + * + * ``` + * services: + * exceptionTypeResolver!: + * class: PHPStan\Rules\Exceptions\ExceptionTypeResolver + * ``` + * + * You can also take advantage of the `PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver` + * by injecting it into the constructor of your ExceptionTypeResolver + * and delegate the logic of the classes and places you don't care about. + * + * DefaultExceptionTypeResolver decides the type of the exception based on configuration + * parameters like `exceptions.uncheckedExceptionClasses` etc. + * + * Learn more: https://phpstan.org/blog/bring-your-exceptions-under-control + */ interface ExceptionTypeResolver { diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php b/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php index aa97cb9fdd..e2ee44c466 100644 --- a/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php +++ b/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php @@ -5,10 +5,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\FunctionReturnStatementsNode; -use PHPStan\Reflection\FunctionReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use function sprintf; /** @@ -29,10 +27,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $statementResult = $node->getStatementResult(); - $functionReflection = $scope->getFunction(); - if (!$functionReflection instanceof FunctionReflection) { - throw new ShouldNotHappenException(); - } + $functionReflection = $node->getFunctionReflection(); $errors = []; foreach ($this->check->check($functionReflection->getThrowType(), $statementResult->getThrowPoints()) as [$className, $throwPointNode]) { @@ -41,8 +36,8 @@ public function processNode(Node $node, Scope $scope): array $functionReflection->getName(), $className, )) - ->line($throwPointNode->getLine()) - ->identifier('exceptions.missingThrowsTag') + ->line($throwPointNode->getStartLine()) + ->identifier('missingType.checkedException') ->build(); } diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php b/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php index 40a398e25e..2aae5d488c 100644 --- a/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php +++ b/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php @@ -5,10 +5,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\MethodReturnStatementsNode; -use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use function sprintf; /** @@ -29,10 +27,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $statementResult = $node->getStatementResult(); - $methodReflection = $scope->getFunction(); - if (!$methodReflection instanceof MethodReflection) { - throw new ShouldNotHappenException(); - } + $methodReflection = $node->getMethodReflection(); $errors = []; foreach ($this->check->check($methodReflection->getThrowType(), $statementResult->getThrowPoints()) as [$className, $throwPointNode]) { @@ -42,8 +37,8 @@ public function processNode(Node $node, Scope $scope): array $methodReflection->getName(), $className, )) - ->line($throwPointNode->getLine()) - ->identifier('exceptions.missingThrowsTag') + ->line($throwPointNode->getStartLine()) + ->identifier('missingType.checkedException') ->build(); } diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php b/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php index 254b3d91ec..508214f275 100644 --- a/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php +++ b/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php @@ -4,11 +4,11 @@ use PhpParser\Node; use PHPStan\Analyser\ThrowPoint; +use PHPStan\TrinaryLogic; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; use Throwable; @@ -43,10 +43,11 @@ public function check(?Type $throwType, array $throwPoints): array continue; } - if ( - $throwPointType instanceof TypeWithClassName - && !$this->exceptionTypeResolver->isCheckedException($throwPointType->getClassName(), $throwPoint->getScope()) - ) { + $isCheckedException = TrinaryLogic::createNo()->lazyOr( + $throwPointType->getObjectClassNames(), + fn (string $objectClassName) => TrinaryLogic::createFromBoolean($this->exceptionTypeResolver->isCheckedException($objectClassName, $throwPoint->getScope())), + ); + if ($isCheckedException->no()) { continue; } diff --git a/src/Rules/Exceptions/NoncapturingCatchRule.php b/src/Rules/Exceptions/NoncapturingCatchRule.php new file mode 100644 index 0000000000..d91bf2fc14 --- /dev/null +++ b/src/Rules/Exceptions/NoncapturingCatchRule.php @@ -0,0 +1,47 @@ + + */ +class NoncapturingCatchRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Catch_::class; + } + + /** + * @param Node\Stmt\Catch_ $node + */ + public function processNode(Node $node, Scope $scope): array + { + if ($this->phpVersion->supportsNoncapturingCatches()) { + return []; + } + + if ($node->var !== null) { + return []; + } + + return [ + RuleErrorBuilder::message('Non-capturing catch is supported only on PHP 8.0 and later.') + ->nonIgnorable() + ->identifier('catch.nonCapturingNotSupported') + ->build(), + ]; + } + +} diff --git a/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php b/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php index 4a3b8f5b09..bcf73954a9 100644 --- a/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php +++ b/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php @@ -29,11 +29,17 @@ public function processNode(Node $node, Scope $scope): array $errors = []; foreach ($node->getTryCatchExitPoints() as $exitPoint) { - $errors[] = RuleErrorBuilder::message(sprintf('This %s is overwritten by a different one in the finally block below.', $this->describeExitPoint($exitPoint->getStatement())))->line($exitPoint->getStatement()->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('This %s is overwritten by a different one in the finally block below.', $this->describeExitPoint($exitPoint->getStatement()))) + ->line($exitPoint->getStatement()->getStartLine()) + ->identifier('finally.exitPoint') + ->build(); } foreach ($node->getFinallyExitPoints() as $exitPoint) { - $errors[] = RuleErrorBuilder::message(sprintf('The overwriting %s is on this line.', $this->describeExitPoint($exitPoint->getStatement())))->line($exitPoint->getStatement()->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('The overwriting %s is on this line.', $this->describeExitPoint($exitPoint->getStatement()))) + ->line($exitPoint->getStatement()->getStartLine()) + ->identifier('finally.exitPoint') + ->build(); } return $errors; diff --git a/src/Rules/Exceptions/ThrowExprTypeRule.php b/src/Rules/Exceptions/ThrowExprTypeRule.php new file mode 100644 index 0000000000..85f648ab97 --- /dev/null +++ b/src/Rules/Exceptions/ThrowExprTypeRule.php @@ -0,0 +1,62 @@ + + */ +class ThrowExprTypeRule implements Rule +{ + + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + ) + { + } + + public function getNodeType(): string + { + return Node\Expr\Throw_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $throwableType = new ObjectType(Throwable::class); + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->expr, + 'Throwing object of an unknown class %s.', + static fn (Type $type): bool => $throwableType->isSuperTypeOf($type)->yes(), + ); + + $foundType = $typeResult->getType(); + if ($foundType instanceof ErrorType) { + return $typeResult->getUnknownClassErrors(); + } + + $isSuperType = $throwableType->isSuperTypeOf($foundType); + if ($isSuperType->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Invalid type %s to throw.', + $foundType->describe(VerbosityLevel::typeOnly()), + ))->identifier('throw.notThrowable')->build(), + ]; + } + +} diff --git a/src/Rules/Exceptions/ThrowExpressionRule.php b/src/Rules/Exceptions/ThrowExpressionRule.php index 3cd3d68b5a..d9b5655ff6 100644 --- a/src/Rules/Exceptions/ThrowExpressionRule.php +++ b/src/Rules/Exceptions/ThrowExpressionRule.php @@ -30,7 +30,9 @@ public function processNode(Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message('Throw expression is supported only on PHP 8.0 and later.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Throw expression is supported only on PHP 8.0 and later.')->nonIgnorable() + ->identifier('throw.notSupported') + ->build(), ]; } diff --git a/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php b/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php index d0eed19fb9..f07d6dc5c0 100644 --- a/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php +++ b/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php @@ -5,14 +5,11 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\FunctionReturnStatementsNode; -use PHPStan\Reflection\FunctionReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; use function sprintf; /** @@ -36,12 +33,9 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $statementResult = $node->getStatementResult(); - $functionReflection = $scope->getFunction(); - if (!$functionReflection instanceof FunctionReflection) { - throw new ShouldNotHappenException(); - } + $functionReflection = $node->getFunctionReflection(); - if (!$functionReflection->getThrowType() instanceof VoidType) { + if ($functionReflection->getThrowType() === null || !$functionReflection->getThrowType()->isVoid()->yes()) { return []; } @@ -52,11 +46,11 @@ public function processNode(Node $node, Scope $scope): array } foreach (TypeUtils::flattenTypes($throwPoint->getType()) as $throwPointType) { - if ( - $throwPointType instanceof TypeWithClassName - && $this->exceptionTypeResolver->isCheckedException($throwPointType->getClassName(), $throwPoint->getScope()) - && $this->missingCheckedExceptionInThrows - ) { + $isCheckedException = TrinaryLogic::createFromBoolean($this->missingCheckedExceptionInThrows)->lazyAnd( + $throwPointType->getObjectClassNames(), + fn (string $objectClassName) => TrinaryLogic::createFromBoolean($this->exceptionTypeResolver->isCheckedException($objectClassName, $throwPoint->getScope())), + ); + if ($isCheckedException->yes()) { continue; } @@ -64,7 +58,10 @@ public function processNode(Node $node, Scope $scope): array 'Function %s() throws exception %s but the PHPDoc contains @throws void.', $functionReflection->getName(), $throwPointType->describe(VerbosityLevel::typeOnly()), - ))->line($throwPoint->getNode()->getLine())->build(); + )) + ->line($throwPoint->getNode()->getStartLine()) + ->identifier('throws.void') + ->build(); } } diff --git a/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php b/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php index 91c97d157b..59c0bd8448 100644 --- a/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php +++ b/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php @@ -5,14 +5,11 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\MethodReturnStatementsNode; -use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; use function sprintf; /** @@ -36,12 +33,9 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $statementResult = $node->getStatementResult(); - $methodReflection = $scope->getFunction(); - if (!$methodReflection instanceof MethodReflection) { - throw new ShouldNotHappenException(); - } + $methodReflection = $node->getMethodReflection(); - if (!$methodReflection->getThrowType() instanceof VoidType) { + if ($methodReflection->getThrowType() === null || !$methodReflection->getThrowType()->isVoid()->yes()) { return []; } @@ -52,11 +46,11 @@ public function processNode(Node $node, Scope $scope): array } foreach (TypeUtils::flattenTypes($throwPoint->getType()) as $throwPointType) { - if ( - $throwPointType instanceof TypeWithClassName - && $this->exceptionTypeResolver->isCheckedException($throwPointType->getClassName(), $throwPoint->getScope()) - && $this->missingCheckedExceptionInThrows - ) { + $isCheckedException = TrinaryLogic::createFromBoolean($this->missingCheckedExceptionInThrows)->lazyAnd( + $throwPointType->getObjectClassNames(), + fn (string $objectClassName) => TrinaryLogic::createFromBoolean($this->exceptionTypeResolver->isCheckedException($objectClassName, $throwPoint->getScope())), + ); + if ($isCheckedException->yes()) { continue; } @@ -65,7 +59,10 @@ public function processNode(Node $node, Scope $scope): array $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), $throwPointType->describe(VerbosityLevel::typeOnly()), - ))->line($throwPoint->getNode()->getLine())->build(); + )) + ->line($throwPoint->getNode()->getStartLine()) + ->identifier('throws.void') + ->build(); } } diff --git a/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php b/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php index fff550cd41..e7f7f9849e 100644 --- a/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php +++ b/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php @@ -5,10 +5,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\FunctionReturnStatementsNode; -use PHPStan\Reflection\FunctionReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use function sprintf; /** @@ -29,10 +27,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $statementResult = $node->getStatementResult(); - $functionReflection = $scope->getFunction(); - if (!$functionReflection instanceof FunctionReflection) { - throw new ShouldNotHappenException(); - } + $functionReflection = $node->getFunctionReflection(); $throwType = $functionReflection->getThrowType(); if ($throwType === null) { @@ -46,12 +41,7 @@ public function processNode(Node $node, Scope $scope): array $functionReflection->getName(), $throwClass, )) - ->identifier('exceptions.tooWideThrowType') - ->metadata([ - 'exceptionName' => $throwClass, - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - ]) + ->identifier('throws.unusedType') ->build(); } diff --git a/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php b/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php index 3a26b6843e..c9a5f231b6 100644 --- a/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php +++ b/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php @@ -5,10 +5,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\MethodReturnStatementsNode; -use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; use function sprintf; @@ -29,21 +27,14 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $statementResult = $node->getStatementResult(); - $methodReflection = $scope->getFunction(); - if (!$methodReflection instanceof MethodReflection) { - throw new ShouldNotHappenException(); - } - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - $docComment = $node->getDocComment(); if ($docComment === null) { return []; } - $classReflection = $scope->getClassReflection(); + $statementResult = $node->getStatementResult(); + $methodReflection = $node->getMethodReflection(); + $classReflection = $node->getClassReflection(); $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( $scope->getFile(), $classReflection->getName(), @@ -66,12 +57,7 @@ public function processNode(Node $node, Scope $scope): array $methodReflection->getName(), $throwClass, )) - ->identifier('exceptions.tooWideThrowType') - ->metadata([ - 'exceptionName' => $throwClass, - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - ]) + ->identifier('throws.unusedType') ->build(); } diff --git a/src/Rules/Exceptions/TooWideThrowTypeCheck.php b/src/Rules/Exceptions/TooWideThrowTypeCheck.php index 59a9ae1d88..2a15d3139f 100644 --- a/src/Rules/Exceptions/TooWideThrowTypeCheck.php +++ b/src/Rules/Exceptions/TooWideThrowTypeCheck.php @@ -8,7 +8,6 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; use function array_map; class TooWideThrowTypeCheck @@ -20,7 +19,7 @@ class TooWideThrowTypeCheck */ public function check(Type $throwType, array $throwPoints): array { - if ($throwType instanceof VoidType) { + if ($throwType->isVoid()->yes()) { return []; } diff --git a/src/Rules/FileRuleError.php b/src/Rules/FileRuleError.php index 7f5cd7fc26..612370f9b2 100644 --- a/src/Rules/FileRuleError.php +++ b/src/Rules/FileRuleError.php @@ -2,9 +2,12 @@ namespace PHPStan\Rules; +/** @api */ interface FileRuleError extends RuleError { public function getFile(): string; + public function getFileDescription(): string; + } diff --git a/src/Rules/FoundTypeResult.php b/src/Rules/FoundTypeResult.php index 919fe2adfd..be17a4c76c 100644 --- a/src/Rules/FoundTypeResult.php +++ b/src/Rules/FoundTypeResult.php @@ -10,7 +10,7 @@ class FoundTypeResult /** * @param string[] $referencedClasses - * @param RuleError[] $unknownClassErrors + * @param list $unknownClassErrors */ public function __construct( private Type $type, @@ -35,7 +35,7 @@ public function getReferencedClasses(): array } /** - * @return RuleError[] + * @return list */ public function getUnknownClassErrors(): array { diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 37be0e0d9a..0e339e9981 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -7,6 +7,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ParameterReflection; +use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ResolvedFunctionVariant; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -23,10 +24,10 @@ use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; use function array_fill; use function array_key_exists; use function count; +use function implode; use function is_string; use function max; use function sprintf; @@ -52,7 +53,8 @@ public function __construct( /** * @param Node\Expr\FuncCall|Node\Expr\MethodCall|Node\Expr\StaticCall|Node\Expr\New_ $funcCall * @param array{0: string, 1: string, 2: string, 3: string, 4: string, 5: string, 6: string, 7: string, 8: string, 9: string, 10: string, 11: string, 12: string, 13?: string} $messages - * @return RuleError[] + * @param 'attribute'|'callable'|'method'|'staticMethod'|'function'|'new' $nodeType + * @return list */ public function check( ParametersAcceptor $parametersAcceptor, @@ -60,6 +62,7 @@ public function check( bool $isBuiltin, $funcCall, array $messages, + string $nodeType = 'function', ): array { $functionParametersMinCount = 0; @@ -86,10 +89,18 @@ public function check( foreach ($args as $i => $arg) { $type = $scope->getType($arg->value); if ($hasNamedArguments && $arg->unpack) { - $errors[] = RuleErrorBuilder::message('Named argument cannot be followed by an unpacked (...) argument.')->line($arg->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message('Named argument cannot be followed by an unpacked (...) argument.') + ->identifier('argument.unpackAfterNamed') + ->line($arg->getStartLine()) + ->nonIgnorable() + ->build(); } if ($hasUnpackedArgument && !$arg->unpack) { - $errors[] = RuleErrorBuilder::message('Unpacked argument (...) cannot be followed by a non-unpacked argument.')->line($arg->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message('Unpacked argument (...) cannot be followed by a non-unpacked argument.') + ->identifier('argument.nonUnpackAfterUnpacked') + ->line($arg->getStartLine()) + ->nonIgnorable() + ->build(); } if ($arg->unpack) { $hasUnpackedArgument = true; @@ -100,11 +111,11 @@ public function check( $argumentName = $arg->name->toString(); } if ($arg->unpack) { - $arrays = TypeUtils::getOldConstantArrays($type); + $arrays = $type->getConstantArrays(); if (count($arrays) > 0) { $minKeys = null; foreach ($arrays as $array) { - $countType = $array->count(); + $countType = $array->getArraySize(); if ($countType instanceof ConstantIntegerType) { $keysCount = $countType->getValue(); } elseif ($countType instanceof IntegerRangeType) { @@ -144,7 +155,7 @@ public function check( TypeCombinator::union(...$types), false, $keyArgumentName, - $arg->getLine(), + $arg->getStartLine(), ]; } } else { @@ -153,7 +164,7 @@ public function check( $type->getIterableValueType(), true, null, - $arg->getLine(), + $arg->getStartLine(), ]; } continue; @@ -164,12 +175,16 @@ public function check( $type, false, $argumentName, - $arg->getLine(), + $arg->getStartLine(), ]; } if ($hasNamedArguments && !$this->phpVersion->supportsNamedArguments() && !(bool) $funcCall->getAttribute('isAttribute', false)) { - $errors[] = RuleErrorBuilder::message('Named arguments are supported only on PHP 8.0 and later.')->line($funcCall->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message('Named arguments are supported only on PHP 8.0 and later.') + ->identifier('argument.namedNotSupported') + ->line($funcCall->getStartLine()) + ->nonIgnorable() + ->build(); } if (!$hasNamedArguments) { @@ -190,33 +205,45 @@ public function check( $invokedParametersCount === 1 ? $messages[0] : $messages[1], $invokedParametersCount, $functionParametersMinCount, - ))->line($funcCall->getLine())->build(); + )) + ->identifier('arguments.count') + ->line($funcCall->getStartLine()) + ->build(); } elseif ($functionParametersMaxCount === -1 && $invokedParametersCount < $functionParametersMinCount) { $errors[] = RuleErrorBuilder::message(sprintf( $invokedParametersCount === 1 ? $messages[2] : $messages[3], $invokedParametersCount, $functionParametersMinCount, - ))->line($funcCall->getLine())->build(); + )) + ->identifier('arguments.count') + ->line($funcCall->getStartLine()) + ->build(); } elseif ($functionParametersMaxCount !== -1) { $errors[] = RuleErrorBuilder::message(sprintf( $invokedParametersCount === 1 ? $messages[4] : $messages[5], $invokedParametersCount, $functionParametersMinCount, $functionParametersMaxCount, - ))->line($funcCall->getLine())->build(); + )) + ->identifier('arguments.count') + ->line($funcCall->getStartLine()) + ->build(); } } } if ( - $scope->getType($funcCall) instanceof VoidType + !$funcCall instanceof Node\Expr\New_ && !$scope->isInFirstLevelStatement() - && !$funcCall instanceof Node\Expr\New_ + && $scope->getKeepVoidType($funcCall)->isVoid()->yes() ) { - $errors[] = RuleErrorBuilder::message($messages[7])->line($funcCall->getLine())->build(); + $errors[] = RuleErrorBuilder::message($messages[7]) + ->identifier(sprintf('%s.void', $nodeType)) + ->line($funcCall->getStartLine()) + ->build(); } - [$addedErrors, $argumentsWithParameters] = $this->processArguments($parametersAcceptor, $funcCall->getLine(), $isBuiltin, $arguments, $hasNamedArguments, $messages[10], $messages[11]); + [$addedErrors, $argumentsWithParameters] = $this->processArguments($parametersAcceptor, $funcCall->getStartLine(), $isBuiltin, $arguments, $hasNamedArguments, $messages[10], $messages[11]); foreach ($addedErrors as $error) { $errors[] = $error; } @@ -242,7 +269,7 @@ public function check( 'Only iterables can be unpacked, %s given in argument #%d.', $iterableTypeResultType->describe(VerbosityLevel::typeOnly()), $i + 1, - ))->line($argumentLine)->build(); + ))->identifier('argument.unpackNonIterable')->line($argumentLine)->build(); } } @@ -250,43 +277,58 @@ public function check( continue; } - $parameterType = TypeUtils::resolveLateResolvableTypes($parameter->getType()); - if ( - $this->checkArgumentTypes - && !$parameter->passedByReference()->createsNewVariable() - && !$this->ruleLevelHelper->accepts($parameterType, $argumentValueType, $scope->isDeclareStrictTypes()) - ) { - $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $argumentValueType); - $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); - $errors[] = RuleErrorBuilder::message(sprintf( - $messages[6], - $argumentName === null ? sprintf( - '#%d %s', - $i + 1, - $parameterDescription, - ) : $parameterDescription, - $parameterType->describe($verbosityLevel), - $argumentValueType->describe($verbosityLevel), - ))->line($argumentLine)->build(); - } + if ($this->checkArgumentTypes) { + $parameterType = TypeUtils::resolveLateResolvableTypes($parameter->getType()); - if ( - $this->checkArgumentTypes - && $this->checkUnresolvableParameterTypes - && $originalParameter !== null - && !$this->unresolvableTypeHelper->containsUnresolvableType($originalParameter->getType()) - && $this->unresolvableTypeHelper->containsUnresolvableType($parameterType) - && isset($messages[13]) - ) { - $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); - $errors[] = RuleErrorBuilder::message(sprintf( - $messages[13], - $argumentName === null ? sprintf( - '#%d %s', - $i + 1, - $parameterDescription, - ) : $parameterDescription, - ))->line($argumentLine)->build(); + if ( + !$parameter->passedByReference()->createsNewVariable() + || (!$isBuiltin && $this->checkUnresolvableParameterTypes) // bleeding edge only + ) { + $accepts = $this->ruleLevelHelper->acceptsWithReason($parameterType, $argumentValueType, $scope->isDeclareStrictTypes()); + + if (!$accepts->result) { + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $argumentValueType); + $errors[] = RuleErrorBuilder::message(sprintf( + $messages[6], + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), + $parameterType->describe($verbosityLevel), + $argumentValueType->describe($verbosityLevel), + )) + ->identifier('argument.type') + ->line($argumentLine) + ->acceptsReasonsTip($accepts->reasons) + ->build(); + } + } + + if ($this->checkUnresolvableParameterTypes + && $originalParameter !== null + && isset($messages[13]) + && !$this->unresolvableTypeHelper->containsUnresolvableType($originalParameter->getType()) + && $this->unresolvableTypeHelper->containsUnresolvableType($parameterType) + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + $messages[13], + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), + ))->identifier('argument.unresolvableType')->line($argumentLine)->build(); + } + + if ( + $parameter instanceof ParameterReflectionWithPhpDocs + && $parameter->getClosureThisType() !== null + && ($argumentValue instanceof Expr\Closure || $argumentValue instanceof Expr\ArrowFunction) + && $argumentValue->static + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + $messages[6], + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), + 'bindable closure', + 'static closure', + )) + ->identifier('argument.staticClosure') + ->line($argumentLine) + ->build(); + } } if ( @@ -297,11 +339,13 @@ public function check( } if ($this->nullsafeCheck->containsNullSafe($argumentValue)) { - $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); $errors[] = RuleErrorBuilder::message(sprintf( $messages[8], - $argumentName === null ? sprintf('#%d %s', $i + 1, $parameterDescription) : $parameterDescription, - ))->line($argumentLine)->build(); + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), + )) + ->identifier('argument.byRef') + ->line($argumentLine) + ->build(); continue; } @@ -324,12 +368,11 @@ public function check( $propertyDescription = sprintf('readonly property %s::$%s', $propertyReflection->getDeclaringClass()->getDisplayName(), $propertyReflection->getName()); } - $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); $errors[] = RuleErrorBuilder::message(sprintf( 'Parameter %s is passed by reference so it does not accept %s.', - $argumentName === null ? sprintf('#%d %s', $i + 1, $parameterDescription) : $parameterDescription, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), $propertyDescription, - ))->line($argumentLine)->build(); + ))->identifier('argument.byRef')->line($argumentLine)->build(); } } @@ -340,11 +383,10 @@ public function check( continue; } - $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); $errors[] = RuleErrorBuilder::message(sprintf( $messages[8], - $argumentName === null ? sprintf('#%d %s', $i + 1, $parameterDescription) : $parameterDescription, - ))->line($argumentLine)->build(); + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), + ))->identifier('argument.byRef')->line($argumentLine)->build(); } if ($this->checkMissingTypehints && $parametersAcceptor instanceof ResolvedFunctionVariant) { @@ -399,7 +441,11 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty continue; } - $errors[] = RuleErrorBuilder::message(sprintf($messages[9], $name))->line($funcCall->getLine())->tip('See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type')->build(); + $errors[] = RuleErrorBuilder::message(sprintf($messages[9], $name)) + ->identifier('argument.templateType') + ->line($funcCall->getStartLine()) + ->tip('See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type') + ->build(); } } @@ -407,7 +453,10 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty !$this->unresolvableTypeHelper->containsUnresolvableType($originalParametersAcceptor->getReturnType()) && $this->unresolvableTypeHelper->containsUnresolvableType($parametersAcceptor->getReturnType()) ) { - $errors[] = RuleErrorBuilder::message($messages[12])->line($funcCall->getLine())->build(); + $errors[] = RuleErrorBuilder::message($messages[12]) + ->identifier(sprintf('%s.unresolvableReturnType', $nodeType)) + ->line($funcCall->getStartLine()) + ->build(); } } @@ -416,7 +465,7 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty /** * @param array $arguments - * @return array{RuleError[], array} + * @return array{list, array} */ private function processArguments( ParametersAcceptor $parametersAcceptor, @@ -481,7 +530,10 @@ private function processArguments( || $parametersCount <= 0 || $isBuiltin ) { - $errors[] = RuleErrorBuilder::message(sprintf($unknownParameterMessage, $argumentName))->line($argumentLine)->build(); + $errors[] = RuleErrorBuilder::message(sprintf($unknownParameterMessage, $argumentName)) + ->identifier('argument.unknown') + ->line($argumentLine) + ->build(); $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null, null]; continue; } @@ -491,7 +543,11 @@ private function processArguments( } if ($namedArgumentAlreadyOccurred && $argumentName === null && !$unpack) { - $errors[] = RuleErrorBuilder::message('Named argument cannot be followed by a positional argument.')->line($argumentLine)->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message('Named argument cannot be followed by a positional argument.') + ->identifier('argument.positionalAfterNamed') + ->line($argumentLine) + ->nonIgnorable() + ->build(); $newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null, null]; continue; } @@ -503,7 +559,10 @@ private function processArguments( && !$parameter->isVariadic() && !array_key_exists($parameter->getName(), $unusedParametersByName) ) { - $errors[] = RuleErrorBuilder::message(sprintf('Argument for parameter $%s has already been passed.', $parameter->getName()))->line($argumentLine)->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Argument for parameter $%s has already been passed.', $parameter->getName())) + ->identifier('argument.duplicate') + ->line($argumentLine) + ->build(); continue; } @@ -516,11 +575,29 @@ private function processArguments( continue; } - $errors[] = RuleErrorBuilder::message(sprintf($missingParameterMessage, sprintf('%s (%s)', $parameter->getName(), $parameter->getType()->describe(VerbosityLevel::typeOnly()))))->line($line)->build(); + $errors[] = RuleErrorBuilder::message(sprintf($missingParameterMessage, sprintf('%s (%s)', $parameter->getName(), $parameter->getType()->describe(VerbosityLevel::typeOnly())))) + ->identifier('argument.missing') + ->line($line) + ->build(); } } return [$errors, $newArguments]; } + private function describeParameter(ParameterReflection $parameter, ?int $position): string + { + $parts = []; + if ($position !== null) { + $parts[] = '#' . $position; + } + + $name = $parameter->getName(); + if ($name !== '') { + $parts[] = ($parameter->isVariadic() ? '...$' : '$') . $name; + } + + return implode(' ', $parts); + } + } diff --git a/src/Rules/FunctionDefinitionCheck.php b/src/Rules/FunctionDefinitionCheck.php index 7ab48062c9..79a8685fc0 100644 --- a/src/Rules/FunctionDefinitionCheck.php +++ b/src/Rules/FunctionDefinitionCheck.php @@ -6,6 +6,10 @@ use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\Variable; use PhpParser\Node\FunctionLike; +use PhpParser\Node\Identifier; +use PhpParser\Node\IntersectionType; +use PhpParser\Node\Name; +use PhpParser\Node\NullableType; use PhpParser\Node\Param; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; @@ -29,11 +33,12 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; +use function array_filter; use function array_keys; use function array_map; use function array_merge; use function count; +use function in_array; use function is_string; use function sprintf; @@ -42,7 +47,7 @@ class FunctionDefinitionCheck public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, private PhpVersion $phpVersion, private bool $checkClassCaseSensitivity, @@ -52,7 +57,7 @@ public function __construct( } /** - * @return RuleError[] + * @return list */ public function checkFunction( Function_ $function, @@ -82,7 +87,7 @@ public function checkFunction( /** * @param Node\Param[] $parameters * @param Node\Identifier|Node\Name|Node\ComplexType|null $returnTypeNode - * @return RuleError[] + * @return list */ public function checkAnonymousFunction( Scope $scope, @@ -106,7 +111,11 @@ public function checkAnonymousFunction( && $param->type instanceof UnionType && !$this->phpVersion->supportsNativeUnionTypes() ) { - $errors[] = RuleErrorBuilder::message($unionTypesMessage)->line($param->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message($unionTypesMessage) + ->line($param->getStartLine()) + ->identifier('parameter.unionTypeNotSupported') + ->nonIgnorable() + ->build(); $unionTypeReported = true; } @@ -114,27 +123,48 @@ public function checkAnonymousFunction( throw new ShouldNotHappenException(); } $type = $scope->getFunctionType($param->type, false, false); - if ($type instanceof VoidType) { - $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $param->var->name, 'void'))->line($param->type->getLine())->nonIgnorable()->build(); + if ($type->isVoid()->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $param->var->name, 'void')) + ->line($param->type->getStartLine()) + ->identifier('parameter.void') + ->nonIgnorable() + ->build(); } if ( $this->phpVersion->supportsPureIntersectionTypes() && $this->unresolvableTypeHelper->containsUnresolvableType($type) ) { - $errors[] = RuleErrorBuilder::message(sprintf($unresolvableParameterTypeMessage, $param->var->name))->line($param->type->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message(sprintf($unresolvableParameterTypeMessage, $param->var->name)) + ->line($param->type->getStartLine()) + ->identifier('parameter.unresolvableNativeType') + ->nonIgnorable() + ->build(); } foreach ($type->getReferencedClasses() as $class) { - if (!$this->reflectionProvider->hasClass($class) || $this->reflectionProvider->getClass($class)->isTrait()) { - $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $param->var->name, $class))->line($param->type->getLine())->build(); - } elseif ($this->checkClassCaseSensitivity) { - $errors = array_merge( - $errors, - $this->classCaseSensitivityCheck->checkClassNames([ - new ClassNameNodePair($class, $param->type), - ]), - ); + if (!$this->reflectionProvider->hasClass($class)) { + $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $param->var->name, $class)) + ->line($param->type->getStartLine()) + ->identifier('class.notFound') + ->build(); + continue; + } + + $classReflection = $this->reflectionProvider->getClass($class); + if ($classReflection->isTrait()) { + $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $param->var->name, $class)) + ->line($param->type->getStartLine()) + ->identifier('parameter.trait') + ->build(); + continue; } + + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames([ + new ClassNameNodePair($class, $param->type), + ], $this->checkClassCaseSensitivity), + ); } } @@ -151,7 +181,11 @@ public function checkAnonymousFunction( && $returnTypeNode instanceof UnionType && !$this->phpVersion->supportsNativeUnionTypes() ) { - $errors[] = RuleErrorBuilder::message($unionTypesMessage)->line($returnTypeNode->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message($unionTypesMessage) + ->line($returnTypeNode->getStartLine()) + ->identifier('reeturn.unionTypeNotSupported') + ->nonIgnorable() + ->build(); } $returnType = $scope->getFunctionType($returnTypeNode, false, false); @@ -159,27 +193,43 @@ public function checkAnonymousFunction( $this->phpVersion->supportsPureIntersectionTypes() && $this->unresolvableTypeHelper->containsUnresolvableType($returnType) ) { - $errors[] = RuleErrorBuilder::message($unresolvableReturnTypeMessage)->line($returnTypeNode->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message($unresolvableReturnTypeMessage) + ->line($returnTypeNode->getStartLine()) + ->identifier('return.unresolvableNativeType') + ->nonIgnorable() + ->build(); } foreach ($returnType->getReferencedClasses() as $returnTypeClass) { - if (!$this->reflectionProvider->hasClass($returnTypeClass) || $this->reflectionProvider->getClass($returnTypeClass)->isTrait()) { - $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $returnTypeClass))->line($returnTypeNode->getLine())->build(); - } elseif ($this->checkClassCaseSensitivity) { - $errors = array_merge( - $errors, - $this->classCaseSensitivityCheck->checkClassNames([ - new ClassNameNodePair($returnTypeClass, $returnTypeNode), - ]), - ); + if (!$this->reflectionProvider->hasClass($returnTypeClass)) { + $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $returnTypeClass)) + ->line($returnTypeNode->getLine()) + ->identifier('class.notFound') + ->build(); + continue; + } + + if ($this->reflectionProvider->getClass($returnTypeClass)->isTrait()) { + $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $returnTypeClass)) + ->line($returnTypeNode->getStartLine()) + ->identifier('return.trait') + ->build(); + continue; } + + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames([ + new ClassNameNodePair($returnTypeClass, $returnTypeNode), + ], $this->checkClassCaseSensitivity), + ); } return $errors; } /** - * @return RuleError[] + * @return list */ public function checkClassMethod( PhpMethodFromParserNodeReflection $methodReflection, @@ -208,7 +258,7 @@ public function checkClassMethod( } /** - * @return RuleError[] + * @return list */ private function checkParametersAcceptor( ParametersAcceptor $parametersAcceptor, @@ -230,13 +280,21 @@ private function checkParametersAcceptor( continue; } - $errors[] = RuleErrorBuilder::message($unionTypesMessage)->line($parameterNode->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message($unionTypesMessage) + ->line($parameterNode->getStartLine()) + ->identifier('parameter.unionTypeNotSupported') + ->nonIgnorable() + ->build(); $unionTypeReported = true; break; } if (!$unionTypeReported && $functionNode->getReturnType() instanceof UnionType) { - $errors[] = RuleErrorBuilder::message($unionTypesMessage)->line($functionNode->getReturnType()->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message($unionTypesMessage) + ->line($functionNode->getReturnType()->getStartLine()) + ->identifier('return.unionTypeNotSupported') + ->nonIgnorable() + ->build(); } } @@ -260,18 +318,37 @@ private function checkParametersAcceptor( if (!$parameterVar instanceof Variable || !is_string($parameterVar->name)) { throw new ShouldNotHappenException(); } - if ($parameter->getNativeType() instanceof VoidType) { - $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $parameterVar->name, 'void'))->line($parameterNodeCallback()->getLine())->nonIgnorable()->build(); + if ($parameter->getNativeType()->isVoid()->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $parameterVar->name, 'void')) + ->line($parameterNodeCallback()->getStartLine()) + ->identifier('parameter.void') + ->nonIgnorable() + ->build(); } if ( $this->phpVersion->supportsPureIntersectionTypes() && $this->unresolvableTypeHelper->containsUnresolvableType($parameter->getNativeType()) ) { - $errors[] = RuleErrorBuilder::message(sprintf($unresolvableParameterTypeMessage, $parameterVar->name))->line($parameterNodeCallback()->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message(sprintf($unresolvableParameterTypeMessage, $parameterVar->name)) + ->line($parameterNodeCallback()->getStartLine()) + ->identifier('parameter.unresolvableNativeType') + ->nonIgnorable() + ->build(); } } foreach ($referencedClasses as $class) { - if ($this->reflectionProvider->hasClass($class) && !$this->reflectionProvider->getClass($class)->isTrait()) { + if (!$this->reflectionProvider->hasClass($class)) { + $errors[] = RuleErrorBuilder::message(sprintf( + $parameterMessage, + $parameter->getName(), + $class, + )) + ->line($parameterNodeCallback()->getStartLine()) + ->identifier('class.notFound') + ->build(); + continue; + } + if (!$this->reflectionProvider->getClass($class)->isTrait()) { continue; } @@ -279,47 +356,72 @@ private function checkParametersAcceptor( $parameterMessage, $parameter->getName(), $class, - ))->line($parameterNodeCallback()->getLine())->build(); + )) + ->line($parameterNodeCallback()->getStartLine()) + ->identifier('parameter.trait') + ->build(); } - if ($this->checkClassCaseSensitivity) { - $errors = array_merge( - $errors, - $this->classCaseSensitivityCheck->checkClassNames(array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $parameterNodeCallback()), $referencedClasses)), - ); - } + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames( + array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $parameterNodeCallback()), $referencedClasses), + $this->checkClassCaseSensitivity, + ), + ); if (!($parameter->getType() instanceof NonexistentParentClassType)) { continue; } - $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $parameter->getName(), $parameter->getType()->describe(VerbosityLevel::typeOnly())))->line($parameterNodeCallback()->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $parameter->getName(), $parameter->getType()->describe(VerbosityLevel::typeOnly()))) + ->line($parameterNodeCallback()->getStartLine()) + ->identifier('parameter.noParent') + ->build(); } if ($this->phpVersion->supportsPureIntersectionTypes() && $functionNode->getReturnType() !== null) { $nativeReturnType = ParserNodeTypeToPHPStanType::resolve($functionNode->getReturnType(), null); if ($this->unresolvableTypeHelper->containsUnresolvableType($nativeReturnType)) { - $errors[] = RuleErrorBuilder::message($unresolvableReturnTypeMessage)->nonIgnorable()->line($returnTypeNode->getLine())->build(); + $errors[] = RuleErrorBuilder::message($unresolvableReturnTypeMessage) + ->nonIgnorable() + ->line($returnTypeNode->getStartLine()) + ->identifier('return.unresolvableNativeType') + ->build(); } } $returnTypeReferencedClasses = $this->getReturnTypeReferencedClasses($parametersAcceptor); foreach ($returnTypeReferencedClasses as $class) { - if ($this->reflectionProvider->hasClass($class) && !$this->reflectionProvider->getClass($class)->isTrait()) { + if (!$this->reflectionProvider->hasClass($class)) { + $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $class)) + ->line($returnTypeNode->getStartLine()) + ->identifier('class.notFound') + ->build(); + continue; + } + if (!$this->reflectionProvider->getClass($class)->isTrait()) { continue; } - $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $class))->line($returnTypeNode->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $class)) + ->line($returnTypeNode->getStartLine()) + ->identifier('return.trait') + ->build(); } - if ($this->checkClassCaseSensitivity) { - $errors = array_merge( - $errors, - $this->classCaseSensitivityCheck->checkClassNames(array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $returnTypeNode), $returnTypeReferencedClasses)), - ); - } + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames( + array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $returnTypeNode), $returnTypeReferencedClasses), + $this->checkClassCaseSensitivity, + ), + ); if ($parametersAcceptor->getReturnType() instanceof NonexistentParentClassType) { - $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $parametersAcceptor->getReturnType()->describe(VerbosityLevel::typeOnly())))->line($returnTypeNode->getLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $parametersAcceptor->getReturnType()->describe(VerbosityLevel::typeOnly()))) + ->line($returnTypeNode->getStartLine()) + ->identifier('return.noParent') + ->build(); } $templateTypeMap = $parametersAcceptor->getTemplateTypeMap(); @@ -349,7 +451,9 @@ private function checkParametersAcceptor( } foreach (array_keys($templateTypes) as $templateTypeName) { - $errors[] = RuleErrorBuilder::message(sprintf($templateTypeMissingInParameterMessage, $templateTypeName))->build(); + $errors[] = RuleErrorBuilder::message(sprintf($templateTypeMissingInParameterMessage, $templateTypeName)) + ->identifier('method.templateTypeNotInParameter') + ->build(); } } @@ -358,13 +462,14 @@ private function checkParametersAcceptor( /** * @param Param[] $parameterNodes - * @return RuleError[] + * @return list */ private function checkRequiredParameterAfterOptional(array $parameterNodes): array { /** @var string|null $optionalParameter */ $optionalParameter = null; $errors = []; + $targetPhpVersion = null; foreach ($parameterNodes as $parameterNode) { if (!$parameterNode->var instanceof Variable) { throw new ShouldNotHappenException(); @@ -374,7 +479,17 @@ private function checkRequiredParameterAfterOptional(array $parameterNodes): arr } $parameterName = $parameterNode->var->name; if ($optionalParameter !== null && $parameterNode->default === null && !$parameterNode->variadic) { - $errors[] = RuleErrorBuilder::message(sprintf('Deprecated in PHP 8.0: Required parameter $%s follows optional parameter $%s.', $parameterName, $optionalParameter))->line($parameterNode->getStartLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Deprecated in PHP %s: Required parameter $%s follows optional parameter $%s.', + $targetPhpVersion ?? '8.0', + $parameterName, + $optionalParameter, + ), + )->line($parameterNode->getStartLine()) + ->identifier('parameter.requiredAfterOptional') + ->build(); + $targetPhpVersion = null; continue; } if ($parameterNode->default === null) { @@ -393,7 +508,35 @@ private function checkRequiredParameterAfterOptional(array $parameterNodes): arr $constantName = $defaultValue->name->toLowerString(); if ($constantName === 'null') { - continue; + if (!$this->phpVersion->deprecatesRequiredParameterAfterOptionalNullableAndDefaultNull()) { + continue; + } + + $parameterNodeType = $parameterNode->type; + + if ($parameterNodeType instanceof NullableType) { + $targetPhpVersion = '8.1'; + } + + if ($this->phpVersion->deprecatesRequiredParameterAfterOptionalUnionOrMixed()) { + $types = []; + + if ($parameterNodeType instanceof UnionType) { + $types = $parameterNodeType->types; + } elseif ($parameterNodeType instanceof Identifier) { + $types = [$parameterNodeType]; + } + + $nullOrMixed = array_filter($types, static fn (Identifier|Name|IntersectionType $type): bool => $type instanceof Identifier && (in_array($type->name, ['null', 'mixed'], true))); + + if (0 < count($nullOrMixed)) { + $targetPhpVersion = '8.3'; + } + } + + if ($targetPhpVersion === null) { + continue; + } } $optionalParameter = $parameterName; diff --git a/src/Rules/FunctionReturnTypeCheck.php b/src/Rules/FunctionReturnTypeCheck.php index ece11b7d74..82bb5d529b 100644 --- a/src/Rules/FunctionReturnTypeCheck.php +++ b/src/Rules/FunctionReturnTypeCheck.php @@ -6,13 +6,11 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; -use PHPStan\Type\GenericTypeVariableResolver; +use PHPStan\Type\ErrorType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; use function sprintf; class FunctionReturnTypeCheck @@ -23,7 +21,7 @@ public function __construct(private RuleLevelHelper $ruleLevelHelper) } /** - * @return RuleError[] + * @return list */ public function checkReturnType( Scope $scope, @@ -42,27 +40,20 @@ public function checkReturnType( if ($returnType instanceof NeverType && $returnType->isExplicit()) { return [ RuleErrorBuilder::message($neverMessage) - ->line($returnNode->getLine()) + ->line($returnNode->getStartLine()) + ->identifier('return.never') ->build(), ]; } if ($isGenerator) { - if (!$returnType instanceof TypeWithClassName) { - return []; - } - - $returnType = GenericTypeVariableResolver::getType( - $returnType, - Generator::class, - 'TReturn', - ); - if ($returnType === null) { + $returnType = $returnType->getTemplateType(Generator::class, 'TReturn'); + if ($returnType instanceof ErrorType) { return []; } } - $isVoidSuperType = (new VoidType())->isSuperTypeOf($returnType); + $isVoidSuperType = $returnType->isVoid(); $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType, null); if ($returnValue === null) { if (!$isVoidSuperType->no()) { @@ -73,7 +64,10 @@ public function checkReturnType( RuleErrorBuilder::message(sprintf( $emptyReturnStatementMessage, $returnType->describe($verbosityLevel), - ))->line($returnNode->getLine())->build(), + )) + ->line($returnNode->getStartLine()) + ->identifier('return.empty') + ->build(), ]; } @@ -89,17 +83,25 @@ public function checkReturnType( RuleErrorBuilder::message(sprintf( $voidMessage, $returnValueType->describe($verbosityLevel), - ))->line($returnNode->getLine())->build(), + )) + ->line($returnNode->getStartLine()) + ->identifier('return.void') + ->build(), ]; } - if (!$this->ruleLevelHelper->accepts($returnType, $returnValueType, $scope->isDeclareStrictTypes())) { + $accepts = $this->ruleLevelHelper->acceptsWithReason($returnType, $returnValueType, $scope->isDeclareStrictTypes()); + if (!$accepts->result) { return [ RuleErrorBuilder::message(sprintf( $typeMismatchMessage, $returnType->describe($verbosityLevel), $returnValueType->describe($verbosityLevel), - ))->line($returnNode->getLine())->build(), + )) + ->line($returnNode->getStartLine()) + ->identifier('return.type') + ->acceptsReasonsTip($accepts->reasons) + ->build(), ]; } diff --git a/src/Rules/Functions/ArrayFilterRule.php b/src/Rules/Functions/ArrayFilterRule.php index 2ff8dd0e90..177d1d218d 100644 --- a/src/Rules/Functions/ArrayFilterRule.php +++ b/src/Rules/Functions/ArrayFilterRule.php @@ -4,7 +4,9 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; +use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\Scope; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -12,7 +14,6 @@ use PHPStan\Type\VerbosityLevel; use function count; use function sprintf; -use function strtolower; /** * @implements Rule @@ -20,7 +21,10 @@ class ArrayFilterRule implements Rule { - public function __construct(private ReflectionProvider $reflectionProvider) + public function __construct( + private ReflectionProvider $reflectionProvider, + private bool $treatPhpDocTypesAsCertain, + ) { } @@ -35,26 +39,52 @@ public function processNode(Node $node, Scope $scope): array return []; } - $functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope); + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } - if ($functionName === null || strtolower($functionName) !== 'array_filter') { + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + if ($functionReflection->getName() !== 'array_filter') { return []; } - $args = $node->getArgs(); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $node->getArgs(), + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + if ($normalizedFuncCall === null) { + return []; + } + + $args = $normalizedFuncCall->getArgs(); if (count($args) !== 1) { return []; } - $arrayType = $scope->getType($args[0]->value); + if ($this->treatPhpDocTypesAsCertain) { + $arrayType = $scope->getType($args[0]->value); + } else { + $arrayType = $scope->getNativeType($args[0]->value); + } if ($arrayType->isIterableAtLeastOnce()->no()) { $message = 'Parameter #1 $array (%s) to function array_filter is empty, call has no effect.'; + $errorBuilder = RuleErrorBuilder::message(sprintf( + $message, + $arrayType->describe(VerbosityLevel::value()), + ))->identifier('arrayFilter.empty'); + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + if (!$nativeArrayType->isIterableAtLeastOnce()->no()) { + $errorBuilder->treatPhpDocTypesAsCertainTip(); + } + } return [ - RuleErrorBuilder::message(sprintf( - $message, - $arrayType->describe(VerbosityLevel::value()), - ))->build(), + $errorBuilder->build(), ]; } @@ -63,21 +93,41 @@ public function processNode(Node $node, Scope $scope): array if ($isSuperType->no()) { $message = 'Parameter #1 $array (%s) to function array_filter does not contain falsy values, the array will always stay the same.'; + $errorBuilder = RuleErrorBuilder::message(sprintf( + $message, + $arrayType->describe(VerbosityLevel::value()), + ))->identifier('arrayFilter.same'); + + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + $isNativeSuperType = $falsyType->isSuperTypeOf($nativeArrayType->getIterableValueType()); + if (!$isNativeSuperType->no()) { + $errorBuilder->treatPhpDocTypesAsCertainTip(); + } + } + return [ - RuleErrorBuilder::message(sprintf( - $message, - $arrayType->describe(VerbosityLevel::value()), - ))->build(), + $errorBuilder->build(), ]; } if ($isSuperType->yes()) { $message = 'Parameter #1 $array (%s) to function array_filter contains falsy values only, the result will always be an empty array.'; + $errorBuilder = RuleErrorBuilder::message(sprintf( + $message, + $arrayType->describe(VerbosityLevel::value()), + ))->identifier('arrayFilter.alwaysEmpty'); + + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + $isNativeSuperType = $falsyType->isSuperTypeOf($nativeArrayType->getIterableValueType()); + if (!$isNativeSuperType->yes()) { + $errorBuilder->treatPhpDocTypesAsCertainTip(); + } + } + return [ - RuleErrorBuilder::message(sprintf( - $message, - $arrayType->describe(VerbosityLevel::value()), - ))->build(), + $errorBuilder->build(), ]; } diff --git a/src/Rules/Functions/ArrayValuesRule.php b/src/Rules/Functions/ArrayValuesRule.php new file mode 100644 index 0000000000..bb62442ec8 --- /dev/null +++ b/src/Rules/Functions/ArrayValuesRule.php @@ -0,0 +1,118 @@ + + */ +class ArrayValuesRule implements Rule +{ + + public function __construct( + private readonly ReflectionProvider $reflectionProvider, + private readonly bool $treatPhpDocTypesAsCertain, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (AccessoryArrayListType::isListTypeEnabled() === false) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + if ($functionReflection->getName() !== 'array_values') { + return []; + } + + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $node->getArgs(), + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + + if ($normalizedFuncCall === null) { + return []; + } + + $args = $normalizedFuncCall->getArgs(); + if (count($args) === 0) { + return []; + } + + if ($this->treatPhpDocTypesAsCertain) { + $arrayType = $scope->getType($args[0]->value); + } else { + $arrayType = $scope->getNativeType($args[0]->value); + } + + if ($arrayType->isIterableAtLeastOnce()->no()) { + $message = 'Parameter #1 $array (%s) to function array_values is empty, call has no effect.'; + $errorBuilder = RuleErrorBuilder::message(sprintf( + $message, + $arrayType->describe(VerbosityLevel::value()), + ))->identifier('arrayValues.empty'); + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + if (!$nativeArrayType->isIterableAtLeastOnce()->no()) { + $errorBuilder->treatPhpDocTypesAsCertainTip(); + } + } + + return [ + $errorBuilder->build(), + ]; + } + + if ($arrayType->isList()->yes()) { + $message = 'Parameter #1 $array (%s) of array_values is already a list, call has no effect.'; + $errorBuilder = RuleErrorBuilder::message(sprintf( + $message, + $arrayType->describe(VerbosityLevel::value()), + ))->identifier('arrayValues.list'); + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + if (!$nativeArrayType->isList()->yes()) { + $errorBuilder->treatPhpDocTypesAsCertainTip(); + } + } + + return [ + $errorBuilder->build(), + ]; + } + + return []; + } + +} diff --git a/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php b/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php index 3606f67a71..371f2fa61a 100644 --- a/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php +++ b/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php @@ -34,7 +34,10 @@ public function processNode(Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message('Nullsafe cannot be returned by reference.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Nullsafe cannot be returned by reference.') + ->nonIgnorable() + ->identifier('nullsafe.byRef') + ->build(), ]; } diff --git a/src/Rules/Functions/ArrowFunctionReturnTypeRule.php b/src/Rules/Functions/ArrowFunctionReturnTypeRule.php index 86bd340d54..aa0a9436de 100644 --- a/src/Rules/Functions/ArrowFunctionReturnTypeRule.php +++ b/src/Rules/Functions/ArrowFunctionReturnTypeRule.php @@ -9,9 +9,9 @@ use PHPStan\Rules\FunctionReturnTypeCheck; use PHPStan\Rules\Rule; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -use PHPStan\Type\VoidType; /** * @implements Rule @@ -39,11 +39,21 @@ public function processNode(Node $node, Scope $scope): array $generatorType = new ObjectType(Generator::class); $originalNode = $node->getOriginalNode(); - $isVoidSuperType = (new VoidType())->isSuperTypeOf($returnType); + $isVoidSuperType = $returnType->isVoid(); if ($originalNode->returnType === null && $isVoidSuperType->yes()) { return []; } + $exprType = $scope->getType($originalNode->expr); + if ( + $returnType instanceof NeverType + && $returnType->isExplicit() + && $exprType instanceof NeverType + && $exprType->isExplicit() + ) { + return []; + } + return $this->returnTypeCheck->checkReturnType( $scope, $returnType, diff --git a/src/Rules/Functions/CallCallablesRule.php b/src/Rules/Functions/CallCallablesRule.php index 05e1865938..e5c0d7dc8c 100644 --- a/src/Rules/Functions/CallCallablesRule.php +++ b/src/Rules/Functions/CallCallablesRule.php @@ -65,14 +65,14 @@ public function processNode( return [ RuleErrorBuilder::message( sprintf('Trying to invoke %s but it\'s not a callable.', $type->describe(VerbosityLevel::value())), - )->build(), + )->identifier('callable.nonCallable')->build(), ]; } if ($this->reportMaybes && $isCallable->maybe()) { return [ RuleErrorBuilder::message( sprintf('Trying to invoke %s but it might not be a callable.', $type->describe(VerbosityLevel::value())), - )->build(), + )->identifier('callable.nonCallable')->build(), ]; } @@ -89,13 +89,14 @@ public function processNode( $method->isPrivate() ? 'private' : 'protected', $method->getName(), $method->getDeclaringClass()->getDisplayName(), - ))->build(); + ))->identifier('callable.inaccessibleMethod')->build(); } $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $node->getArgs(), $parametersAcceptors, + null, ); if ($type instanceof ClosureType) { @@ -127,6 +128,7 @@ public function processNode( 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', 'Parameter %s of ' . $callableDescription . ' contains unresolvable type.', ], + 'callable', ), ); } diff --git a/src/Rules/Functions/CallToFunctionParametersRule.php b/src/Rules/Functions/CallToFunctionParametersRule.php index 342b79ffc1..01b80d7f68 100644 --- a/src/Rules/Functions/CallToFunctionParametersRule.php +++ b/src/Rules/Functions/CallToFunctionParametersRule.php @@ -44,6 +44,7 @@ public function processNode(Node $node, Scope $scope): array $scope, $node->getArgs(), $function->getVariants(), + $function->getNamedArgumentsVariants(), ), $scope, $function->isBuiltin(), @@ -64,6 +65,7 @@ public function processNode(Node $node, Scope $scope): array 'Return type of call to function ' . $functionName . ' contains unresolvable type.', 'Parameter %s of function ' . $functionName . ' contains unresolvable type.', ], + 'function', ); } diff --git a/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php index b675563d4c..685fa41de0 100644 --- a/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php +++ b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php @@ -3,12 +3,13 @@ namespace PHPStan\Rules\Functions; use PhpParser\Node; +use PhpParser\Node\Arg; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\NeverType; -use PHPStan\Type\VoidType; +use PHPStan\Type\Type; use function in_array; use function sprintf; @@ -18,6 +19,21 @@ class CallToFunctionStatementWithoutSideEffectsRule implements Rule { + private const SIDE_EFFECT_FLIP_PARAMETERS = [ + // functionName => [name, pos, testName] + 'print_r' => ['return', 1, 'isTruthy'], + 'var_export' => ['return', 1, 'isTruthy'], + 'highlight_string' => ['return', 1, 'isTruthy'], + + ]; + + public const PHPSTAN_TESTING_FUNCTIONS = [ + 'PHPStan\\dumpType', + 'PHPStan\\Testing\\assertType', + 'PHPStan\\Testing\\assertNativeType', + 'PHPStan\\Testing\\assertVariableCertainty', + ]; + public function __construct(private ReflectionProvider $reflectionProvider) { } @@ -43,10 +59,62 @@ public function processNode(Node $node, Scope $scope): array } $function = $this->reflectionProvider->getFunction($funcCall->name, $scope); - if ($function->hasSideEffects()->no() || $node->expr->isFirstClassCallable()) { + $functionName = $function->getName(); + $functionHasSideEffects = !$function->hasSideEffects()->no(); + + if (in_array($functionName, self::PHPSTAN_TESTING_FUNCTIONS, true)) { + return []; + } + + if (isset(self::SIDE_EFFECT_FLIP_PARAMETERS[$functionName])) { + [ + $flipParameterName, + $flipParameterPosition, + $testName, + ] = self::SIDE_EFFECT_FLIP_PARAMETERS[$functionName]; + + $sideEffectFlipped = false; + $hasNamedParameter = false; + $checker = [ + 'isNotNull' => static fn (Type $type) => $type->isNull()->no(), + 'isTruthy' => static fn (Type $type) => $type->toBoolean()->isTrue()->yes(), + ][$testName]; + + foreach ($funcCall->getRawArgs() as $i => $arg) { + if (!$arg instanceof Arg) { + return []; + } + + $isFlipParameter = false; + + if ($arg->name !== null) { + $hasNamedParameter = true; + if ($arg->name->name === $flipParameterName) { + $isFlipParameter = true; + } + } + + if (!$hasNamedParameter && $i === $flipParameterPosition) { + $isFlipParameter = true; + } + + if ($isFlipParameter) { + $sideEffectFlipped = $checker($scope->getType($arg->value)); + break; + } + } + + if (!$sideEffectFlipped) { + return []; + } + + $functionHasSideEffects = false; + } + + if (!$functionHasSideEffects || $node->expr->isFirstClassCallable()) { if (!$node->expr->isFirstClassCallable()) { $throwsType = $function->getThrowType(); - if ($throwsType !== null && !$throwsType instanceof VoidType) { + if ($throwsType !== null && !$throwsType->isVoid()->yes()) { return []; } } @@ -56,20 +124,11 @@ public function processNode(Node $node, Scope $scope): array return []; } - if (in_array($function->getName(), [ - 'PHPStan\\dumpType', - 'PHPStan\\Testing\\assertType', - 'PHPStan\\Testing\\assertNativeType', - 'PHPStan\\Testing\\assertVariableCertainty', - ], true)) { - return []; - } - return [ RuleErrorBuilder::message(sprintf( 'Call to function %s() on a separate line has no effect.', $function->getName(), - ))->build(), + ))->identifier('function.resultUnused')->build(), ]; } diff --git a/src/Rules/Functions/CallToNonExistentFunctionRule.php b/src/Rules/Functions/CallToNonExistentFunctionRule.php index 7fc4443838..72fca5073c 100644 --- a/src/Rules/Functions/CallToNonExistentFunctionRule.php +++ b/src/Rules/Functions/CallToNonExistentFunctionRule.php @@ -41,7 +41,7 @@ public function processNode(Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message(sprintf('Function %s not found.', (string) $node->name))->discoveringSymbolsTip()->build(), + RuleErrorBuilder::message(sprintf('Function %s not found.', (string) $node->name))->identifier('function.notFound')->discoveringSymbolsTip()->build(), ]; } @@ -60,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array 'Call to function %s() with incorrect case: %s', $function->getName(), $name, - ))->build(), + ))->identifier('function.nameCase')->build(), ]; } } diff --git a/src/Rules/Functions/CallUserFuncRule.php b/src/Rules/Functions/CallUserFuncRule.php new file mode 100644 index 0000000000..040a5aecce --- /dev/null +++ b/src/Rules/Functions/CallUserFuncRule.php @@ -0,0 +1,81 @@ + + */ +class CallUserFuncRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private FunctionCallParametersCheck $check, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Name) { + return []; + } + + if (count($node->getArgs()) === 0) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + if ($functionReflection->getName() !== 'call_user_func') { + return []; + } + + $result = ArgumentsNormalizer::reorderCallUserFuncArguments( + $node, + $scope, + ); + if ($result === null) { + return []; + } + [$parametersAcceptor, $funcCall] = $result; + + $callableDescription = 'callable passed to call_user_func()'; + + return $this->check->check($parametersAcceptor, $scope, false, $funcCall, [ + ucfirst($callableDescription) . ' invoked with %d parameter, %d required.', + ucfirst($callableDescription) . ' invoked with %d parameters, %d required.', + ucfirst($callableDescription) . ' invoked with %d parameter, at least %d required.', + ucfirst($callableDescription) . ' invoked with %d parameters, at least %d required.', + ucfirst($callableDescription) . ' invoked with %d parameter, %d-%d required.', + ucfirst($callableDescription) . ' invoked with %d parameters, %d-%d required.', + 'Parameter %s of ' . $callableDescription . ' expects %s, %s given.', + 'Result of ' . $callableDescription . ' (void) is used.', + 'Parameter %s of ' . $callableDescription . ' is passed by reference, so it expects variables only.', + 'Unable to resolve the template type %s in call to ' . $callableDescription, + 'Missing parameter $%s in call to ' . $callableDescription . '.', + 'Unknown parameter $%s in call to ' . $callableDescription . '.', + 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', + 'Parameter %s of ' . $callableDescription . ' contains unresolvable type.', + ]); + } + +} diff --git a/src/Rules/Functions/ClosureReturnTypeRule.php b/src/Rules/Functions/ClosureReturnTypeRule.php index 22603a10c7..98f72deb73 100644 --- a/src/Rules/Functions/ClosureReturnTypeRule.php +++ b/src/Rules/Functions/ClosureReturnTypeRule.php @@ -9,7 +9,6 @@ use PHPStan\Rules\Rule; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use function count; /** * @implements Rule @@ -53,7 +52,7 @@ public function processNode(Node $node, Scope $scope): array 'Anonymous function with return type void returns %s but should not return anything.', 'Anonymous function should return %s but returns %s.', 'Anonymous function should never return but return statement found.', - count($node->getYieldStatements()) > 0, + $node->isGenerator(), ); foreach ($returnMessages as $returnMessage) { diff --git a/src/Rules/Functions/DefineParametersRule.php b/src/Rules/Functions/DefineParametersRule.php index ec5a2590b3..5aac3ee5c2 100644 --- a/src/Rules/Functions/DefineParametersRule.php +++ b/src/Rules/Functions/DefineParametersRule.php @@ -47,7 +47,10 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( 'Argument #3 ($case_insensitive) is ignored since declaration of case-insensitive constants is no longer supported.', - )->line($node->getLine())->build(), + ) + ->line($node->getStartLine()) + ->identifier('argument.unused') + ->build(), ]; } diff --git a/src/Rules/Functions/DuplicateFunctionDeclarationRule.php b/src/Rules/Functions/DuplicateFunctionDeclarationRule.php new file mode 100644 index 0000000000..06d92643ae --- /dev/null +++ b/src/Rules/Functions/DuplicateFunctionDeclarationRule.php @@ -0,0 +1,59 @@ + + */ +class DuplicateFunctionDeclarationRule implements Rule +{ + + public function __construct(private Reflector $reflector, private RelativePathHelper $relativePathHelper) + { + } + + public function getNodeType(): string + { + return InFunctionNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $thisFunction = $node->getFunctionReflection(); + $allFunctions = $this->reflector->reflectAllFunctions(); + $filteredFunctions = []; + foreach ($allFunctions as $reflectionFunction) { + if ($reflectionFunction->getName() !== $thisFunction->getName()) { + continue; + } + + $filteredFunctions[] = $reflectionFunction; + } + + if (count($filteredFunctions) < 2) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + "Function %s declared multiple times:\n%s", + $thisFunction->getName(), + implode("\n", array_map(fn (ReflectionFunction $function) => sprintf('- %s:%d', $this->relativePathHelper->getRelativePath($function->getFileName() ?? 'unknown'), $function->getStartLine()), $filteredFunctions)), + ))->identifier('function.duplicate')->build(), + ]; + } + +} diff --git a/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php b/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php index ecaf404573..a2107c73a0 100644 --- a/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php @@ -4,8 +4,13 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionDefinitionCheck; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\NonAcceptingNeverType; +use PHPStan\Type\ParserNodeTypeToPHPStanType; +use function array_merge; /** * @implements Rule @@ -13,7 +18,7 @@ class ExistingClassesInArrowFunctionTypehintsRule implements Rule { - public function __construct(private FunctionDefinitionCheck $check) + public function __construct(private FunctionDefinitionCheck $check, private PhpVersion $phpVersion) { } @@ -24,7 +29,18 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - return $this->check->checkAnonymousFunction( + $messages = []; + if ($node->returnType !== null && !$this->phpVersion->supportsNeverReturnTypeInArrowFunction()) { + $returnType = ParserNodeTypeToPHPStanType::resolve($node->returnType, $scope->isInClass() ? $scope->getClassReflection() : null); + if ($returnType instanceof NonAcceptingNeverType) { + $messages[] = RuleErrorBuilder::message('Never return type in arrow function is supported only on PHP 8.2 and later.') + ->identifier('return.neverTypeNotSupported') + ->nonIgnorable() + ->build(); + } + } + + return array_merge($messages, $this->check->checkAnonymousFunction( $scope, $node->getParams(), $node->getReturnType(), @@ -33,7 +49,7 @@ public function processNode(Node $node, Scope $scope): array 'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.', 'Parameter $%s of anonymous function has unresolvable native type.', 'Anonymous function has unresolvable native return type.', - ); + )); } } diff --git a/src/Rules/Functions/ExistingClassesInTypehintsRule.php b/src/Rules/Functions/ExistingClassesInTypehintsRule.php index 4314e6f69d..6a9586a820 100644 --- a/src/Rules/Functions/ExistingClassesInTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInTypehintsRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InFunctionNode; -use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Rules\FunctionDefinitionCheck; use PHPStan\Rules\Rule; use function sprintf; @@ -28,15 +27,11 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->getFunction() instanceof PhpFunctionFromParserNodeReflection) { - return []; - } - - $functionName = SprintfHelper::escapeFormatString($scope->getFunction()->getName()); + $functionName = SprintfHelper::escapeFormatString($node->getFunctionReflection()->getName()); return $this->check->checkFunction( $node->getOriginalNode(), - $scope->getFunction(), + $node->getFunctionReflection(), sprintf( 'Parameter $%%s of function %s() has invalid type %%s.', $functionName, diff --git a/src/Rules/Functions/FunctionCallableRule.php b/src/Rules/Functions/FunctionCallableRule.php index 5905fc6dec..fdecd2073f 100644 --- a/src/Rules/Functions/FunctionCallableRule.php +++ b/src/Rules/Functions/FunctionCallableRule.php @@ -38,6 +38,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message('First-class callables are supported only on PHP 8.1 and later.') ->nonIgnorable() + ->identifier('callable.notSupported') ->build(), ]; } @@ -60,7 +61,7 @@ public function processNode(Node $node, Scope $scope): array 'Call to function %s() with incorrect case: %s', $function->getName(), $functionNameName, - ))->build(), + ))->identifier('function.nameCase')->build(), ]; } } @@ -74,6 +75,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf('Function %s not found.', $functionNameName)) + ->identifier('function.notFound') ->build(), ]; } @@ -94,14 +96,14 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( sprintf('Creating callable from %s but it\'s not a callable.', $type->describe(VerbosityLevel::value())), - )->build(), + )->identifier('callable.nonCallable')->build(), ]; } if ($this->reportMaybes && $isCallable->maybe()) { return [ RuleErrorBuilder::message( sprintf('Creating callable from %s but it might not be a callable.', $type->describe(VerbosityLevel::value())), - )->build(), + )->identifier('callable.nonCallable')->build(), ]; } diff --git a/src/Rules/Functions/ImplodeFunctionRule.php b/src/Rules/Functions/ImplodeFunctionRule.php index 3d75e6d789..a9615bedeb 100644 --- a/src/Rules/Functions/ImplodeFunctionRule.php +++ b/src/Rules/Functions/ImplodeFunctionRule.php @@ -71,7 +71,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( sprintf('Parameter #%d $array of function %s expects array, %s given.', $paramNo, $functionName, $typeResult->getType()->describe(VerbosityLevel::typeOnly())), - )->build(), + )->identifier('argument.type')->build(), ]; } diff --git a/src/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRule.php b/src/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRule.php new file mode 100644 index 0000000000..585b00137a --- /dev/null +++ b/src/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRule.php @@ -0,0 +1,70 @@ + + */ +class IncompatibleArrowFunctionDefaultParameterTypeRule implements Rule +{ + + public function getNodeType(): string + { + return InArrowFunctionNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $parameters = $node->getClosureType()->getParameters(); + + $errors = []; + foreach ($node->getOriginalNode()->getParams() as $paramI => $param) { + if ($param->default === null) { + continue; + } + if ( + $param->var instanceof Node\Expr\Error + || !is_string($param->var->name) + ) { + throw new ShouldNotHappenException(); + } + + $defaultValueType = $scope->getType($param->default); + $parameterType = $parameters[$paramI]->getType(); + $parameterType = TemplateTypeHelper::resolveToBounds($parameterType); + + $accepts = $parameterType->acceptsWithReason($defaultValueType, true); + if ($accepts->yes()) { + continue; + } + + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $defaultValueType); + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Default value of the parameter #%d $%s (%s) of anonymous function is incompatible with type %s.', + $paramI + 1, + $param->var->name, + $defaultValueType->describe($verbosityLevel), + $parameterType->describe($verbosityLevel), + )) + ->line($param->getStartLine()) + ->identifier('parameter.defaultValue') + ->acceptsReasonsTip($accepts->reasons) + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Functions/IncompatibleClosureDefaultParameterTypeRule.php b/src/Rules/Functions/IncompatibleClosureDefaultParameterTypeRule.php new file mode 100644 index 0000000000..3cd1c51390 --- /dev/null +++ b/src/Rules/Functions/IncompatibleClosureDefaultParameterTypeRule.php @@ -0,0 +1,70 @@ + + */ +class IncompatibleClosureDefaultParameterTypeRule implements Rule +{ + + public function getNodeType(): string + { + return InClosureNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $parameters = $node->getClosureType()->getParameters(); + + $errors = []; + foreach ($node->getOriginalNode()->getParams() as $paramI => $param) { + if ($param->default === null) { + continue; + } + if ( + $param->var instanceof Node\Expr\Error + || !is_string($param->var->name) + ) { + throw new ShouldNotHappenException(); + } + + $defaultValueType = $scope->getType($param->default); + $parameterType = $parameters[$paramI]->getType(); + $parameterType = TemplateTypeHelper::resolveToBounds($parameterType); + + $accepts = $parameterType->acceptsWithReason($defaultValueType, true); + if ($accepts->yes()) { + continue; + } + + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $defaultValueType); + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Default value of the parameter #%d $%s (%s) of anonymous function is incompatible with type %s.', + $paramI + 1, + $param->var->name, + $defaultValueType->describe($verbosityLevel), + $parameterType->describe($verbosityLevel), + )) + ->line($param->getStartLine()) + ->identifier('parameter.defaultValue') + ->acceptsReasonsTip($accepts->reasons) + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php b/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php index 4d31ea77a4..a142269a68 100644 --- a/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php +++ b/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InFunctionNode; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; @@ -28,10 +27,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $function = $scope->getFunction(); - if (!$function instanceof PhpFunctionFromParserNodeReflection) { - return []; - } + $function = $node->getFunctionReflection(); $parameters = ParametersAcceptorSelector::selectSingle($function->getVariants()); $errors = []; @@ -50,7 +46,8 @@ public function processNode(Node $node, Scope $scope): array $parameterType = $parameters->getParameters()[$paramI]->getType(); $parameterType = TemplateTypeHelper::resolveToBounds($parameterType); - if ($parameterType->accepts($defaultValueType, true)->yes()) { + $accepts = $parameterType->acceptsWithReason($defaultValueType, true); + if ($accepts->yes()) { continue; } @@ -63,7 +60,11 @@ public function processNode(Node $node, Scope $scope): array $defaultValueType->describe($verbosityLevel), $function->getName(), $parameterType->describe($verbosityLevel), - ))->line($param->getLine())->build(); + )) + ->line($param->getStartLine()) + ->identifier('parameter.defaultValue') + ->acceptsReasonsTip($accepts->reasons) + ->build(); } return $errors; diff --git a/src/Rules/Functions/InnerFunctionRule.php b/src/Rules/Functions/InnerFunctionRule.php index 9c58ea233a..24118f5fbb 100644 --- a/src/Rules/Functions/InnerFunctionRule.php +++ b/src/Rules/Functions/InnerFunctionRule.php @@ -28,7 +28,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( 'Inner named functions are not supported by PHPStan. Consider refactoring to an anonymous function, class method, or a top-level-defined function. See issue #165 (https://github.com/phpstan/phpstan/issues/165) for more details.', - )->build(), + )->identifier('function.inner')->build(), ]; } diff --git a/src/Rules/Functions/InvalidLexicalVariablesInClosureUseRule.php b/src/Rules/Functions/InvalidLexicalVariablesInClosureUseRule.php new file mode 100644 index 0000000000..257441183c --- /dev/null +++ b/src/Rules/Functions/InvalidLexicalVariablesInClosureUseRule.php @@ -0,0 +1,86 @@ + + */ +class InvalidLexicalVariablesInClosureUseRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Expr\Closure::class; + } + + /** + * @param Node\Expr\Closure $node + */ + public function processNode(Node $node, Scope $scope): array + { + $errors = []; + $params = array_filter(array_map( + static function (Node\Param $param) { + if (!$param->var instanceof Node\Expr\Variable) { + return false; + } + + if (!is_string($param->var->name)) { + return false; + } + + return $param->var->name; + }, + $node->getParams(), + )); + + foreach ($node->uses as $use) { + if (!is_string($use->var->name)) { + continue; + } + + $var = $use->var->name; + + if ($var === 'this') { + $errors[] = RuleErrorBuilder::message('Cannot use $this as lexical variable.') + ->line($use->getStartLine()) + ->identifier('closure.useThis') + ->nonIgnorable() + ->build(); + continue; + } + + if (in_array($var, Scope::SUPERGLOBAL_VARIABLES, true)) { + $errors[] = RuleErrorBuilder::message(sprintf('Cannot use superglobal variable $%s as lexical variable.', $var)) + ->line($use->getStartLine()) + ->identifier('closure.useSuperGlobal') + ->nonIgnorable() + ->build(); + continue; + } + + if (!in_array($var, $params, true)) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf('Cannot use lexical variable $%s since a parameter with the same name already exists.', $var)) + ->line($use->getStartLine()) + ->identifier('closure.useDuplicate') + ->nonIgnorable() + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php index 8a96133984..e58828c978 100644 --- a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php @@ -6,14 +6,13 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InFunctionNode; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function implode; use function sprintf; @@ -26,6 +25,7 @@ final class MissingFunctionParameterTypehintRule implements Rule public function __construct( private MissingTypehintCheck $missingTypehintCheck, + private bool $paramOut, ) { } @@ -37,15 +37,28 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $functionReflection = $scope->getFunction(); - if (!$functionReflection instanceof PhpFunctionFromParserNodeReflection) { - return []; - } - + $functionReflection = $node->getFunctionReflection(); $messages = []; foreach (ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getParameters() as $parameterReflection) { - foreach ($this->checkFunctionParameter($functionReflection, $parameterReflection) as $parameterMessage) { + foreach ($this->checkFunctionParameter($functionReflection, sprintf('parameter $%s', $parameterReflection->getName()), $parameterReflection->getType()) as $parameterMessage) { + $messages[] = $parameterMessage; + } + + if ($parameterReflection->getClosureThisType() !== null) { + foreach ($this->checkFunctionParameter($functionReflection, sprintf('@param-closure-this PHPDoc tag for parameter $%s', $parameterReflection->getName()), $parameterReflection->getClosureThisType()) as $parameterMessage) { + $messages[] = $parameterMessage; + } + } + + if (!$this->paramOut) { + continue; + } + if ($parameterReflection->getOutType() === null) { + continue; + } + + foreach ($this->checkFunctionParameter($functionReflection, sprintf('@param-out PHPDoc tag for parameter $%s', $parameterReflection->getName()), $parameterReflection->getOutType()) as $parameterMessage) { $messages[] = $parameterMessage; } } @@ -54,19 +67,17 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ - private function checkFunctionParameter(FunctionReflection $functionReflection, ParameterReflection $parameterReflection): array + private function checkFunctionParameter(FunctionReflection $functionReflection, string $parameterMessage, Type $parameterType): array { - $parameterType = $parameterReflection->getType(); - if ($parameterType instanceof MixedType && !$parameterType->isExplicitMixed()) { return [ RuleErrorBuilder::message(sprintf( - 'Function %s() has parameter $%s with no type specified.', + 'Function %s() has %s with no type specified.', $functionReflection->getName(), - $parameterReflection->getName(), - ))->build(), + $parameterMessage, + ))->identifier('missingType.parameter')->build(), ]; } @@ -74,30 +85,36 @@ private function checkFunctionParameter(FunctionReflection $functionReflection, foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($parameterType) as $iterableType) { $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); $messages[] = RuleErrorBuilder::message(sprintf( - 'Function %s() has parameter $%s with no value type specified in iterable type %s.', + 'Function %s() has %s with no value type specified in iterable type %s.', $functionReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $iterableTypeDescription, - ))->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); } foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($parameterType) as [$name, $genericTypeNames]) { $messages[] = RuleErrorBuilder::message(sprintf( - 'Function %s() has parameter $%s with generic %s but does not specify its types: %s', + 'Function %s() has %s with generic %s but does not specify its types: %s', $functionReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $name, implode(', ', $genericTypeNames), - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + )) + ->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP) + ->identifier('missingType.generics') + ->build(); } foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($parameterType) as $callableType) { $messages[] = RuleErrorBuilder::message(sprintf( - 'Function %s() has parameter $%s with no signature specified for %s.', + 'Function %s() has %s with no signature specified for %s.', $functionReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $callableType->describe(VerbosityLevel::typeOnly()), - ))->build(); + ))->identifier('missingType.callable')->build(); } return $messages; diff --git a/src/Rules/Functions/MissingFunctionReturnTypehintRule.php b/src/Rules/Functions/MissingFunctionReturnTypehintRule.php index 6cb79530d8..9f2607f813 100644 --- a/src/Rules/Functions/MissingFunctionReturnTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionReturnTypehintRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InFunctionNode; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -34,11 +33,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $functionReflection = $scope->getFunction(); - if (!$functionReflection instanceof PhpFunctionFromParserNodeReflection) { - return []; - } - + $functionReflection = $node->getFunctionReflection(); $returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); if ($returnType instanceof MixedType && !$returnType->isExplicitMixed()) { @@ -46,14 +41,17 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Function %s() has no return type specified.', $functionReflection->getName(), - ))->build(), + ))->identifier('missingType.return')->build(), ]; } $messages = []; foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($returnType) as $iterableType) { $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); - $messages[] = RuleErrorBuilder::message(sprintf('Function %s() return type has no value type specified in iterable type %s.', $functionReflection->getName(), $iterableTypeDescription))->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); + $messages[] = RuleErrorBuilder::message(sprintf('Function %s() return type has no value type specified in iterable type %s.', $functionReflection->getName(), $iterableTypeDescription)) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); } foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($returnType) as [$name, $genericTypeNames]) { @@ -62,7 +60,10 @@ public function processNode(Node $node, Scope $scope): array $functionReflection->getName(), $name, implode(', ', $genericTypeNames), - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + )) + ->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP) + ->identifier('missingType.generics') + ->build(); } foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($returnType) as $callableType) { @@ -70,7 +71,7 @@ public function processNode(Node $node, Scope $scope): array 'Function %s() return type has no signature specified for %s.', $functionReflection->getName(), $callableType->describe(VerbosityLevel::typeOnly()), - ))->build(); + ))->identifier('missingType.callable')->build(); } return $messages; diff --git a/src/Rules/Functions/ParamAttributesRule.php b/src/Rules/Functions/ParamAttributesRule.php index aeadb1cfb6..ee9e8f257c 100644 --- a/src/Rules/Functions/ParamAttributesRule.php +++ b/src/Rules/Functions/ParamAttributesRule.php @@ -7,7 +7,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; -use function count; /** * @implements Rule @@ -27,25 +26,16 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $targetName = 'parameter'; + $targetType = Attribute::TARGET_PARAMETER; if ($node->flags !== 0) { $targetName = 'parameter or property'; - - $propertyTargetErrors = $this->attributesCheck->check( - $scope, - $node->attrGroups, - Attribute::TARGET_PROPERTY, - $targetName, - ); - - if (count($propertyTargetErrors) === 0) { - return $propertyTargetErrors; - } + $targetType |= Attribute::TARGET_PROPERTY; } return $this->attributesCheck->check( $scope, $node->attrGroups, - Attribute::TARGET_PARAMETER, + $targetType, $targetName, ); } diff --git a/src/Rules/Functions/PrintfParametersRule.php b/src/Rules/Functions/PrintfParametersRule.php index b6f0552b65..8f5f9f2bd5 100644 --- a/src/Rules/Functions/PrintfParametersRule.php +++ b/src/Rules/Functions/PrintfParametersRule.php @@ -9,8 +9,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\TypeUtils; use function array_filter; +use function array_key_exists; use function count; use function in_array; use function max; @@ -54,7 +54,7 @@ public function processNode(Node $node, Scope $scope): array ]; $name = strtolower((string) $node->name); - if (!isset($functionsArgumentPositions[$name])) { + if (!array_key_exists($name, $functionsArgumentPositions)) { return []; } @@ -73,7 +73,7 @@ public function processNode(Node $node, Scope $scope): array $formatArgType = $scope->getType($args[$formatArgumentPosition]->value); $placeHoldersCount = null; - foreach (TypeUtils::getConstantStrings($formatArgType) as $formatString) { + foreach ($formatArgType->getConstantStrings() as $formatString) { $format = $formatString->getValue(); $tempPlaceHoldersCount = $this->getPlaceholdersCount($name, $format); if ($placeHoldersCount === null) { @@ -100,7 +100,7 @@ public function processNode(Node $node, Scope $scope): array $name, $placeHoldersCount, $argsCount - 1, - ))->build(), + ))->identifier(sprintf('argument.%s', $name))->build(), ]; } @@ -109,7 +109,7 @@ public function processNode(Node $node, Scope $scope): array private function getPlaceholdersCount(string $functionName, string $format): int { - $specifiers = in_array($functionName, ['sprintf', 'printf'], true) ? '[bcdeEfFgGosuxX%s]' : '(?:[cdDeEfinosuxX%s]|\[[^\]]+\])'; + $specifiers = in_array($functionName, ['sprintf', 'printf'], true) ? '(?:[bs%s]|l?[cdeEgfFGouxX])' : '(?:[cdDeEfinosuxX%s]|\[[^\]]+\])'; $addSpecifier = ''; if ($this->phpVersion->supportsHhPrintfSpecifier()) { $addSpecifier .= 'hH'; @@ -117,7 +117,7 @@ private function getPlaceholdersCount(string $functionName, string $format): int $specifiers = sprintf($specifiers, $addSpecifier); - $pattern = '~(?%*)%(?:(?\d+)\$)?[-+]?(?:[ 0]|(?:\'[^%]))?-?\d*(?:\.\d*)?' . $specifiers . '~'; + $pattern = '~(?%*)%(?:(?\d+)\$)?[-+]?(?:[ 0]|(?:\'[^%]))?(?\*)?-?\d*(?:\.(?:\d+|(?\*))?)?' . $specifiers . '~'; $matches = Strings::matchAll($format, $pattern, PREG_SET_ORDER); @@ -134,6 +134,14 @@ private function getPlaceholdersCount(string $functionName, string $format): int $maxPositionedNumber = 0; $maxOrdinaryNumber = 0; foreach ($placeholders as $placeholder) { + if (isset($placeholder['width']) && $placeholder['width'] !== '') { + $maxOrdinaryNumber++; + } + + if (isset($placeholder['precision']) && $placeholder['precision'] !== '') { + $maxOrdinaryNumber++; + } + if (isset($placeholder['position']) && $placeholder['position'] !== '') { $maxPositionedNumber = max((int) $placeholder['position'], $maxPositionedNumber); } else { diff --git a/src/Rules/Functions/RandomIntParametersRule.php b/src/Rules/Functions/RandomIntParametersRule.php index f3ce19c174..cc9da2c3ed 100644 --- a/src/Rules/Functions/RandomIntParametersRule.php +++ b/src/Rules/Functions/RandomIntParametersRule.php @@ -64,7 +64,7 @@ public function processNode(Node $node, Scope $scope): array $message, $minType->describe(VerbosityLevel::value()), $maxType->describe(VerbosityLevel::value()), - ))->build(), + ))->identifier('argument.type')->build(), ]; } diff --git a/src/Rules/Functions/RedefinedParametersRule.php b/src/Rules/Functions/RedefinedParametersRule.php new file mode 100644 index 0000000000..f364c82b9d --- /dev/null +++ b/src/Rules/Functions/RedefinedParametersRule.php @@ -0,0 +1,60 @@ + + */ +class RedefinedParametersRule implements Rule +{ + + public function getNodeType(): string + { + return Node\FunctionLike::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $params = $node->getParams(); + + if (count($params) <= 1) { + return []; + } + + $vars = []; + $errors = []; + + foreach ($params as $param) { + if (!$param->var instanceof Node\Expr\Variable) { + continue; + } + + if (!is_string($param->var->name)) { + continue; + } + + $var = $param->var->name; + + if (!isset($vars[$var])) { + $vars[$var] = true; + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf('Redefinition of parameter $%s.', $var)) + ->identifier('parameter.duplicate') + ->nonIgnorable() + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Functions/ReturnNullsafeByRefRule.php b/src/Rules/Functions/ReturnNullsafeByRefRule.php index d842cf4584..84f2c81f3c 100644 --- a/src/Rules/Functions/ReturnNullsafeByRefRule.php +++ b/src/Rules/Functions/ReturnNullsafeByRefRule.php @@ -41,7 +41,11 @@ public function processNode(Node $node, Scope $scope): array continue; } - $errors[] = RuleErrorBuilder::message('Nullsafe cannot be returned by reference.')->line($returnNode->getLine())->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message('Nullsafe cannot be returned by reference.') + ->line($returnNode->getStartLine()) + ->identifier('nullsafe.byRef') + ->nonIgnorable() + ->build(); } return $errors; diff --git a/src/Rules/Functions/UnusedClosureUsesRule.php b/src/Rules/Functions/UnusedClosureUsesRule.php index 79b7268c9b..0caa2c2ae9 100644 --- a/src/Rules/Functions/UnusedClosureUsesRule.php +++ b/src/Rules/Functions/UnusedClosureUsesRule.php @@ -42,13 +42,7 @@ public function processNode(Node $node, Scope $scope): array }, $node->uses), $node->stmts, 'Anonymous function has an unused use $%s.', - 'anonymousFunction.unusedUse', - [ - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - 'depth' => $node->getAttribute('expressionDepth'), - 'order' => $node->getAttribute('expressionOrder'), - ], + 'closure.unusedUse', ); } diff --git a/src/Rules/Functions/VariadicParametersDeclarationRule.php b/src/Rules/Functions/VariadicParametersDeclarationRule.php new file mode 100644 index 0000000000..2d7c048d80 --- /dev/null +++ b/src/Rules/Functions/VariadicParametersDeclarationRule.php @@ -0,0 +1,51 @@ + + */ +class VariadicParametersDeclarationRule implements Rule +{ + + public function getNodeType(): string + { + return Node\FunctionLike::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $parameters = $node->getParams(); + $paramCount = count($parameters); + + if ($paramCount === 0) { + return []; + } + + $errors = []; + + foreach ($parameters as $index => $parameter) { + if (!$parameter->variadic) { + continue; + } + + if ($paramCount - 1 === $index) { + continue; + } + + $errors[] = RuleErrorBuilder::message('Only the last parameter can be variadic.') + ->nonIgnorable() + ->identifier('parameter.variadicNotLast') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Generators/YieldFromTypeRule.php b/src/Rules/Generators/YieldFromTypeRule.php index 8926b3f270..deb3f8212e 100644 --- a/src/Rules/Generators/YieldFromTypeRule.php +++ b/src/Rules/Generators/YieldFromTypeRule.php @@ -10,11 +10,9 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; -use PHPStan\Type\GenericTypeVariableResolver; +use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; use function sprintf; /** @@ -45,7 +43,10 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( $messagePattern, $exprType->describe(VerbosityLevel::typeOnly()), - ))->line($node->expr->getLine())->build(), + )) + ->line($node->expr->getStartLine()) + ->identifier('generator.nonIterable') + ->build(), ]; } elseif ( !$exprType instanceof MixedType @@ -56,7 +57,10 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( $messagePattern, $exprType->describe(VerbosityLevel::typeOnly()), - ))->line($node->expr->getLine())->build(), + )) + ->line($node->expr->getStartLine()) + ->identifier('generator.nonIterable') + ->build(), ]; } @@ -75,21 +79,32 @@ public function processNode(Node $node, Scope $scope): array } $messages = []; - if (!$this->ruleLevelHelper->accepts($returnType->getIterableKeyType(), $exprType->getIterableKeyType(), $scope->isDeclareStrictTypes())) { + $acceptsKey = $this->ruleLevelHelper->acceptsWithReason($returnType->getIterableKeyType(), $exprType->getIterableKeyType(), $scope->isDeclareStrictTypes()); + if (!$acceptsKey->result) { $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableKeyType(), $exprType->getIterableKeyType()); $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects key type %s, %s given.', $returnType->getIterableKeyType()->describe($verbosityLevel), $exprType->getIterableKeyType()->describe($verbosityLevel), - ))->line($node->expr->getLine())->build(); + )) + ->line($node->expr->getStartLine()) + ->identifier('generator.keyType') + ->acceptsReasonsTip($acceptsKey->reasons) + ->build(); } - if (!$this->ruleLevelHelper->accepts($returnType->getIterableValueType(), $exprType->getIterableValueType(), $scope->isDeclareStrictTypes())) { + + $acceptsValue = $this->ruleLevelHelper->acceptsWithReason($returnType->getIterableValueType(), $exprType->getIterableValueType(), $scope->isDeclareStrictTypes()); + if (!$acceptsValue->result) { $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableValueType(), $exprType->getIterableValueType()); $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects value type %s, %s given.', $returnType->getIterableValueType()->describe($verbosityLevel), $exprType->getIterableValueType()->describe($verbosityLevel), - ))->line($node->expr->getLine())->build(); + )) + ->line($node->expr->getStartLine()) + ->identifier('generator.valueType') + ->acceptsReasonsTip($acceptsValue->reasons) + ->build(); } $scopeFunction = $scope->getFunction(); @@ -97,18 +112,10 @@ public function processNode(Node $node, Scope $scope): array return $messages; } - if (!$exprType instanceof TypeWithClassName) { - return $messages; - } - $currentReturnType = ParametersAcceptorSelector::selectSingle($scopeFunction->getVariants())->getReturnType(); - if (!$currentReturnType instanceof TypeWithClassName) { - return $messages; - } - - $exprSendType = GenericTypeVariableResolver::getType($exprType, Generator::class, 'TSend'); - $thisSendType = GenericTypeVariableResolver::getType($currentReturnType, Generator::class, 'TSend'); - if ($exprSendType === null || $thisSendType === null) { + $exprSendType = $exprType->getTemplateType(Generator::class, 'TSend'); + $thisSendType = $currentReturnType->getTemplateType(Generator::class, 'TSend'); + if ($exprSendType instanceof ErrorType || $thisSendType instanceof ErrorType) { return $messages; } @@ -118,17 +125,19 @@ public function processNode(Node $node, Scope $scope): array 'Generator expects delegated TSend type %s, %s given.', $exprSendType->describe(VerbosityLevel::typeOnly()), $thisSendType->describe(VerbosityLevel::typeOnly()), - ))->build(); + ))->identifier('generator.sendType')->build(); } elseif ($this->reportMaybes && !$isSuperType->yes()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects delegated TSend type %s, %s given.', $exprSendType->describe(VerbosityLevel::typeOnly()), $thisSendType->describe(VerbosityLevel::typeOnly()), - ))->build(); + ))->identifier('generator.sendType')->build(); } - if ($scope->getType($node) instanceof VoidType && !$scope->isInFirstLevelStatement()) { - $messages[] = RuleErrorBuilder::message('Result of yield from (void) is used.')->build(); + if (!$scope->isInFirstLevelStatement() && $scope->getType($node)->isVoid()->yes()) { + $messages[] = RuleErrorBuilder::message('Result of yield from (void) is used.') + ->identifier('generator.void') + ->build(); } return $messages; diff --git a/src/Rules/Generators/YieldInGeneratorRule.php b/src/Rules/Generators/YieldInGeneratorRule.php index 230853ca38..9be06b937d 100644 --- a/src/Rules/Generators/YieldInGeneratorRule.php +++ b/src/Rules/Generators/YieldInGeneratorRule.php @@ -40,7 +40,12 @@ public function processNode(Node $node, Scope $scope): array } elseif ($scopeFunction !== null) { $returnType = ParametersAcceptorSelector::selectSingle($scopeFunction->getVariants())->getReturnType(); } else { - return [RuleErrorBuilder::message('Yield can be used only inside a function.')->build()]; + return [ + RuleErrorBuilder::message('Yield can be used only inside a function.') + ->identifier('generator.outOfFunction') + ->nonIgnorable() + ->build(), + ]; } if ($returnType instanceof MixedType) { @@ -66,7 +71,7 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Yield can be used only with these return types: %s.', 'Generator, Iterator, Traversable, iterable', - ))->build(), + ))->identifier('generator.returnType')->build(), ]; } diff --git a/src/Rules/Generators/YieldTypeRule.php b/src/Rules/Generators/YieldTypeRule.php index c5678f8a66..3de12e41a3 100644 --- a/src/Rules/Generators/YieldTypeRule.php +++ b/src/Rules/Generators/YieldTypeRule.php @@ -12,7 +12,6 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; use function sprintf; /** @@ -54,31 +53,42 @@ public function processNode(Node $node, Scope $scope): array $keyType = $scope->getType($node->key); } - if ($node->value === null) { - $valueType = new NullType(); - } else { - $valueType = $scope->getType($node->value); - } - $messages = []; - if (!$this->ruleLevelHelper->accepts($returnType->getIterableKeyType(), $keyType, $scope->isDeclareStrictTypes())) { + $acceptsKey = $this->ruleLevelHelper->acceptsWithReason($returnType->getIterableKeyType(), $keyType, $scope->isDeclareStrictTypes()); + if (!$acceptsKey->result) { $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableKeyType(), $keyType); $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects key type %s, %s given.', $returnType->getIterableKeyType()->describe($verbosityLevel), $keyType->describe($verbosityLevel), - ))->build(); + )) + ->acceptsReasonsTip($acceptsKey->reasons) + ->identifier('generator.keyType') + ->build(); } - if (!$this->ruleLevelHelper->accepts($returnType->getIterableValueType(), $valueType, $scope->isDeclareStrictTypes())) { + + if ($node->value === null) { + $valueType = new NullType(); + } else { + $valueType = $scope->getType($node->value); + } + + $acceptsValue = $this->ruleLevelHelper->acceptsWithReason($returnType->getIterableValueType(), $valueType, $scope->isDeclareStrictTypes()); + if (!$acceptsValue->result) { $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType->getIterableValueType(), $valueType); $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects value type %s, %s given.', $returnType->getIterableValueType()->describe($verbosityLevel), $valueType->describe($verbosityLevel), - ))->build(); + )) + ->acceptsReasonsTip($acceptsValue->reasons) + ->identifier('generator.valueType') + ->build(); } - if ($scope->getType($node) instanceof VoidType && !$scope->isInFirstLevelStatement()) { - $messages[] = RuleErrorBuilder::message('Result of yield (void) is used.')->build(); + if (!$scope->isInFirstLevelStatement() && $scope->getType($node)->isVoid()->yes()) { + $messages[] = RuleErrorBuilder::message('Result of yield (void) is used.') + ->identifier('generator.void') + ->build(); } return $messages; diff --git a/src/Rules/Generics/ClassAncestorsRule.php b/src/Rules/Generics/ClassAncestorsRule.php index 4c77d8899a..0359073c44 100644 --- a/src/Rules/Generics/ClassAncestorsRule.php +++ b/src/Rules/Generics/ClassAncestorsRule.php @@ -38,10 +38,7 @@ public function processNode(Node $node, Scope $scope): array if (!$originalNode instanceof Node\Stmt\Class_) { return []; } - if (!$scope->isInClass()) { - return []; - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); if ($classReflection->isAnonymous()) { return []; } @@ -58,6 +55,7 @@ public function processNode(Node $node, Scope $scope): array 'Generic type %s in PHPDoc tag @extends does not specify all template types of %s %s: %s', 'Generic type %s in PHPDoc tag @extends specifies %d template types, but %s %s supports only %d: %s', 'Type %s in generic type %s in PHPDoc tag @extends is not subtype of template type %s of %s %s.', + 'Call-site variance annotation of %s in generic type %s in PHPDoc tag @extends is not allowed.', 'PHPDoc tag @extends has invalid type %s.', sprintf('Class %s extends generic class %%s but does not specify its types: %%s', $escapedClassName), sprintf('in extended type %%s of class %s', $escapedClassName), @@ -73,6 +71,7 @@ public function processNode(Node $node, Scope $scope): array 'Generic type %s in PHPDoc tag @implements does not specify all template types of %s %s: %s', 'Generic type %s in PHPDoc tag @implements specifies %d template types, but %s %s supports only %d: %s', 'Type %s in generic type %s in PHPDoc tag @implements is not subtype of template type %s of %s %s.', + 'Call-site variance annotation of %s in generic type %s in PHPDoc tag @implements is not allowed.', 'PHPDoc tag @implements has invalid type %s.', sprintf('Class %s implements generic interface %%s but does not specify its types: %%s', $escapedClassName), sprintf('in implemented type %%s of class %s', $escapedClassName), diff --git a/src/Rules/Generics/ClassTemplateTypeRule.php b/src/Rules/Generics/ClassTemplateTypeRule.php index 5e24afd305..a21d0561e5 100644 --- a/src/Rules/Generics/ClassTemplateTypeRule.php +++ b/src/Rules/Generics/ClassTemplateTypeRule.php @@ -29,10 +29,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - return []; - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); if (!$classReflection->isClass()) { return []; } @@ -44,6 +41,7 @@ public function processNode(Node $node, Scope $scope): array } return $this->templateTypeCheck->check( + $scope, $node, TemplateTypeScope::createWithClass($className), $classReflection->getTemplateTags(), diff --git a/src/Rules/Generics/CrossCheckInterfacesHelper.php b/src/Rules/Generics/CrossCheckInterfacesHelper.php index f272063e24..e35854b5ec 100644 --- a/src/Rules/Generics/CrossCheckInterfacesHelper.php +++ b/src/Rules/Generics/CrossCheckInterfacesHelper.php @@ -3,7 +3,7 @@ namespace PHPStan\Rules\Generics; use PHPStan\Reflection\ClassReflection; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; use function array_key_exists; @@ -13,7 +13,7 @@ class CrossCheckInterfacesHelper { /** - * @return RuleError[] + * @return list */ public function check(ClassReflection $classReflection): array { @@ -44,7 +44,7 @@ public function check(ClassReflection $classReflection): array $interface->getName(), $type->describe(VerbosityLevel::value()), $otherType->describe(VerbosityLevel::value()), - ))->build(); + ))->identifier('generics.interfaceConflict')->build(); } continue; } diff --git a/src/Rules/Generics/EnumAncestorsRule.php b/src/Rules/Generics/EnumAncestorsRule.php index 63219efb22..8c786d7359 100644 --- a/src/Rules/Generics/EnumAncestorsRule.php +++ b/src/Rules/Generics/EnumAncestorsRule.php @@ -38,10 +38,7 @@ public function processNode(Node $node, Scope $scope): array if (!$originalNode instanceof Node\Stmt\Enum_) { return []; } - if (!$scope->isInClass()) { - return []; - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); $enumName = $classReflection->getName(); $escapedEnumName = SprintfHelper::escapeFormatString($enumName); @@ -59,6 +56,7 @@ public function processNode(Node $node, Scope $scope): array '', '', '', + '', ); $implementsErrors = $this->genericAncestorsCheck->check( @@ -71,6 +69,7 @@ public function processNode(Node $node, Scope $scope): array 'Generic type %s in PHPDoc tag @implements does not specify all template types of %s %s: %s', 'Generic type %s in PHPDoc tag @implements specifies %d template types, but %s %s supports only %d: %s', 'Type %s in generic type %s in PHPDoc tag @implements is not subtype of template type %s of %s %s.', + 'Call-site variance annotation of %s in generic type %s in PHPDoc tag @implements is not allowed.', 'PHPDoc tag @implements has invalid type %s.', sprintf('Enum %s implements generic interface %%s but does not specify its types: %%s', $escapedEnumName), sprintf('in implemented type %%s of enum %s', $escapedEnumName), diff --git a/src/Rules/Generics/EnumTemplateTypeRule.php b/src/Rules/Generics/EnumTemplateTypeRule.php index a52ac23255..9c149811bf 100644 --- a/src/Rules/Generics/EnumTemplateTypeRule.php +++ b/src/Rules/Generics/EnumTemplateTypeRule.php @@ -23,10 +23,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - return []; - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); if (!$classReflection->isEnum()) { return []; } @@ -39,7 +36,9 @@ public function processNode(Node $node, Scope $scope): array $className = $classReflection->getDisplayName(); return [ - RuleErrorBuilder::message(sprintf('Enum %s has PHPDoc @template tag%s but enums cannot be generic.', $className, $templateTagsCount === 1 ? '' : 's'))->build(), + RuleErrorBuilder::message(sprintf('Enum %s has PHPDoc @template tag%s but enums cannot be generic.', $className, $templateTagsCount === 1 ? '' : 's')) + ->identifier('enum.generic') + ->build(), ]; } diff --git a/src/Rules/Generics/FunctionSignatureVarianceRule.php b/src/Rules/Generics/FunctionSignatureVarianceRule.php index 9b75ab2f57..e95b2e7712 100644 --- a/src/Rules/Generics/FunctionSignatureVarianceRule.php +++ b/src/Rules/Generics/FunctionSignatureVarianceRule.php @@ -27,19 +27,18 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $functionReflection = $scope->getFunction(); - if ($functionReflection === null) { - return []; - } - + $functionReflection = $node->getFunctionReflection(); $functionName = $functionReflection->getName(); return $this->varianceCheck->checkParametersAcceptor( ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()), sprintf('in parameter %%s of function %s()', SprintfHelper::escapeFormatString($functionName)), + sprintf('in param-out type of parameter %%s of function %s()', SprintfHelper::escapeFormatString($functionName)), sprintf('in return type of function %s()', $functionName), sprintf('in function %s()', $functionName), false, + false, + 'function', ); } diff --git a/src/Rules/Generics/FunctionTemplateTypeRule.php b/src/Rules/Generics/FunctionTemplateTypeRule.php index 5353d0a9cf..3700dd3b73 100644 --- a/src/Rules/Generics/FunctionTemplateTypeRule.php +++ b/src/Rules/Generics/FunctionTemplateTypeRule.php @@ -52,6 +52,7 @@ public function processNode(Node $node, Scope $scope): array $escapedFunctionName = SprintfHelper::escapeFormatString($functionName); return $this->templateTypeCheck->check( + $scope, $node, TemplateTypeScope::createWithFunction($functionName), $resolvedPhpDoc->getTemplateTags(), diff --git a/src/Rules/Generics/GenericAncestorsCheck.php b/src/Rules/Generics/GenericAncestorsCheck.php index 4210bf7e29..14ebfb0c71 100644 --- a/src/Rules/Generics/GenericAncestorsCheck.php +++ b/src/Rules/Generics/GenericAncestorsCheck.php @@ -5,11 +5,12 @@ use PhpParser\Node; use PhpParser\Node\Name; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\MissingTypehintCheck; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TypeProjectionHelper; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function array_fill_keys; @@ -40,7 +41,7 @@ public function __construct( /** * @param array $nameNodes * @param array $ancestorTypes - * @return RuleError[] + * @return list */ public function check( array $nameNodes, @@ -52,6 +53,7 @@ public function check( string $notEnoughTypesMessage, string $extraTypesMessage, string $typeIsNotSubtypeMessage, + string $typeProjectionIsNotAllowedMessage, string $invalidTypeMessage, string $genericClassInNonGenericObjectType, string $invalidVarianceMessage, @@ -64,16 +66,22 @@ public function check( $messages = []; foreach ($ancestorTypes as $ancestorType) { if (!$ancestorType instanceof GenericObjectType) { - $messages[] = RuleErrorBuilder::message(sprintf($incompatibleTypeMessage, $ancestorType->describe(VerbosityLevel::typeOnly())))->build(); + $messages[] = RuleErrorBuilder::message(sprintf($incompatibleTypeMessage, $ancestorType->describe(VerbosityLevel::typeOnly()))) + ->identifier('generics.notCompatible') + ->build(); continue; } $ancestorTypeClassName = $ancestorType->getClassName(); if (!isset($names[$ancestorTypeClassName])) { if (count($names) === 0) { - $messages[] = RuleErrorBuilder::message($noNamesMessage)->build(); + $messages[] = RuleErrorBuilder::message($noNamesMessage) + ->identifier('generics.noParent') + ->build(); } else { - $messages[] = RuleErrorBuilder::message(sprintf($noRelatedNameMessage, $ancestorTypeClassName, implode(', ', array_keys($names))))->build(); + $messages[] = RuleErrorBuilder::message(sprintf($noRelatedNameMessage, $ancestorTypeClassName, implode(', ', array_keys($names)))) + ->identifier('generics.wrongParent') + ->build(); } continue; @@ -87,6 +95,8 @@ public function check( $notEnoughTypesMessage, $extraTypesMessage, $typeIsNotSubtypeMessage, + '', + '', ); $messages = array_merge($messages, $genericObjectTypeCheckMessages); @@ -95,10 +105,12 @@ public function check( continue; } - $messages[] = RuleErrorBuilder::message(sprintf($invalidTypeMessage, $referencedClass))->build(); + $messages[] = RuleErrorBuilder::message(sprintf($invalidTypeMessage, $referencedClass)) + ->identifier('class.notFound') + ->build(); } - $variance = TemplateTypeVariance::createInvariant(); + $variance = TemplateTypeVariance::createStatic(); $messageContext = sprintf( $invalidVarianceMessage, $ancestorType->describe(VerbosityLevel::typeOnly()), @@ -106,6 +118,18 @@ public function check( foreach ($this->varianceCheck->check($variance, $ancestorType, $messageContext) as $message) { $messages[] = $message; } + + foreach ($ancestorType->getVariances() as $index => $typeVariance) { + if ($typeVariance->invariant()) { + continue; + } + + $messages[] = RuleErrorBuilder::message(sprintf( + $typeProjectionIsNotAllowedMessage, + TypeProjectionHelper::describe($ancestorType->getTypes()[$index], $typeVariance, VerbosityLevel::typeOnly()), + $ancestorType->describe(VerbosityLevel::typeOnly()), + ))->identifier('generics.callSiteVarianceNotAllowed')->build(); + } } if ($this->checkGenericClassInNonGenericObjectType) { @@ -126,7 +150,10 @@ public function check( $genericClassInNonGenericObjectType, $unusedName, implode(', ', array_keys($unusedNameClassReflection->getTemplateTypeMap()->getTypes())), - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + )) + ->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP) + ->identifier('missingType.generics') + ->build(); } } diff --git a/src/Rules/Generics/GenericObjectTypeCheck.php b/src/Rules/Generics/GenericObjectTypeCheck.php index db2eaf27d6..9df0d06b44 100644 --- a/src/Rules/Generics/GenericObjectTypeCheck.php +++ b/src/Rules/Generics/GenericObjectTypeCheck.php @@ -2,12 +2,15 @@ namespace PHPStan\Rules\Generics; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeHelper; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; +use PHPStan\Type\Generic\TypeProjectionHelper; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use PHPStan\Type\VerbosityLevel; @@ -16,12 +19,13 @@ use function count; use function implode; use function sprintf; +use function strtolower; class GenericObjectTypeCheck { /** - * @return RuleError[] + * @return list */ public function check( Type $phpDocType, @@ -29,6 +33,8 @@ public function check( string $notEnoughTypesMessage, string $extraTypesMessage, string $typeIsNotSubtypeMessage, + string $typeProjectionHasConflictingVarianceMessage, + string $typeProjectionIsRedundantMessage, ): array { $genericTypes = $this->getGenericTypes($phpDocType); @@ -39,22 +45,18 @@ public function check( continue; } - $classLikeDescription = 'class'; - if ($classReflection->isInterface()) { - $classLikeDescription = 'interface'; - } elseif ($classReflection->isTrait()) { - $classLikeDescription = 'trait'; - } elseif ($classReflection->isEnum()) { - $classLikeDescription = 'enum'; - } + $classLikeDescription = strtolower($classReflection->getClassTypeDescription()); if (!$classReflection->isGeneric()) { - $messages[] = RuleErrorBuilder::message(sprintf($classNotGenericMessage, $genericType->describe(VerbosityLevel::typeOnly()), $classLikeDescription, $classReflection->getDisplayName()))->build(); + $messages[] = RuleErrorBuilder::message(sprintf($classNotGenericMessage, $genericType->describe(VerbosityLevel::typeOnly()), $classLikeDescription, $classReflection->getDisplayName())) + ->identifier('generics.notGeneric') + ->build(); continue; } $templateTypes = array_values($classReflection->getTemplateTypeMap()->getTypes()); $genericTypeTypes = $genericType->getTypes(); + $genericTypeVariances = $genericType->getVariances(); $templateTypesCount = count($templateTypes); $genericTypeTypesCount = count($genericTypeTypes); if ($templateTypesCount > $genericTypeTypesCount) { @@ -64,7 +66,7 @@ public function check( $classLikeDescription, $classReflection->getDisplayName(false), implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())), - ))->build(); + ))->identifier('generics.lessTypes')->build(); } elseif ($templateTypesCount < $genericTypeTypesCount) { $messages[] = RuleErrorBuilder::message(sprintf( $extraTypesMessage, @@ -74,7 +76,7 @@ public function check( $classReflection->getDisplayName(false), $templateTypesCount, implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())), - ))->build(); + ))->identifier('generics.moreTypes')->build(); } $templateTypesCount = count($templateTypes); @@ -84,8 +86,36 @@ public function check( } $templateType = $templateTypes[$i]; - $boundType = TemplateTypeHelper::resolveToBounds($templateType); $genericTypeType = $genericTypeTypes[$i]; + + $genericTypeVariance = $genericTypeVariances[$i] ?? TemplateTypeVariance::createInvariant(); + if ($templateType instanceof TemplateType && !$genericTypeVariance->invariant()) { + if ($genericTypeVariance->equals($templateType->getVariance())) { + $messages[] = RuleErrorBuilder::message(sprintf( + $typeProjectionIsRedundantMessage, + TypeProjectionHelper::describe($genericTypeType, $genericTypeVariance, VerbosityLevel::typeOnly()), + $genericType->describe(VerbosityLevel::typeOnly()), + $templateType->describe(VerbosityLevel::typeOnly()), + $classLikeDescription, + $classReflection->getDisplayName(false), + )) + ->identifier('generics.callSiteVarianceRedundant') + ->tip('You can safely remove the call-site variance annotation.') + ->build(); + } elseif (!$genericTypeVariance->validPosition($templateType->getVariance())) { + $messages[] = RuleErrorBuilder::message(sprintf( + $typeProjectionHasConflictingVarianceMessage, + TypeProjectionHelper::describe($genericTypeType, $genericTypeVariance, VerbosityLevel::typeOnly()), + $genericType->describe(VerbosityLevel::typeOnly()), + $templateType->getVariance()->describe(), + $templateType->describe(VerbosityLevel::typeOnly()), + $classLikeDescription, + $classReflection->getDisplayName(false), + ))->identifier('generics.callSiteVarianceConflict')->build(); + } + } + + $boundType = TemplateTypeHelper::resolveToBounds($templateType); if ($boundType->isSuperTypeOf($genericTypeType)->yes()) { if (!$templateType instanceof TemplateType) { continue; @@ -96,11 +126,20 @@ public function check( continue; } - $templateTypes[$j] = TemplateTypeHelper::resolveTemplateTypes($templateTypes[$j], $map); + $templateTypes[$j] = TemplateTypeHelper::resolveTemplateTypes( + $templateTypes[$j], + $map, + TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createStatic(), + ); } continue; } + if ($genericTypeVariance->bivariant()) { + continue; + } + $messages[] = RuleErrorBuilder::message(sprintf( $typeIsNotSubtypeMessage, $genericTypeType->describe(VerbosityLevel::typeOnly()), @@ -108,7 +147,7 @@ public function check( $templateType->describe(VerbosityLevel::typeOnly()), $classLikeDescription, $classReflection->getDisplayName(false), - ))->build(); + ))->identifier('generics.notSubtype')->build(); } } diff --git a/src/Rules/Generics/InterfaceAncestorsRule.php b/src/Rules/Generics/InterfaceAncestorsRule.php index bb207f5ae6..465c2b9f36 100644 --- a/src/Rules/Generics/InterfaceAncestorsRule.php +++ b/src/Rules/Generics/InterfaceAncestorsRule.php @@ -38,10 +38,7 @@ public function processNode(Node $node, Scope $scope): array if (!$originalNode instanceof Node\Stmt\Interface_) { return []; } - if (!$scope->isInClass()) { - return []; - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); $interfaceName = $classReflection->getName(); $escapedInterfaceName = SprintfHelper::escapeFormatString($interfaceName); @@ -56,6 +53,7 @@ public function processNode(Node $node, Scope $scope): array 'Generic type %s in PHPDoc tag @extends does not specify all template types of %s %s: %s', 'Generic type %s in PHPDoc tag @extends specifies %d template types, but %s %s supports only %d: %s', 'Type %s in generic type %s in PHPDoc tag @extends is not subtype of template type %s of %s %s.', + 'Call-site variance annotation of %s in generic type %s in PHPDoc tag @extends is not allowed.', 'PHPDoc tag @extends has invalid type %s.', sprintf('Interface %s extends generic interface %%s but does not specify its types: %%s', $escapedInterfaceName), sprintf('in extended type %%s of interface %s', $escapedInterfaceName), @@ -74,6 +72,7 @@ public function processNode(Node $node, Scope $scope): array '', '', '', + '', ); foreach ($this->crossCheckInterfacesHelper->check($classReflection) as $error) { diff --git a/src/Rules/Generics/InterfaceTemplateTypeRule.php b/src/Rules/Generics/InterfaceTemplateTypeRule.php index dd9198e96b..e808174a9f 100644 --- a/src/Rules/Generics/InterfaceTemplateTypeRule.php +++ b/src/Rules/Generics/InterfaceTemplateTypeRule.php @@ -29,10 +29,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - return []; - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); if (!$classReflection->isInterface()) { return []; } @@ -41,6 +38,7 @@ public function processNode(Node $node, Scope $scope): array $escapadInterfaceName = SprintfHelper::escapeFormatString($interfaceName); return $this->templateTypeCheck->check( + $scope, $node, TemplateTypeScope::createWithClass($interfaceName), $classReflection->getTemplateTags(), diff --git a/src/Rules/Generics/MethodSignatureVarianceRule.php b/src/Rules/Generics/MethodSignatureVarianceRule.php index b2207734d9..4f99378be0 100644 --- a/src/Rules/Generics/MethodSignatureVarianceRule.php +++ b/src/Rules/Generics/MethodSignatureVarianceRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassMethodNode; -use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use function sprintf; @@ -28,17 +27,17 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - if (!$method instanceof MethodReflection) { - return []; - } + $method = $node->getMethodReflection(); return $this->varianceCheck->checkParametersAcceptor( ParametersAcceptorSelector::selectSingle($method->getVariants()), sprintf('in parameter %%s of method %s::%s()', SprintfHelper::escapeFormatString($method->getDeclaringClass()->getDisplayName()), SprintfHelper::escapeFormatString($method->getName())), + sprintf('in param-out type of parameter %%s of method %s::%s()', SprintfHelper::escapeFormatString($method->getDeclaringClass()->getDisplayName()), SprintfHelper::escapeFormatString($method->getName())), sprintf('in return type of method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName()), sprintf('in method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName()), - $method->getName() === '__construct' || $method->isStatic(), + $method->isStatic(), + $method->isPrivate() || $method->getName() === '__construct', + 'method', ); } diff --git a/src/Rules/Generics/MethodTagTemplateTypeRule.php b/src/Rules/Generics/MethodTagTemplateTypeRule.php new file mode 100644 index 0000000000..6b60d6557a --- /dev/null +++ b/src/Rules/Generics/MethodTagTemplateTypeRule.php @@ -0,0 +1,86 @@ + + */ +class MethodTagTemplateTypeRule implements Rule +{ + + public function __construct( + private FileTypeMapper $fileTypeMapper, + private TemplateTypeCheck $templateTypeCheck, + ) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + + $classReflection = $node->getClassReflection(); + $className = $classReflection->getDisplayName(); + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $classReflection->getName(), + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + null, + $docComment->getText(), + ); + + $messages = []; + $escapedClassName = SprintfHelper::escapeFormatString($className); + $classTemplateTypes = $classReflection->getTemplateTypeMap()->getTypes(); + + foreach ($resolvedPhpDoc->getMethodTags() as $methodName => $methodTag) { + $methodTemplateTags = $methodTag->getTemplateTags(); + $escapedMethodName = SprintfHelper::escapeFormatString($methodName); + + $messages = array_merge($messages, $this->templateTypeCheck->check( + $scope, + $node, + TemplateTypeScope::createWithMethod($className, $methodName), + $methodTemplateTags, + sprintf('PHPDoc tag @method template for method %s::%s() cannot have existing class %%s as its name.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @method template for method %s::%s() cannot have existing type alias %%s as its name.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @method template %%s for method %s::%s() has invalid bound type %%s.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @method template %%s for method %s::%s() with bound type %%s is not supported.', $escapedClassName, $escapedMethodName), + )); + + foreach (array_keys($methodTemplateTags) as $name) { + if (!isset($classTemplateTypes[$name])) { + continue; + } + + $messages[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @method template %s for method %s::%s() shadows @template %s for class %s.', $name, $className, $methodName, $classTemplateTypes[$name]->describe(VerbosityLevel::typeOnly()), $classReflection->getDisplayName(false))) + ->identifier('methodTag.shadowTemplate') + ->build(); + } + } + + return $messages; + } + +} diff --git a/src/Rules/Generics/MethodTemplateTypeRule.php b/src/Rules/Generics/MethodTemplateTypeRule.php index 7f51d9e0d5..80f21c3de7 100644 --- a/src/Rules/Generics/MethodTemplateTypeRule.php +++ b/src/Rules/Generics/MethodTemplateTypeRule.php @@ -58,6 +58,7 @@ public function processNode(Node $node, Scope $scope): array $escapedClassName = SprintfHelper::escapeFormatString($className); $escapedMethodName = SprintfHelper::escapeFormatString($methodName); $messages = $this->templateTypeCheck->check( + $scope, $node, TemplateTypeScope::createWithMethod($className, $methodName), $methodTemplateTags, @@ -73,7 +74,9 @@ public function processNode(Node $node, Scope $scope): array continue; } - $messages[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @template %s for method %s::%s() shadows @template %s for class %s.', $name, $className, $methodName, $classTemplateTypes[$name]->describe(VerbosityLevel::typeOnly()), $classReflection->getDisplayName(false)))->build(); + $messages[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @template %s for method %s::%s() shadows @template %s for class %s.', $name, $className, $methodName, $classTemplateTypes[$name]->describe(VerbosityLevel::typeOnly()), $classReflection->getDisplayName(false))) + ->identifier('method.shadowTemplate') + ->build(); } return $messages; diff --git a/src/Rules/Generics/PropertyVarianceRule.php b/src/Rules/Generics/PropertyVarianceRule.php new file mode 100644 index 0000000000..b62c494ba5 --- /dev/null +++ b/src/Rules/Generics/PropertyVarianceRule.php @@ -0,0 +1,56 @@ + + */ +class PropertyVarianceRule implements Rule +{ + + public function __construct( + private VarianceCheck $varianceCheck, + private bool $readOnlyByPhpDoc, + ) + { + } + + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + + if (!$classReflection->hasNativeProperty($node->getName())) { + return []; + } + + $propertyReflection = $classReflection->getNativeProperty($node->getName()); + + if ($propertyReflection->isPrivate()) { + return []; + } + + $variance = $node->isReadOnly() || ($this->readOnlyByPhpDoc && $node->isReadOnlyByPhpDoc()) + ? TemplateTypeVariance::createCovariant() + : TemplateTypeVariance::createInvariant(); + + return $this->varianceCheck->check( + $variance, + $propertyReflection->getReadableType(), + sprintf('in property %s::$%s', SprintfHelper::escapeFormatString($classReflection->getDisplayName()), SprintfHelper::escapeFormatString($node->getName())), + ); + } + +} diff --git a/src/Rules/Generics/TemplateTypeCheck.php b/src/Rules/Generics/TemplateTypeCheck.php index 83b1b8fa35..234c4e8a71 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -3,12 +3,13 @@ namespace PHPStan\Rules\Generics; use PhpParser\Node; +use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; @@ -23,6 +24,7 @@ use PHPStan\Type\IntersectionType; use PHPStan\Type\KeyOfType; use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectShapeType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; @@ -39,7 +41,7 @@ class TemplateTypeCheck public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private GenericObjectTypeCheck $genericObjectTypeCheck, private TypeAliasResolver $typeAliasResolver, private bool $checkClassCaseSensitivity, @@ -49,9 +51,10 @@ public function __construct( /** * @param array $templateTags - * @return RuleError[] + * @return list */ public function check( + Scope $scope, Node $node, TemplateTypeScope $templateTypeScope, array $templateTags, @@ -63,25 +66,30 @@ public function check( { $messages = []; foreach ($templateTags as $templateTag) { - $templateTagName = $templateTag->getName(); + $templateTagName = $scope->resolveName(new Node\Name($templateTag->getName())); if ($this->reflectionProvider->hasClass($templateTagName)) { $messages[] = RuleErrorBuilder::message(sprintf( $sameTemplateTypeNameAsClassMessage, $templateTagName, - ))->build(); + ))->identifier('generics.existingClass')->build(); } if ($this->typeAliasResolver->hasTypeAlias($templateTagName, $templateTypeScope->getClassName())) { $messages[] = RuleErrorBuilder::message(sprintf( $sameTemplateTypeNameAsTypeMessage, $templateTagName, - ))->build(); + ))->identifier('generics.existingTypeAlias')->build(); } $boundType = $templateTag->getBound(); foreach ($boundType->getReferencedClasses() as $referencedClass) { - if ( - $this->reflectionProvider->hasClass($referencedClass) - && !$this->reflectionProvider->getClass($referencedClass)->isTrait() - ) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + $messages[] = RuleErrorBuilder::message(sprintf( + $invalidBoundTypeMessage, + $templateTagName, + $referencedClass, + ))->identifier('class.notFound')->build(); + continue; + } + if (!$this->reflectionProvider->getClass($referencedClass)->isTrait()) { continue; } @@ -89,15 +97,12 @@ public function check( $invalidBoundTypeMessage, $templateTagName, $referencedClass, - ))->build(); + ))->identifier('generics.traitBound')->build(); } - if ($this->checkClassCaseSensitivity) { - $classNameNodePairs = array_map(static fn (string $referencedClass): ClassNameNodePair => new ClassNameNodePair($referencedClass, $node), $boundType->getReferencedClasses()); - $messages = array_merge($messages, $this->classCaseSensitivityCheck->checkClassNames($classNameNodePairs)); - } + $classNameNodePairs = array_map(static fn (string $referencedClass): ClassNameNodePair => new ClassNameNodePair($referencedClass, $node), $boundType->getReferencedClasses()); + $messages = array_merge($messages, $this->classCheck->checkClassNames($classNameNodePairs, $this->checkClassCaseSensitivity)); - $boundType = $templateTag->getBound(); $boundTypeClass = get_class($boundType); if ( $boundTypeClass !== MixedType::class @@ -111,13 +116,16 @@ public function check( && $boundTypeClass !== BooleanType::class && $boundTypeClass !== ObjectWithoutClassType::class && $boundTypeClass !== ObjectType::class + && $boundTypeClass !== ObjectShapeType::class && $boundTypeClass !== GenericObjectType::class && $boundTypeClass !== KeyOfType::class && !$boundType instanceof UnionType && !$boundType instanceof IntersectionType && !$boundType instanceof TemplateType ) { - $messages[] = RuleErrorBuilder::message(sprintf($notSupportedBoundMessage, $templateTagName, $boundType->describe(VerbosityLevel::typeOnly())))->build(); + $messages[] = RuleErrorBuilder::message(sprintf($notSupportedBoundMessage, $templateTagName, $boundType->describe(VerbosityLevel::typeOnly()))) + ->identifier('generics.notSupportedBound') + ->build(); } $escapedTemplateTagName = SprintfHelper::escapeFormatString($templateTagName); @@ -127,6 +135,8 @@ public function check( sprintf('PHPDoc tag @template %s bound has type %%s which does not specify all template types of %%s %%s: %%s', $escapedTemplateTagName), sprintf('PHPDoc tag @template %s bound has type %%s which specifies %%d template types, but %%s %%s supports only %%d: %%s', $escapedTemplateTagName), sprintf('Type %%s in generic type %%s in PHPDoc tag @template %s is not subtype of template type %%s of %%s %%s.', $escapedTemplateTagName), + sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag @template %s is in conflict with %%s template type %%s of %%s %%s.', $escapedTemplateTagName), + sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag @template %s is redundant, template type %%s of %%s %%s has the same variance.', $escapedTemplateTagName), ); foreach ($genericObjectErrors as $genericObjectError) { $messages[] = $genericObjectError; diff --git a/src/Rules/Generics/TraitTemplateTypeRule.php b/src/Rules/Generics/TraitTemplateTypeRule.php index 5294ae9eaa..c90b398fcf 100644 --- a/src/Rules/Generics/TraitTemplateTypeRule.php +++ b/src/Rules/Generics/TraitTemplateTypeRule.php @@ -52,6 +52,7 @@ public function processNode(Node $node, Scope $scope): array $escapedTraitName = SprintfHelper::escapeFormatString($traitName); return $this->templateTypeCheck->check( + $scope, $node, TemplateTypeScope::createWithClass($traitName), $resolvedPhpDoc->getTemplateTags(), diff --git a/src/Rules/Generics/UsedTraitsRule.php b/src/Rules/Generics/UsedTraitsRule.php index fa8ec17a47..5ad6141c06 100644 --- a/src/Rules/Generics/UsedTraitsRule.php +++ b/src/Rules/Generics/UsedTraitsRule.php @@ -12,6 +12,7 @@ use PHPStan\Type\Type; use function array_map; use function sprintf; +use function strtolower; use function ucfirst; /** @@ -56,11 +57,11 @@ public function processNode(Node $node, Scope $scope): array $useTags = $resolvedPhpDoc->getUsesTags(); } - $description = sprintf('class %s', SprintfHelper::escapeFormatString($className)); - $typeDescription = 'class'; + $typeDescription = strtolower($scope->getClassReflection()->getClassTypeDescription()); + $description = sprintf('%s %s', $typeDescription, SprintfHelper::escapeFormatString($className)); if ($traitName !== null) { - $description = sprintf('trait %s', SprintfHelper::escapeFormatString($traitName)); $typeDescription = 'trait'; + $description = sprintf('%s %s', $typeDescription, SprintfHelper::escapeFormatString($traitName)); } return $this->genericAncestorsCheck->check( @@ -73,6 +74,7 @@ public function processNode(Node $node, Scope $scope): array 'Generic type %s in PHPDoc tag @use does not specify all template types of %s %s: %s', 'Generic type %s in PHPDoc tag @use specifies %d template types, but %s %s supports only %d: %s', 'Type %s in generic type %s in PHPDoc tag @use is not subtype of template type %s of %s %s.', + 'Call-site variance annotation of %s in generic type %s in PHPDoc tag @use is not allowed.', 'PHPDoc tag @use has invalid type %s.', sprintf('%s uses generic trait %%s but does not specify its types: %%s', ucfirst($description)), sprintf('in used type %%s of %s', $description), diff --git a/src/Rules/Generics/VarianceCheck.php b/src/Rules/Generics/VarianceCheck.php index 34f2e4ffe6..7447c13cce 100644 --- a/src/Rules/Generics/VarianceCheck.php +++ b/src/Rules/Generics/VarianceCheck.php @@ -2,8 +2,8 @@ namespace PHPStan\Rules\Generics; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Rules\RuleError; +use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeVariance; @@ -13,28 +13,30 @@ class VarianceCheck { - /** @return RuleError[] */ + public function __construct( + private bool $checkParamOutVariance, + private bool $strictStaticVariance, + ) + { + } + + /** + * @param 'function'|'method' $identifier + * @return list + */ public function checkParametersAcceptor( - ParametersAcceptor $parametersAcceptor, + ParametersAcceptorWithPhpDocs $parametersAcceptor, string $parameterTypeMessage, + string $parameterOutTypeMessage, string $returnTypeMessage, string $generalMessage, bool $isStatic, + bool $isPrivate, + string $identifier, ): array { $errors = []; - foreach ($parametersAcceptor->getParameters() as $parameterReflection) { - $variance = $isStatic - ? TemplateTypeVariance::createStatic() - : TemplateTypeVariance::createContravariant(); - $type = $parameterReflection->getType(); - $message = sprintf($parameterTypeMessage, $parameterReflection->getName()); - foreach ($this->check($variance, $type, $message) as $error) { - $errors[] = $error; - } - } - foreach ($parametersAcceptor->getTemplateTypeMap()->getTypes() as $templateType) { if (!$templateType instanceof TemplateType || $templateType->getScope()->getFunctionName() === null @@ -47,19 +49,49 @@ public function checkParametersAcceptor( 'Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type %s in %s.', $templateType->getName(), $generalMessage, - ))->build(); + ))->identifier(sprintf('%s.variance', $identifier))->build(); + } + + if ($isPrivate) { + return $errors; + } + + $covariant = TemplateTypeVariance::createCovariant(); + $parameterVariance = $isStatic && !$this->strictStaticVariance + ? TemplateTypeVariance::createStatic() + : TemplateTypeVariance::createContravariant(); + + foreach ($parametersAcceptor->getParameters() as $parameterReflection) { + $type = $parameterReflection->getType(); + $message = sprintf($parameterTypeMessage, $parameterReflection->getName()); + foreach ($this->check($parameterVariance, $type, $message) as $error) { + $errors[] = $error; + } + + if (!$this->checkParamOutVariance) { + continue; + } + + $paramOutType = $parameterReflection->getOutType(); + if ($paramOutType === null) { + continue; + } + + $outMessage = sprintf($parameterOutTypeMessage, $parameterReflection->getName()); + foreach ($this->check($covariant, $paramOutType, $outMessage) as $error) { + $errors[] = $error; + } } - $variance = TemplateTypeVariance::createCovariant(); $type = $parametersAcceptor->getReturnType(); - foreach ($this->check($variance, $type, $returnTypeMessage) as $error) { + foreach ($this->check($covariant, $type, $returnTypeMessage) as $error) { $errors[] = $error; } return $errors; } - /** @return RuleError[] */ + /** @return list */ public function check(TemplateTypeVariance $positionVariance, Type $type, string $messageContext): array { $errors = []; @@ -77,7 +109,7 @@ public function check(TemplateTypeVariance $positionVariance, Type $type, string $referredType->getVariance()->describe(), $reference->getPositionVariance()->describe(), $messageContext, - ))->build(); + ))->identifier('generics.variance')->build(); } return $errors; diff --git a/src/Rules/IdentifierRuleError.php b/src/Rules/IdentifierRuleError.php index fb556fc875..b7e32e019a 100644 --- a/src/Rules/IdentifierRuleError.php +++ b/src/Rules/IdentifierRuleError.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules; +/** @api */ interface IdentifierRuleError extends RuleError { diff --git a/src/Rules/Ignore/IgnoreParseErrorRule.php b/src/Rules/Ignore/IgnoreParseErrorRule.php new file mode 100644 index 0000000000..330ac5a8d1 --- /dev/null +++ b/src/Rules/Ignore/IgnoreParseErrorRule.php @@ -0,0 +1,47 @@ + + */ +class IgnoreParseErrorRule implements Rule +{ + + public function getNodeType(): string + { + return FileNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $nodes = $node->getNodes(); + if (count($nodes) === 0) { + return []; + } + + $firstNode = $nodes[0]; + $parseErrors = $firstNode->getAttribute('linesToIgnoreParseErrors', []); + $errors = []; + foreach ($parseErrors as $line => $lineParseErrors) { + foreach ($lineParseErrors as $parseError) { + $errors[] = RuleErrorBuilder::message(sprintf('Parse error in @phpstan-ignore: %s', $parseError)) + ->line($line) + ->identifier('ignore.parseError') + ->nonIgnorable() + ->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/IssetCheck.php b/src/Rules/IssetCheck.php index bf58da539d..a660be4e61 100644 --- a/src/Rules/IssetCheck.php +++ b/src/Rules/IssetCheck.php @@ -8,11 +8,15 @@ use PHPStan\Rules\Properties\PropertyDescriptor; use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function is_string; use function sprintf; +/** + * @phpstan-type ErrorIdentifier = 'empty'|'isset'|'nullCoalesce' + */ class IssetCheck { @@ -27,9 +31,10 @@ public function __construct( } /** + * @param ErrorIdentifier $identifier * @param callable(Type): ?string $typeMessageCallback */ - public function check(Expr $expr, Scope $scope, string $operatorDescription, callable $typeMessageCallback, ?RuleError $error = null): ?RuleError + public function check(Expr $expr, Scope $scope, string $operatorDescription, string $identifier, callable $typeMessageCallback, ?IdentifierRuleError $error = null): ?IdentifierRuleError { // mirrored in PHPStan\Analyser\MutatingScope::issetCheck() if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { @@ -44,14 +49,21 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal return null; } - return $this->generateError( - $scope->getVariableType($expr->name), - sprintf('Variable $%s %s always exists and', $expr->name, $operatorDescription), - $typeMessageCallback, - ); + $type = $this->treatPhpDocTypesAsCertain ? $scope->getType($expr) : $scope->getNativeType($expr); + if (!$type instanceof NeverType) { + return $this->generateError( + $type, + sprintf('Variable $%s %s always exists and', $expr->name, $operatorDescription), + $typeMessageCallback, + $identifier, + 'variable', + ); + } } - return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription))->build(); + return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription)) + ->identifier(sprintf('%s.variable', $identifier)) + ->build(); } return $error; @@ -59,14 +71,14 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal $type = $this->treatPhpDocTypesAsCertain ? $scope->getType($expr->var) : $scope->getNativeType($expr->var); + if (!$type->isOffsetAccessible()->yes()) { + return $error ?? $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); + } + $dimType = $this->treatPhpDocTypesAsCertain ? $scope->getType($expr->dim) : $scope->getNativeType($expr->dim); $hasOffsetValue = $type->hasOffsetValueType($dimType); - if (!$type->isOffsetAccessible()->yes()) { - return $error ?? $this->checkUndefined($expr->var, $scope, $operatorDescription); - } - if ($hasOffsetValue->no()) { if (!$this->checkAdvancedIsset) { return null; @@ -79,12 +91,12 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal $type->describe(VerbosityLevel::value()), $operatorDescription, ), - )->build(); + )->identifier(sprintf('%s.offset', $identifier))->build(); } - // If offset is cannot be null, store this error message and see if one of the earlier offsets is. + // If offset cannot be null, store this error message and see if one of the earlier offsets is. // E.g. $array['a']['b']['c'] ?? null; is a valid coalesce if a OR b or C might be null. - if ($hasOffsetValue->yes() || $scope->isSpecified($expr)) { + if ($hasOffsetValue->yes() || $scope->hasExpressionType($expr)->yes()) { if (!$this->checkAdvancedIsset) { return null; } @@ -94,10 +106,10 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value()), $operatorDescription, - ), $typeMessageCallback); + ), $typeMessageCallback, $identifier, 'offset'); if ($error !== null) { - return $this->check($expr->var, $scope, $operatorDescription, $typeMessageCallback, $error); + return $this->check($expr->var, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); } } @@ -110,11 +122,11 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal if ($propertyReflection === null) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } if ($expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription); + return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); } return null; @@ -122,11 +134,11 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal if (!$propertyReflection->isNative()) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } if ($expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription); + return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); } return null; @@ -134,31 +146,31 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal $nativeType = $propertyReflection->getNativeType(); if (!$nativeType instanceof MixedType) { - if (!$scope->isSpecified($expr)) { + if (!$scope->hasExpressionType($expr)->yes()) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } if ($expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription); + return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); } return null; } } - $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $expr); + $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $expr); $propertyType = $propertyReflection->getWritableType(); if ($error !== null) { return $error; } if (!$this->checkAdvancedIsset) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } if ($expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription); + return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); } return null; @@ -168,15 +180,17 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal $propertyReflection->getWritableType(), sprintf('%s (%s) %s', $propertyDescription, $propertyType->describe(VerbosityLevel::typeOnly()), $operatorDescription), $typeMessageCallback, + $identifier, + 'property', ); if ($error !== null) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->check($expr->var, $scope, $operatorDescription, $typeMessageCallback, $error); + return $this->check($expr->var, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); } if ($expr->class instanceof Expr) { - return $this->check($expr->class, $scope, $operatorDescription, $typeMessageCallback, $error); + return $this->check($expr->class, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); } } @@ -191,7 +205,13 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal return null; } - $error = $this->generateError($scope->getType($expr), sprintf('Expression %s', $operatorDescription), $typeMessageCallback); + $error = $this->generateError( + $this->treatPhpDocTypesAsCertain ? $scope->getType($expr) : $scope->getNativeType($expr), + sprintf('Expression %s', $operatorDescription), + $typeMessageCallback, + $identifier, + 'expr', + ); if ($error !== null) { return $error; } @@ -202,16 +222,23 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal } if ($expr->name instanceof Node\Identifier) { - return RuleErrorBuilder::message(sprintf('Using nullsafe property access "?->%s" %s is unnecessary. Use -> instead.', $expr->name->name, $operatorDescription))->build(); + return RuleErrorBuilder::message(sprintf('Using nullsafe property access "?->%s" %s is unnecessary. Use -> instead.', $expr->name->name, $operatorDescription)) + ->identifier('nullsafe.neverNull') + ->build(); } - return RuleErrorBuilder::message(sprintf('Using nullsafe property access "?->(Expression)" %s is unnecessary. Use -> instead.', $operatorDescription))->build(); + return RuleErrorBuilder::message(sprintf('Using nullsafe property access "?->(Expression)" %s is unnecessary. Use -> instead.', $operatorDescription)) + ->identifier('nullsafe.neverNull') + ->build(); } return null; } - private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescription): ?RuleError + /** + * @param ErrorIdentifier $identifier + */ + private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescription, string $identifier): ?IdentifierRuleError { if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { $hasVariable = $scope->hasVariableType($expr->name); @@ -219,19 +246,21 @@ private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescri return null; } - return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription))->build(); + return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription)) + ->identifier(sprintf('%s.variable', $identifier)) + ->build(); } if ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { - $type = $scope->getType($expr->var); - $dimType = $scope->getType($expr->dim); + $type = $this->treatPhpDocTypesAsCertain ? $scope->getType($expr->var) : $scope->getNativeType($expr->var); + $dimType = $this->treatPhpDocTypesAsCertain ? $scope->getType($expr->dim) : $scope->getNativeType($expr->dim); $hasOffsetValue = $type->hasOffsetValueType($dimType); if (!$type->isOffsetAccessible()->yes()) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } if (!$hasOffsetValue->no()) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } return RuleErrorBuilder::message( @@ -241,15 +270,15 @@ private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescri $type->describe(VerbosityLevel::value()), $operatorDescription, ), - )->build(); + )->identifier(sprintf('%s.offset', $identifier))->build(); } if ($expr instanceof Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription); + return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); } if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription); + return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); } return null; @@ -257,8 +286,10 @@ private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescri /** * @param callable(Type): ?string $typeMessageCallback + * @param ErrorIdentifier $identifier + * @param 'variable'|'offset'|'property'|'expr' $identifierSecondPart */ - private function generateError(Type $type, string $message, callable $typeMessageCallback): ?RuleError + private function generateError(Type $type, string $message, callable $typeMessageCallback, string $identifier, string $identifierSecondPart): ?IdentifierRuleError { $typeMessage = $typeMessageCallback($type); if ($typeMessage === null) { @@ -267,7 +298,7 @@ private function generateError(Type $type, string $message, callable $typeMessag return RuleErrorBuilder::message( sprintf('%s %s.', $message, $typeMessage), - )->build(); + )->identifier(sprintf('%s.%s', $identifier, $identifierSecondPart))->build(); } } diff --git a/src/Rules/Keywords/ContinueBreakInLoopRule.php b/src/Rules/Keywords/ContinueBreakInLoopRule.php index 604f98f7cb..d97e3fd119 100644 --- a/src/Rules/Keywords/ContinueBreakInLoopRule.php +++ b/src/Rules/Keywords/ContinueBreakInLoopRule.php @@ -48,7 +48,10 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Keyword %s used outside of a loop or a switch statement.', $node instanceof Stmt\Continue_ ? 'continue' : 'break', - ))->nonIgnorable()->build(), + )) + ->nonIgnorable() + ->identifier(sprintf('%s.outOfLoop', $node instanceof Stmt\Continue_ ? 'continue' : 'break')) + ->build(), ]; } if ( @@ -70,7 +73,10 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Keyword %s used outside of a loop or a switch statement.', $node instanceof Stmt\Continue_ ? 'continue' : 'break', - ))->nonIgnorable()->build(), + )) + ->nonIgnorable() + ->identifier(sprintf('%s.outOfLoop', $node instanceof Stmt\Continue_ ? 'continue' : 'break')) + ->build(), ]; } diff --git a/src/Rules/Keywords/DeclareStrictTypesRule.php b/src/Rules/Keywords/DeclareStrictTypesRule.php new file mode 100644 index 0000000000..3878c1c77c --- /dev/null +++ b/src/Rules/Keywords/DeclareStrictTypesRule.php @@ -0,0 +1,80 @@ + + */ +class DeclareStrictTypesRule implements Rule +{ + + public function __construct( + private readonly ExprPrinter $exprPrinter, + ) + { + } + + public function getNodeType(): string + { + return Stmt\Declare_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $declaresStrictTypes = false; + foreach ($node->declares as $declare) { + if ( + $declare->key->name !== 'strict_types' + ) { + continue; + } + + if ( + !$declare->value instanceof Node\Scalar\LNumber + || !in_array($declare->value->value, [0, 1], true) + ) { + return [ + RuleErrorBuilder::message(sprintf( + sprintf( + 'Declare strict_types must have 0 or 1 as its value, %s given.', + $this->exprPrinter->printExpr($declare->value), + ), + ))->identifier('declareStrictTypes.value')->nonIgnorable()->build(), + ]; + } + + $declaresStrictTypes = true; + break; + } + + if ($declaresStrictTypes === false) { + return []; + } + + if (!$node->hasAttribute(DeclarePositionVisitor::ATTRIBUTE_NAME)) { + return []; + } + + $isFirstStatement = (bool) $node->getAttribute(DeclarePositionVisitor::ATTRIBUTE_NAME); + if ($isFirstStatement) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Declare strict_types must be the very first statement.', + ))->identifier('declareStrictTypes.notFirst')->nonIgnorable()->build(), + ]; + } + +} diff --git a/src/Rules/LineRuleError.php b/src/Rules/LineRuleError.php index 0388b7fca7..986840eff2 100644 --- a/src/Rules/LineRuleError.php +++ b/src/Rules/LineRuleError.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules; +/** @api */ interface LineRuleError extends RuleError { diff --git a/src/Rules/MetadataRuleError.php b/src/Rules/MetadataRuleError.php index 01b6d15515..5123d37c80 100644 --- a/src/Rules/MetadataRuleError.php +++ b/src/Rules/MetadataRuleError.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules; +/** @api */ interface MetadataRuleError extends RuleError { diff --git a/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php b/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php index da1a3950b7..d6d7b603c8 100644 --- a/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php +++ b/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php @@ -27,17 +27,36 @@ public function processNode(Node $node, Scope $scope): array } $class = $scope->getClassReflection(); - if ($class->isAbstract()) { - return []; + + if (!$class->isAbstract() && $node->isAbstract()) { + $description = $class->getClassTypeDescription(); + return [ + RuleErrorBuilder::message(sprintf( + '%s %s contains abstract method %s().', + $description === 'Class' ? 'Non-abstract class' : $description, + $class->getDisplayName(), + $node->name->toString(), + )) + ->nonIgnorable() + ->identifier('method.abstract') + ->build(), + ]; } - if (!$node->isAbstract()) { - return []; + if (!$class->isAbstract() && !$class->isInterface() && $node->getStmts() === null) { + return [ + RuleErrorBuilder::message(sprintf( + 'Non-abstract method %s::%s() must contain a body.', + $class->getDisplayName(), + $node->name->toString(), + )) + ->nonIgnorable() + ->identifier('method.nonAbstract') + ->build(), + ]; } - return [ - RuleErrorBuilder::message(sprintf('Non-abstract class %s contains abstract method %s().', $class->getDisplayName(), $node->name->toString()))->nonIgnorable()->build(), - ]; + return []; } } diff --git a/src/Rules/Methods/AbstractPrivateMethodRule.php b/src/Rules/Methods/AbstractPrivateMethodRule.php new file mode 100644 index 0000000000..060bb2a27a --- /dev/null +++ b/src/Rules/Methods/AbstractPrivateMethodRule.php @@ -0,0 +1,58 @@ + */ +class AbstractPrivateMethodRule implements Rule +{ + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $method = $node->getMethodReflection(); + + if (!$method->isPrivate()) { + return []; + } + + if (!$method->isAbstract()->yes()) { + return []; + } + + if ($scope->isInTrait()) { + return []; + } + + $classReflection = $scope->getClassReflection(); + if ($classReflection === null) { + return []; + } + + if (!$classReflection->isAbstract() && !$classReflection->isInterface()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Private method %s::%s() cannot be abstract.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + )) + ->identifier('method.abstractPrivate') + ->nonIgnorable() + ->build(), + ]; + } + +} diff --git a/src/Rules/Methods/AlwaysUsedMethodExtension.php b/src/Rules/Methods/AlwaysUsedMethodExtension.php new file mode 100644 index 0000000000..cfccf5b972 --- /dev/null +++ b/src/Rules/Methods/AlwaysUsedMethodExtension.php @@ -0,0 +1,27 @@ +getArgs(), $methodReflection->getVariants(), + $methodReflection->getNamedArgumentsVariants(), ), $scope, $declaringClass->isBuiltin(), @@ -70,6 +71,7 @@ public function processNode(Node $node, Scope $scope): array 'Return type of call to method ' . $messagesMethodName . ' contains unresolvable type.', 'Parameter %s of method ' . $messagesMethodName . ' contains unresolvable type.', ], + 'method', )); } diff --git a/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php b/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php index 8fd13a19e5..0a37e48657 100644 --- a/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php +++ b/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php @@ -55,7 +55,7 @@ public function processNode(Node $node, Scope $scope): array 'Unsafe call to private method %s::%s() through static::.', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - ))->build(), + ))->identifier('staticClassAccess.privateMethod')->build(), ]; } diff --git a/src/Rules/Methods/CallStaticMethodsRule.php b/src/Rules/Methods/CallStaticMethodsRule.php index 7534017147..b1f2f013c2 100644 --- a/src/Rules/Methods/CallStaticMethodsRule.php +++ b/src/Rules/Methods/CallStaticMethodsRule.php @@ -58,6 +58,7 @@ public function processNode(Node $node, Scope $scope): array $scope, $node->getArgs(), $method->getVariants(), + $method->getNamedArgumentsVariants(), ), $scope, $method->getDeclaringClass()->isBuiltin(), @@ -78,6 +79,7 @@ public function processNode(Node $node, Scope $scope): array 'Return type of call to ' . $lowercasedMethodName . ' contains unresolvable type.', 'Parameter %s of ' . $lowercasedMethodName . ' contains unresolvable type.', ], + 'staticMethod', )); return $errors; diff --git a/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php b/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php index 713f3996af..da361f018a 100644 --- a/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php +++ b/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php @@ -8,7 +8,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\NeverType; -use PHPStan\Type\VoidType; use function sprintf; /** @@ -17,7 +16,10 @@ class CallToConstructorStatementWithoutSideEffectsRule implements Rule { - public function __construct(private ReflectionProvider $reflectionProvider) + public function __construct( + private ReflectionProvider $reflectionProvider, + private bool $reportNoConstructor, + ) { } @@ -44,13 +46,22 @@ public function processNode(Node $node, Scope $scope): array $classReflection = $this->reflectionProvider->getClass($className); if (!$classReflection->hasConstructor()) { + if ($this->reportNoConstructor) { + return [ + RuleErrorBuilder::message(sprintf( + 'Call to new %s() on a separate line has no effect.', + $classReflection->getDisplayName(), + ))->identifier('new.resultUnused')->build(), + ]; + } + return []; } $constructor = $classReflection->getConstructor(); if ($constructor->hasSideEffects()->no()) { $throwsType = $constructor->getThrowType(); - if ($throwsType !== null && !$throwsType instanceof VoidType) { + if ($throwsType !== null && !$throwsType->isVoid()->yes()) { return []; } @@ -64,7 +75,7 @@ public function processNode(Node $node, Scope $scope): array 'Call to %s::%s() on a separate line has no effect.', $classReflection->getDisplayName(), $constructor->getName(), - ))->build(), + ))->identifier('new.resultUnused')->build(), ]; } diff --git a/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php b/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php index 93b9202984..abeaff4110 100644 --- a/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php +++ b/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php @@ -11,7 +11,6 @@ use PHPStan\Type\ErrorType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; -use PHPStan\Type\VoidType; use function sprintf; /** @@ -65,7 +64,7 @@ public function processNode(Node $node, Scope $scope): array if ($method->hasSideEffects()->no() || $node->expr->isFirstClassCallable()) { if (!$node->expr->isFirstClassCallable()) { $throwsType = $method->getThrowType(); - if ($throwsType !== null && !$throwsType instanceof VoidType) { + if ($throwsType !== null && !$throwsType->isVoid()->yes()) { return []; } } @@ -81,7 +80,7 @@ public function processNode(Node $node, Scope $scope): array $method->isStatic() ? 'static method' : 'method', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - ))->build(), + ))->identifier('method.resultUnused')->build(), ]; } diff --git a/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php b/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php index 593846a1d2..9db2493f65 100644 --- a/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php +++ b/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php @@ -13,7 +13,6 @@ use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -use PHPStan\Type\VoidType; use function sprintf; use function strtolower; @@ -88,7 +87,7 @@ public function processNode(Node $node, Scope $scope): array if ($method->hasSideEffects()->no() || $node->expr->isFirstClassCallable()) { if (!$node->expr->isFirstClassCallable()) { $throwsType = $method->getThrowType(); - if ($throwsType !== null && !$throwsType instanceof VoidType) { + if ($throwsType !== null && !$throwsType->isVoid()->yes()) { return []; } } @@ -104,7 +103,7 @@ public function processNode(Node $node, Scope $scope): array $method->isStatic() ? 'static method' : 'method', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - ))->build(), + ))->identifier('staticMethod.resultUnused')->build(), ]; } diff --git a/src/Rules/Methods/ConsistentConstructorRule.php b/src/Rules/Methods/ConsistentConstructorRule.php index cb440dba68..e32d6fa551 100644 --- a/src/Rules/Methods/ConsistentConstructorRule.php +++ b/src/Rules/Methods/ConsistentConstructorRule.php @@ -5,13 +5,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; -use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Dummy\DummyConstructorReflection; -use PHPStan\Reflection\MethodPrototypeReflection; -use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; -use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Rules\Rule; -use PHPStan\ShouldNotHappenException; use function strtolower; /** @implements Rule */ @@ -31,12 +26,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - - if (! $method instanceof PhpMethodFromParserNodeReflection) { - throw new ShouldNotHappenException(); - } - + $method = $node->getMethodReflection(); if (strtolower($method->getName()) !== '__construct') { return []; } @@ -50,54 +40,14 @@ public function processNode(Node $node, Scope $scope): array if ($parent->hasConstructor()) { $parentConstructor = $parent->getConstructor(); } else { - $parentConstructor = $this->getEmptyConstructor($parent); - } - - if (! $parentConstructor instanceof PhpMethodReflection && ! $parentConstructor instanceof MethodPrototypeReflection) { - return []; + $parentConstructor = new DummyConstructorReflection($parent); } if (! $parentConstructor->getDeclaringClass()->hasConsistentConstructor()) { return []; } - if (! $parentConstructor instanceof MethodPrototypeReflection) { - $parentConstructor = $this->getMethodPrototypeReflection($parentConstructor, $parent); - } - - return $this->methodParameterComparisonHelper->compare($parentConstructor, $method, true); - } - - private function getMethodPrototypeReflection(PhpMethodReflection $methodReflection, ClassReflection $classReflection): MethodPrototypeReflection - { - return new MethodPrototypeReflection( - $methodReflection->getName(), - $classReflection, - $methodReflection->isStatic(), - $methodReflection->isPrivate(), - $methodReflection->isPublic(), - $methodReflection->isAbstract(), - $methodReflection->isFinal()->yes(), - $classReflection->getNativeMethod($methodReflection->getName())->getVariants(), - null, - ); - } - - private function getEmptyConstructor(ClassReflection $classReflection): MethodPrototypeReflection - { - $emptyConstructor = new DummyConstructorReflection($classReflection); - - return new MethodPrototypeReflection( - $emptyConstructor->getName(), - $classReflection, - $emptyConstructor->isStatic(), - $emptyConstructor->isPrivate(), - $emptyConstructor->isPublic(), - false, - $emptyConstructor->isFinal()->yes(), - $emptyConstructor->getVariants(), - null, - ); + return $this->methodParameterComparisonHelper->compare($parentConstructor, $parentConstructor->getDeclaringClass(), $method, true); } } diff --git a/src/Rules/Methods/ConstructorReturnTypeRule.php b/src/Rules/Methods/ConstructorReturnTypeRule.php new file mode 100644 index 0000000000..c93634b94e --- /dev/null +++ b/src/Rules/Methods/ConstructorReturnTypeRule.php @@ -0,0 +1,63 @@ + + */ +class ConstructorReturnTypeRule implements Rule +{ + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + $methodNode = $node->getOriginalNode(); + if ($scope->isInTrait()) { + $originalMethodName = $methodNode->getAttribute('originalTraitMethodName'); + if ( + $originalMethodName === '__construct' + && $methodNode->returnType !== null + ) { + return [ + RuleErrorBuilder::message(sprintf('Original constructor of trait %s has a return type.', $scope->getTraitReflection()->getDisplayName())) + ->identifier('constructor.returnType') + ->nonIgnorable() + ->build(), + ]; + } + } + if (!$classReflection->hasConstructor()) { + return []; + } + + $constructorReflection = $classReflection->getConstructor(); + $methodReflection = $node->getMethodReflection(); + if ($methodReflection->getName() !== $constructorReflection->getName()) { + return []; + } + + if ($methodNode->returnType === null) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf('Constructor of class %s has a return type.', $classReflection->getDisplayName())) + ->identifier('constructor.returnType') + ->nonIgnorable() + ->build(), + ]; + } + +} diff --git a/src/Rules/Methods/DirectAlwaysUsedMethodExtensionProvider.php b/src/Rules/Methods/DirectAlwaysUsedMethodExtensionProvider.php new file mode 100644 index 0000000000..b51cf7763a --- /dev/null +++ b/src/Rules/Methods/DirectAlwaysUsedMethodExtensionProvider.php @@ -0,0 +1,20 @@ +extensions; + } + +} diff --git a/src/Rules/Methods/ExistingClassesInTypehintsRule.php b/src/Rules/Methods/ExistingClassesInTypehintsRule.php index 0f3ee88ed5..524f509395 100644 --- a/src/Rules/Methods/ExistingClassesInTypehintsRule.php +++ b/src/Rules/Methods/ExistingClassesInTypehintsRule.php @@ -6,10 +6,8 @@ use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassMethodNode; -use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Rules\FunctionDefinitionCheck; use PHPStan\Rules\Rule; -use PHPStan\ShouldNotHappenException; use function sprintf; /** @@ -29,15 +27,8 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $methodReflection = $scope->getFunction(); - if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) { - throw new ShouldNotHappenException(); - } - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - - $className = SprintfHelper::escapeFormatString($scope->getClassReflection()->getDisplayName()); + $methodReflection = $node->getMethodReflection(); + $className = SprintfHelper::escapeFormatString($node->getClassReflection()->getDisplayName()); $methodName = SprintfHelper::escapeFormatString($methodReflection->getName()); return $this->check->checkClassMethod( diff --git a/src/Rules/Methods/FinalPrivateMethodRule.php b/src/Rules/Methods/FinalPrivateMethodRule.php index 51cfc6ab8f..b0934bb0c4 100644 --- a/src/Rules/Methods/FinalPrivateMethodRule.php +++ b/src/Rules/Methods/FinalPrivateMethodRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; use PHPStan\Php\PhpVersion; -use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; @@ -28,11 +27,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - if (!$method instanceof PhpMethodFromParserNodeReflection) { - return []; - } - + $method = $node->getMethodReflection(); if (!$this->phpVersion->producesWarningForFinalPrivateMethods()) { return []; } @@ -50,7 +45,7 @@ public function processNode(Node $node, Scope $scope): array 'Private method %s::%s() cannot be final as it is never overridden by other classes.', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - ))->build(), + ))->identifier('method.finalPrivate')->build(), ]; } diff --git a/src/Rules/Methods/IllegalConstructorMethodCallRule.php b/src/Rules/Methods/IllegalConstructorMethodCallRule.php index 94eb726ab3..3305529134 100644 --- a/src/Rules/Methods/IllegalConstructorMethodCallRule.php +++ b/src/Rules/Methods/IllegalConstructorMethodCallRule.php @@ -26,6 +26,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message('Call to __construct() on an existing object is not allowed.') + ->identifier('constructor.call') ->build(), ]; } diff --git a/src/Rules/Methods/IllegalConstructorStaticCallRule.php b/src/Rules/Methods/IllegalConstructorStaticCallRule.php index ac3e0fa52a..6e839f54ca 100644 --- a/src/Rules/Methods/IllegalConstructorStaticCallRule.php +++ b/src/Rules/Methods/IllegalConstructorStaticCallRule.php @@ -6,8 +6,10 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function array_key_exists; use function array_map; use function in_array; +use function sprintf; use function strtolower; /** @@ -33,20 +35,24 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message('Static call to __construct() is only allowed on a parent class in the constructor.') + ->identifier('constructor.call') ->build(), ]; } - private function isCollectCallingConstructor(Node $node, Scope $scope): bool + private function isCollectCallingConstructor(Node\Expr\StaticCall $node, Scope $scope): bool { - if (!$node instanceof Node\Expr\StaticCall) { - return true; - } // __construct should be called from inside constructor - if ($scope->getFunction() !== null && $scope->getFunction()->getName() !== '__construct') { + if ($scope->getFunction() === null) { return false; } + if ($scope->getFunction()->getName() !== '__construct') { + if (!$this->isInRenamedTraitConstructor($scope)) { + return false; + } + } + if (!$scope->isInClass()) { return false; } @@ -60,4 +66,27 @@ private function isCollectCallingConstructor(Node $node, Scope $scope): bool return in_array(strtolower($scope->resolveName($node->class)), $parentClasses, true); } + private function isInRenamedTraitConstructor(Scope $scope): bool + { + if (!$scope->isInClass()) { + return false; + } + + if (!$scope->isInTrait()) { + return false; + } + + if ($scope->getFunction() === null) { + return false; + } + + $traitAliases = $scope->getClassReflection()->getNativeReflection()->getTraitAliases(); + $functionName = $scope->getFunction()->getName(); + if (!array_key_exists($functionName, $traitAliases)) { + return false; + } + + return $traitAliases[$functionName] === sprintf('%s::%s', $scope->getTraitReflection()->getName(), '__construct'); + } + } diff --git a/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php b/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php index 19aa3e2947..92aa955a0e 100644 --- a/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php +++ b/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; @@ -28,11 +27,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - if (!$method instanceof PhpMethodFromParserNodeReflection) { - return []; - } - + $method = $node->getMethodReflection(); $parameters = ParametersAcceptorSelector::selectSingle($method->getVariants()); $errors = []; @@ -48,10 +43,12 @@ public function processNode(Node $node, Scope $scope): array } $defaultValueType = $scope->getType($param->default); - $parameterType = $parameters->getParameters()[$paramI]->getType(); + $parameter = $parameters->getParameters()[$paramI]; + $parameterType = $parameter->getType(); $parameterType = TemplateTypeHelper::resolveToBounds($parameterType); - if ($parameterType->accepts($defaultValueType, true)->yes()) { + $accepts = $parameterType->acceptsWithReason($defaultValueType, true); + if ($accepts->yes()) { continue; } @@ -65,7 +62,11 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $parameterType->describe($verbosityLevel), - ))->line($param->getLine())->build(); + )) + ->line($param->getStartLine()) + ->identifier('parameter.defaultValue') + ->acceptsReasonsTip($accepts->reasons) + ->build(); } return $errors; diff --git a/src/Rules/Methods/LazyAlwaysUsedMethodExtensionProvider.php b/src/Rules/Methods/LazyAlwaysUsedMethodExtensionProvider.php new file mode 100644 index 0000000000..cbd397ee94 --- /dev/null +++ b/src/Rules/Methods/LazyAlwaysUsedMethodExtensionProvider.php @@ -0,0 +1,22 @@ +extensions ??= $this->container->getServicesByTag(static::EXTENSION_TAG); + } + +} diff --git a/src/Rules/Methods/MethodCallCheck.php b/src/Rules/Methods/MethodCallCheck.php index bd46e50708..57d0c165e6 100644 --- a/src/Rules/Methods/MethodCallCheck.php +++ b/src/Rules/Methods/MethodCallCheck.php @@ -6,12 +6,13 @@ use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; +use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function count; @@ -31,7 +32,7 @@ public function __construct( } /** - * @return array{RuleError[], MethodReflection|null} + * @return array{list, ExtendedMethodReflection|null} */ public function check( Scope $scope, @@ -50,14 +51,19 @@ public function check( if ($type instanceof ErrorType) { return [$typeResult->getUnknownClassErrors(), null]; } - if (!$type->canCallMethods()->yes()) { + + $typeForDescribe = $type; + if ($type instanceof StaticType) { + $typeForDescribe = $type->getStaticObjectType(); + } + if (!$type->canCallMethods()->yes() || $type->isClassStringType()->yes()) { return [ [ RuleErrorBuilder::message(sprintf( 'Cannot call method %s() on %s.', $methodName, - $type->describe(VerbosityLevel::typeOnly()), - ))->build(), + $typeForDescribe->describe(VerbosityLevel::typeOnly()), + ))->identifier('method.nonObject')->build(), ], null, ]; @@ -91,7 +97,7 @@ public function check( 'Call to private method %s() of parent class %s.', $methodReflection->getName(), $parentClassReflection->getDisplayName(), - ))->build(), + ))->identifier('method.private')->build(), ], $methodReflection, ]; @@ -105,9 +111,9 @@ public function check( [ RuleErrorBuilder::message(sprintf( 'Call to an undefined method %s::%s().', - $type->describe(VerbosityLevel::typeOnly()), + $typeForDescribe->describe(VerbosityLevel::typeOnly()), $methodName, - ))->build(), + ))->identifier('method.notFound')->build(), ], null, ]; @@ -123,7 +129,9 @@ public function check( $methodReflection->isPrivate() ? 'private' : 'protected', $methodReflection->getName(), $declaringClass->getDisplayName(), - ))->build(); + )) + ->identifier(sprintf('method.%s', $methodReflection->isPrivate() ? 'private' : 'protected')) + ->build(); } if ( @@ -133,7 +141,7 @@ public function check( ) { $errors[] = RuleErrorBuilder::message( sprintf('Call to method %s with incorrect case: %s', $messagesMethodName, $methodName), - )->build(); + )->identifier('method.nameCase')->build(); } return [$errors, $methodReflection]; diff --git a/src/Rules/Methods/MethodCallableRule.php b/src/Rules/Methods/MethodCallableRule.php index 8cd5c3f14b..15027c8c2c 100644 --- a/src/Rules/Methods/MethodCallableRule.php +++ b/src/Rules/Methods/MethodCallableRule.php @@ -32,6 +32,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message('First-class callables are supported only on PHP 8.1 and later.') ->nonIgnorable() + ->identifier('callable.notSupported') ->build(), ]; } @@ -55,7 +56,9 @@ public function processNode(Node $node, Scope $scope): array $messagesMethodName = SprintfHelper::escapeFormatString($declaringClass->getDisplayName() . '::' . $methodReflection->getName() . '()'); - $errors[] = RuleErrorBuilder::message(sprintf('Creating callable from a non-native method %s.', $messagesMethodName))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Creating callable from a non-native method %s.', $messagesMethodName)) + ->identifier('callable.nonNativeMethod') + ->build(); return $errors; } diff --git a/src/Rules/Methods/MethodParameterComparisonHelper.php b/src/Rules/Methods/MethodParameterComparisonHelper.php index 9fb408ca02..357d3cf9b3 100644 --- a/src/Rules/Methods/MethodParameterComparisonHelper.php +++ b/src/Rules/Methods/MethodParameterComparisonHelper.php @@ -3,16 +3,15 @@ namespace PHPStan\Rules\Methods; use PHPStan\Php\PhpVersion; -use PHPStan\Reflection\MethodPrototypeReflection; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ArrayType; use PHPStan\Type\IterableType; use PHPStan\Type\MixedType; -use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; @@ -25,16 +24,16 @@ class MethodParameterComparisonHelper { - public function __construct(private PhpVersion $phpVersion) + public function __construct(private PhpVersion $phpVersion, private bool $genericPrototypeMessage) { } /** - * @return RuleError[] + * @return list */ - public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParserNodeReflection $method, bool $ignorable = false): array + public function compare(ExtendedMethodReflection $prototype, ClassReflection $prototypeDeclaringClass, PhpMethodFromParserNodeReflection $method, bool $ignorable = false): array { - /** @var RuleError[] $messages */ + /** @var list $messages */ $messages = []; $prototypeVariant = $prototype->getVariants()[0]; @@ -48,11 +47,11 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse 'Method %s::%s() overrides method %s::%s() but misses parameter #%d $%s.', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), $i + 1, $prototypeParameter->getName(), - )); + ))->identifier('parameter.missing'); if (! $ignorable) { $error->nonIgnorable(); @@ -74,9 +73,9 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $method->getName(), $i + 1, $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), - )); + ))->identifier('parameter.byRef'); if (! $ignorable) { $error->nonIgnorable(); @@ -93,9 +92,9 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $method->getName(), $i + 1, $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), - )); + ))->identifier('parameter.notByRef'); if (! $ignorable) { $error->nonIgnorable(); @@ -115,7 +114,7 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $methodParameter->getName(), $method->getDeclaringClass()->getDisplayName(), $method->getName(), - )); + ))->identifier('parameter.notOptional'); if (! $ignorable) { $error->nonIgnorable(); @@ -134,9 +133,9 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $method->getName(), $i + 1, $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), - )); + ))->identifier('parameter.notVariadic'); if (! $ignorable) { $error->nonIgnorable(); @@ -152,7 +151,7 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $methodParameter->getName(), $method->getDeclaringClass()->getDisplayName(), $method->getName(), - )); + ))->identifier('parameter.notVariadic'); if (! $ignorable) { $error->nonIgnorable(); @@ -165,9 +164,6 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse if ($this->phpVersion->supportsLessOverridenParametersWithVariadic()) { $remainingPrototypeParameters = array_slice($prototypeVariant->getParameters(), $i); foreach ($remainingPrototypeParameters as $j => $remainingPrototypeParameter) { - if (!$remainingPrototypeParameter instanceof ParameterReflectionWithPhpDocs) { - continue; - } if ($methodParameter->getNativeType()->isSuperTypeOf($remainingPrototypeParameter->getNativeType())->yes()) { continue; } @@ -182,9 +178,9 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $i + $j + 1, $remainingPrototypeParameter->getName(), $remainingPrototypeParameter->getNativeType()->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), - )); + ))->identifier('method.childParameterType'); if (! $ignorable) { $error->nonIgnorable(); @@ -202,9 +198,9 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $method->getName(), $i + 1, $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), - )); + ))->identifier('parameter.variadic'); if (! $ignorable) { $error->nonIgnorable(); @@ -224,9 +220,9 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $method->getName(), $i + 1, $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), - )); + ))->identifier('parameter.notOptional'); if (! $ignorable) { $error->nonIgnorable(); @@ -237,10 +233,6 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $methodParameterType = $methodParameter->getNativeType(); - if (!$prototypeParameter instanceof ParameterReflectionWithPhpDocs) { - continue; - } - $prototypeParameterType = $prototypeParameter->getNativeType(); if (!$this->phpVersion->supportsParameterTypeWidening()) { if (!$methodParameterType->equals($prototypeParameterType)) { @@ -254,9 +246,9 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $i + 1, $prototypeParameter->getName(), $prototypeParameterType->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), - )); + ))->identifier('method.childParameterType'); if (! $ignorable) { $error->nonIgnorable(); @@ -282,9 +274,9 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $i + 1, $prototypeParameter->getName(), $prototypeParameterType->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), - )); + ))->identifier('method.childParameterType'); if (! $ignorable) { $error->nonIgnorable(); @@ -302,9 +294,9 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $i + 1, $prototypeParameter->getName(), $prototypeParameterType->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), - )); + ))->identifier('method.childParameterType'); if (! $ignorable) { $error->nonIgnorable(); @@ -334,7 +326,7 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $methodParameter->getName(), $method->getDeclaringClass()->getDisplayName(), $method->getName(), - )); + ))->identifier('parameter.notVariadic'); if (! $ignorable) { $error->nonIgnorable(); @@ -355,7 +347,7 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $methodParameter->getName(), $method->getDeclaringClass()->getDisplayName(), $method->getName(), - )); + ))->identifier('parameter.notOptional'); if (! $ignorable) { $error->nonIgnorable(); @@ -402,7 +394,7 @@ private function isTypeCompatible(Type $methodParameterType, Type $prototypePara if ($prototypeParameterType instanceof ArrayType) { return true; } - if ($prototypeParameterType instanceof ObjectType && $prototypeParameterType->getClassName() === Traversable::class) { + if ($prototypeParameterType->isObject()->yes() && $prototypeParameterType->getObjectClassNames() === [Traversable::class]) { return true; } } diff --git a/src/Rules/Methods/MethodSignatureRule.php b/src/Rules/Methods/MethodSignatureRule.php index 3065030aba..f7a1a8120b 100644 --- a/src/Rules/Methods/MethodSignatureRule.php +++ b/src/Rules/Methods/MethodSignatureRule.php @@ -10,11 +10,18 @@ use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; -use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; +use PHPStan\Reflection\Php\NativeBuiltinMethodReflection; +use PHPStan\Reflection\Php\PhpClassReflectionExtension; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\ArrayType; use PHPStan\Type\Generic\TemplateTypeHelper; +use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\StaticType; @@ -22,10 +29,10 @@ use PHPStan\Type\TypehintHelper; use PHPStan\Type\TypeTraverser; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; use function count; use function min; use function sprintf; +use function strtolower; /** * @implements Rule @@ -34,8 +41,10 @@ class MethodSignatureRule implements Rule { public function __construct( + private PhpClassReflectionExtension $phpClassReflectionExtension, private bool $reportMaybes, private bool $reportStatic, + private bool $abstractTraitMethod, ) { } @@ -47,11 +56,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - if (!$method instanceof PhpMethodFromParserNodeReflection) { - return []; - } - + $method = $node->getMethodReflection(); $methodName = $method->getName(); if ($methodName === '__construct') { return []; @@ -66,28 +71,49 @@ public function processNode(Node $node, Scope $scope): array $errors = []; $declaringClass = $method->getDeclaringClass(); - foreach ($this->collectParentMethods($methodName, $method->getDeclaringClass()) as $parentMethod) { + foreach ($this->collectParentMethods($methodName, $method->getDeclaringClass()) as [$parentMethod, $parentMethodDeclaringClass]) { $parentVariants = $parentMethod->getVariants(); if (count($parentVariants) !== 1) { continue; } $parentParameters = ParametersAcceptorSelector::selectSingle($parentVariants); - if (!$parentParameters instanceof ParametersAcceptorWithPhpDocs) { - continue; - } - [$returnTypeCompatibility, $returnType, $parentReturnType] = $this->checkReturnTypeCompatibility($declaringClass, $parameters, $parentParameters); if ($returnTypeCompatibility->no() || (!$returnTypeCompatibility->yes() && $this->reportMaybes)) { - $errors[] = RuleErrorBuilder::message(sprintf( + $builder = RuleErrorBuilder::message(sprintf( 'Return type (%s) of method %s::%s() should be %s with return type (%s) of method %s::%s()', $returnType->describe(VerbosityLevel::value()), $method->getDeclaringClass()->getDisplayName(), $method->getName(), $returnTypeCompatibility->no() ? 'compatible' : 'covariant', $parentReturnType->describe(VerbosityLevel::value()), - $parentMethod->getDeclaringClass()->getDisplayName(), + $parentMethodDeclaringClass->getDisplayName(), $parentMethod->getName(), - ))->build(); + ))->identifier('method.childReturnType'); + if ( + $parentMethod->getDeclaringClass()->getName() === Rule::class + && strtolower($methodName) === 'processnode' + ) { + $ruleErrorType = new ObjectType(RuleError::class); + $identifierRuleErrorType = new ObjectType(IdentifierRuleError::class); + $listOfIdentifierRuleErrors = new IntersectionType([ + new ArrayType(IntegerRangeType::fromInterval(0, null), $identifierRuleErrorType), + new AccessoryArrayListType(), + ]); + if ($listOfIdentifierRuleErrors->isSuperTypeOf($parentReturnType)->yes()) { + $returnValueType = $returnType->getIterableValueType(); + if (!$returnValueType->isString()->no()) { + $builder->tip('Rules can no longer return plain strings. See: https://phpstan.org/blog/using-rule-error-builder'); + } elseif ( + $ruleErrorType->isSuperTypeOf($returnValueType)->yes() + && !$identifierRuleErrorType->isSuperTypeOf($returnValueType)->yes() + ) { + $builder->tip('Errors are missing identifiers. See: https://phpstan.org/blog/using-rule-error-builder'); + } elseif (!$returnType->isList()->yes()) { + $builder->tip('Return type must be a list. See: https://phpstan.org/blog/using-rule-error-builder'); + } + } + } + $errors[] = $builder->build(); } $parameterResults = $this->checkParameterTypeCompatibility($declaringClass, $parameters->getParameters(), $parentParameters->getParameters()); @@ -110,9 +136,9 @@ public function processNode(Node $node, Scope $scope): array $parameterResult->no() ? 'compatible' : 'contravariant', $parentParameter->getName(), $parentParameterType->describe(VerbosityLevel::value()), - $parentMethod->getDeclaringClass()->getDisplayName(), + $parentMethodDeclaringClass->getDisplayName(), $parentMethod->getName(), - ))->build(); + ))->identifier('method.childParameterType')->build(); } } @@ -120,7 +146,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return ExtendedMethodReflection[] + * @return list */ private function collectParentMethods(string $methodName, ClassReflection $class): array { @@ -130,7 +156,7 @@ private function collectParentMethods(string $methodName, ClassReflection $class if ($parentClass !== null && $parentClass->hasNativeMethod($methodName)) { $parentMethod = $parentClass->getNativeMethod($methodName); if (!$parentMethod->isPrivate()) { - $parentMethods[] = $parentMethod; + $parentMethods[] = [$parentMethod, $parentMethod->getDeclaringClass()]; } } @@ -139,7 +165,34 @@ private function collectParentMethods(string $methodName, ClassReflection $class continue; } - $parentMethods[] = $interface->getNativeMethod($methodName); + $method = $interface->getNativeMethod($methodName); + $parentMethods[] = [$method, $method->getDeclaringClass()]; + } + + if ($this->abstractTraitMethod) { + foreach ($class->getTraits(true) as $trait) { + $nativeTraitReflection = $trait->getNativeReflection(); + if (!$nativeTraitReflection->hasMethod($methodName)) { + continue; + } + + $methodReflection = $nativeTraitReflection->getMethod($methodName); + $isAbstract = $methodReflection->isAbstract(); + if (!$isAbstract) { + continue; + } + + $declaringTrait = $trait->getNativeMethod($methodName)->getDeclaringClass(); + $parentMethods[] = [ + $this->phpClassReflectionExtension->createUserlandMethodReflection( + $trait, + $class, + new NativeBuiltinMethodReflection($methodReflection), + $declaringTrait->getName(), + ), + $declaringTrait, + ]; + } } return $parentMethods; @@ -164,12 +217,12 @@ private function checkReturnTypeCompatibility( ); $parentReturnType = $this->transformStaticType($declaringClass, $originalParentReturnType); // Allow adding `void` return type hints when the parent defines no return type - if ($returnType instanceof VoidType && $parentReturnType instanceof MixedType) { + if ($returnType->isVoid()->yes() && $parentReturnType instanceof MixedType) { return [TrinaryLogic::createYes(), $returnType, $parentReturnType]; } // We can return anything - if ($parentReturnType instanceof VoidType) { + if ($parentReturnType->isVoid()->yes()) { return [TrinaryLogic::createYes(), $returnType, $parentReturnType]; } diff --git a/src/Rules/Methods/MethodVisibilityInInterfaceRule.php b/src/Rules/Methods/MethodVisibilityInInterfaceRule.php new file mode 100644 index 0000000000..2bb1fb915d --- /dev/null +++ b/src/Rules/Methods/MethodVisibilityInInterfaceRule.php @@ -0,0 +1,47 @@ + */ +class MethodVisibilityInInterfaceRule implements Rule +{ + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $method = $node->getMethodReflection(); + + if ($method->isPublic()) { + return []; + } + + $classReflection = $scope->getClassReflection(); + if ($classReflection === null) { + return []; + } + + if (!$classReflection->isInterface()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Method %s::%s() cannot use non-public visibility in interface.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->identifier('method.visibility')->nonIgnorable()->build(), + ]; + } + +} diff --git a/src/Rules/Methods/MissingMagicSerializationMethodsRule.php b/src/Rules/Methods/MissingMagicSerializationMethodsRule.php new file mode 100644 index 0000000000..a169d86794 --- /dev/null +++ b/src/Rules/Methods/MissingMagicSerializationMethodsRule.php @@ -0,0 +1,87 @@ + + */ +class MissingMagicSerializationMethodsRule implements Rule +{ + + public function __construct(private PhpVersion $phpversion) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + if (!$this->phpversion->serializableRequiresMagicMethods()) { + return []; + } + if (!$classReflection->implementsInterface(Serializable::class)) { + return []; + } + if ($classReflection->isAbstract() || $classReflection->isInterface() || $classReflection->isEnum()) { + return []; + } + + $messages = []; + + try { + $nativeMethods = $classReflection->getNativeReflection()->getMethods(); + } catch (IdentifierNotFound) { + return []; + } + + $missingMagicSerialize = true; + $missingMagicUnserialize = true; + foreach ($nativeMethods as $method) { + if (strtolower($method->getName()) === '__serialize') { + $missingMagicSerialize = false; + } + if (strtolower($method->getName()) !== '__unserialize') { + continue; + } + + $missingMagicUnserialize = false; + } + + if ($missingMagicSerialize) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Non-abstract class %s implements the Serializable interface, but does not implement __serialize().', + $classReflection->getDisplayName(), + )) + ->tip('See https://wiki.php.net/rfc/phase_out_serializable') + ->identifier('class.serializable') + ->build(); + } + if ($missingMagicUnserialize) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Non-abstract class %s implements the Serializable interface, but does not implement __unserialize().', + $classReflection->getDisplayName(), + )) + ->tip('See https://wiki.php.net/rfc/phase_out_serializable') + ->identifier('class.serializable') + ->build(); + } + + return $messages; + } + +} diff --git a/src/Rules/Methods/MissingMethodImplementationRule.php b/src/Rules/Methods/MissingMethodImplementationRule.php index 4ccab0d1e1..e7b6d2c3bb 100644 --- a/src/Rules/Methods/MissingMethodImplementationRule.php +++ b/src/Rules/Methods/MissingMethodImplementationRule.php @@ -57,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array $method->getName(), $declaringClass->isInterface() ? 'interface' : 'class', $declaringClass->getName(), - ))->nonIgnorable()->build(); + ))->nonIgnorable()->identifier('method.abstract')->build(); } return $messages; diff --git a/src/Rules/Methods/MissingMethodParameterTypehintRule.php b/src/Rules/Methods/MissingMethodParameterTypehintRule.php index 391dbaf122..62873865c0 100644 --- a/src/Rules/Methods/MissingMethodParameterTypehintRule.php +++ b/src/Rules/Methods/MissingMethodParameterTypehintRule.php @@ -6,13 +6,13 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function implode; use function sprintf; @@ -23,7 +23,10 @@ final class MissingMethodParameterTypehintRule implements Rule { - public function __construct(private MissingTypehintCheck $missingTypehintCheck) + public function __construct( + private MissingTypehintCheck $missingTypehintCheck, + private bool $paramOut, + ) { } @@ -34,15 +37,28 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $methodReflection = $scope->getFunction(); - if (!$methodReflection instanceof MethodReflection) { - return []; - } - + $methodReflection = $node->getMethodReflection(); $messages = []; foreach (ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getParameters() as $parameterReflection) { - foreach ($this->checkMethodParameter($methodReflection, $parameterReflection) as $parameterMessage) { + foreach ($this->checkMethodParameter($methodReflection, sprintf('parameter $%s', $parameterReflection->getName()), $parameterReflection->getType()) as $parameterMessage) { + $messages[] = $parameterMessage; + } + + if ($parameterReflection->getClosureThisType() !== null) { + foreach ($this->checkMethodParameter($methodReflection, sprintf('@param-closure-this PHPDoc tag for parameter $%s', $parameterReflection->getName()), $parameterReflection->getClosureThisType()) as $parameterMessage) { + $messages[] = $parameterMessage; + } + } + + if (!$this->paramOut) { + continue; + } + if ($parameterReflection->getOutType() === null) { + continue; + } + + foreach ($this->checkMethodParameter($methodReflection, sprintf('@param-out PHPDoc tag for parameter $%s', $parameterReflection->getName()), $parameterReflection->getOutType()) as $parameterMessage) { $messages[] = $parameterMessage; } } @@ -51,20 +67,18 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ - private function checkMethodParameter(MethodReflection $methodReflection, ParameterReflection $parameterReflection): array + private function checkMethodParameter(MethodReflection $methodReflection, string $parameterMessage, Type $parameterType): array { - $parameterType = $parameterReflection->getType(); - if ($parameterType instanceof MixedType && !$parameterType->isExplicitMixed()) { return [ RuleErrorBuilder::message(sprintf( - 'Method %s::%s() has parameter $%s with no type specified.', + 'Method %s::%s() has %s with no type specified.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName(), - ))->build(), + $parameterMessage, + ))->identifier('missingType.parameter')->build(), ]; } @@ -72,33 +86,39 @@ private function checkMethodParameter(MethodReflection $methodReflection, Parame foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($parameterType) as $iterableType) { $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); $messages[] = RuleErrorBuilder::message(sprintf( - 'Method %s::%s() has parameter $%s with no value type specified in iterable type %s.', + 'Method %s::%s() has %s with no value type specified in iterable type %s.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $iterableTypeDescription, - ))->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); } foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($parameterType) as [$name, $genericTypeNames]) { $messages[] = RuleErrorBuilder::message(sprintf( - 'Method %s::%s() has parameter $%s with generic %s but does not specify its types: %s', + 'Method %s::%s() has %s with generic %s but does not specify its types: %s', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $name, implode(', ', $genericTypeNames), - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + )) + ->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP) + ->identifier('missingType.generics') + ->build(); } foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($parameterType) as $callableType) { $messages[] = RuleErrorBuilder::message(sprintf( - 'Method %s::%s() has parameter $%s with no signature specified for %s.', + 'Method %s::%s() has %s with no signature specified for %s.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName(), + $parameterMessage, $callableType->describe(VerbosityLevel::typeOnly()), - ))->build(); + ))->identifier('missingType.callable')->build(); } return $messages; diff --git a/src/Rules/Methods/MissingMethodReturnTypehintRule.php b/src/Rules/Methods/MissingMethodReturnTypehintRule.php index 25c12ec143..64e0826908 100644 --- a/src/Rules/Methods/MissingMethodReturnTypehintRule.php +++ b/src/Rules/Methods/MissingMethodReturnTypehintRule.php @@ -5,7 +5,6 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; -use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; @@ -32,11 +31,14 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $methodReflection = $scope->getFunction(); - if (!$methodReflection instanceof MethodReflection) { - return []; + $methodReflection = $node->getMethodReflection(); + if ($scope->isInTrait()) { + $methodNode = $node->getOriginalNode(); + $originalMethodName = $methodNode->getAttribute('originalTraitMethodName'); + if ($originalMethodName === '__construct') { + return []; + } } - $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); if ($returnType instanceof MixedType && !$returnType->isExplicitMixed()) { @@ -45,7 +47,7 @@ public function processNode(Node $node, Scope $scope): array 'Method %s::%s() has no return type specified.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - ))->build(), + ))->identifier('missingType.return')->build(), ]; } @@ -57,7 +59,10 @@ public function processNode(Node $node, Scope $scope): array $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), $iterableTypeDescription, - ))->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); } foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($returnType) as [$name, $genericTypeNames]) { @@ -67,7 +72,10 @@ public function processNode(Node $node, Scope $scope): array $methodReflection->getName(), $name, implode(', ', $genericTypeNames), - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + )) + ->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP) + ->identifier('missingType.generics') + ->build(); } foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($returnType) as $callableType) { @@ -76,7 +84,7 @@ public function processNode(Node $node, Scope $scope): array $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), $callableType->describe(VerbosityLevel::typeOnly()), - ))->build(); + ))->identifier('missingType.callable')->build(); } return $messages; diff --git a/src/Rules/Methods/NullsafeMethodCallRule.php b/src/Rules/Methods/NullsafeMethodCallRule.php index d060485df7..008f4e7d08 100644 --- a/src/Rules/Methods/NullsafeMethodCallRule.php +++ b/src/Rules/Methods/NullsafeMethodCallRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\NullType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -23,18 +22,15 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $nullType = new NullType(); $calledOnType = $scope->getType($node->var); - if ($calledOnType->equals($nullType)) { - return []; - } - - if (!$calledOnType->isSuperTypeOf($nullType)->no()) { + if (!$calledOnType->isNull()->no()) { return []; } return [ - RuleErrorBuilder::message(sprintf('Using nullsafe method call on non-nullable type %s. Use -> instead.', $calledOnType->describe(VerbosityLevel::typeOnly())))->build(), + RuleErrorBuilder::message(sprintf('Using nullsafe method call on non-nullable type %s. Use -> instead.', $calledOnType->describe(VerbosityLevel::typeOnly()))) + ->identifier('nullsafe.neverNull') + ->build(), ]; } diff --git a/src/Rules/Methods/OverridingMethodRule.php b/src/Rules/Methods/OverridingMethodRule.php index 0e140f51eb..17c4f09990 100644 --- a/src/Rules/Methods/OverridingMethodRule.php +++ b/src/Rules/Methods/OverridingMethodRule.php @@ -6,17 +6,23 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\FunctionVariantWithPhpDocs; use PHPStan\Reflection\MethodPrototypeReflection; +use PHPStan\Reflection\Native\NativeMethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; +use PHPStan\Reflection\Php\NativeBuiltinMethodReflection; +use PHPStan\Reflection\Php\PhpClassReflectionExtension; +use PHPStan\Reflection\Php\PhpMethodReflection; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; +use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; use function array_merge; use function count; +use function is_bool; use function sprintf; use function strtolower; @@ -31,6 +37,10 @@ public function __construct( private MethodSignatureRule $methodSignatureRule, private bool $checkPhpDocMethodSignatures, private MethodParameterComparisonHelper $methodParameterComparisonHelper, + private PhpClassReflectionExtension $phpClassReflectionExtension, + private bool $genericPrototypeMessage, + private bool $finalByPhpDoc, + private bool $checkMissingOverrideMethodAttribute, ) { } @@ -42,47 +52,94 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - if (!$method instanceof PhpMethodFromParserNodeReflection) { - throw new ShouldNotHappenException(); - } - - $prototype = $method->getPrototype(); - if ($prototype->getDeclaringClass()->getName() === $method->getDeclaringClass()->getName()) { + $method = $node->getMethodReflection(); + $prototypeData = $this->findPrototype($node->getClassReflection(), $method->getName()); + if ($prototypeData === null) { if (strtolower($method->getName()) === '__construct') { $parent = $method->getDeclaringClass()->getParentClass(); if ($parent !== null && $parent->hasConstructor()) { $parentConstructor = $parent->getConstructor(); - if ($parentConstructor->isFinal()->yes()) { + if ($parentConstructor->isFinalByKeyword()->yes()) { return $this->addErrors([ RuleErrorBuilder::message(sprintf( 'Method %s::%s() overrides final method %s::%s().', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $parent->getDisplayName(), + $parent->getDisplayName($this->genericPrototypeMessage), + $parentConstructor->getName(), + )) + ->nonIgnorable() + ->identifier('method.parentMethodFinal') + ->build(), + ], $node, $scope); + } + if ($parentConstructor->isFinal()->yes() && $this->finalByPhpDoc) { + return $this->addErrors([ + RuleErrorBuilder::message(sprintf( + 'Method %s::%s() overrides @final method %s::%s().', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $parent->getDisplayName($this->genericPrototypeMessage), $parentConstructor->getName(), - ))->nonIgnorable()->build(), + ))->identifier('method.parentMethodFinalByPhpDoc') + ->build(), ], $node, $scope); } } } - return []; - } + if ($this->phpVersion->supportsOverrideAttribute() && $this->hasOverrideAttribute($node->getOriginalNode())) { + return [ + RuleErrorBuilder::message(sprintf( + 'Method %s::%s() has #[\Override] attribute but does not override any method.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + )) + ->nonIgnorable() + ->identifier('method.override') + ->build(), + ]; + } - if (!$prototype instanceof MethodPrototypeReflection) { return []; } + [$prototype, $prototypeDeclaringClass, $checkVisibility] = $prototypeData; + $messages = []; - if ($prototype->isFinal()) { + if ( + $this->phpVersion->supportsOverrideAttribute() + && $this->checkMissingOverrideMethodAttribute + && !$this->hasOverrideAttribute($node->getOriginalNode()) + ) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Method %s::%s() overrides method %s::%s() but is missing the #[\Override] attribute.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), + $prototype->getName(), + ))->identifier('method.missingOverride')->build(); + } + if ($prototype->isFinalByKeyword()->yes()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Method %s::%s() overrides final method %s::%s().', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), + $prototype->getName(), + )) + ->nonIgnorable() + ->identifier('method.parentMethodFinal') + ->build(); + } elseif ($prototype->isFinal()->yes() && $this->finalByPhpDoc) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Method %s::%s() overrides @final method %s::%s().', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), - ))->nonIgnorable()->build(); + ))->identifier('method.parentMethodFinalByPhpDoc') + ->build(); } if ($prototype->isStatic()) { @@ -91,39 +148,53 @@ public function processNode(Node $node, Scope $scope): array 'Non-static method %s::%s() overrides static method %s::%s().', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), - ))->nonIgnorable()->build(); + )) + ->nonIgnorable() + ->identifier('method.nonStatic') + ->build(); } } elseif ($method->isStatic()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Static method %s::%s() overrides non-static method %s::%s().', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), - ))->nonIgnorable()->build(); + )) + ->nonIgnorable() + ->identifier('method.static') + ->build(); } - if ($prototype->isPublic()) { - if (!$method->isPublic()) { + if ($checkVisibility) { + if ($prototype->isPublic()) { + if (!$method->isPublic()) { + $messages[] = RuleErrorBuilder::message(sprintf( + '%s method %s::%s() overriding public method %s::%s() should also be public.', + $method->isPrivate() ? 'Private' : 'Protected', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), + $prototype->getName(), + )) + ->nonIgnorable() + ->identifier('method.visibility') + ->build(); + } + } elseif ($method->isPrivate()) { $messages[] = RuleErrorBuilder::message(sprintf( - '%s method %s::%s() overriding public method %s::%s() should also be public.', - $method->isPrivate() ? 'Private' : 'Protected', + 'Private method %s::%s() overriding protected method %s::%s() should be protected or public.', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), - ))->nonIgnorable()->build(); + )) + ->nonIgnorable() + ->identifier('method.visibility') + ->build(); } - } elseif ($method->isPrivate()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Private method %s::%s() overriding protected method %s::%s() should be protected or public.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName(), - ))->nonIgnorable()->build(); } $prototypeVariants = $prototype->getVariants(); @@ -136,34 +207,65 @@ public function processNode(Node $node, Scope $scope): array $methodVariant = ParametersAcceptorSelector::selectSingle($method->getVariants()); $methodReturnType = $methodVariant->getNativeReturnType(); + $realPrototype = $method->getPrototype(); + if ( - $this->phpVersion->hasTentativeReturnTypes() - && $prototype->getTentativeReturnType() !== null + $realPrototype instanceof MethodPrototypeReflection + && $this->phpVersion->hasTentativeReturnTypes() + && $realPrototype->getTentativeReturnType() !== null && !$this->hasReturnTypeWillChangeAttribute($node->getOriginalNode()) + && count($prototypeDeclaringClass->getNativeReflection()->getMethod($prototype->getName())->getAttributes('ReturnTypeWillChange')) === 0 ) { - - if (!$this->methodParameterComparisonHelper->isReturnTypeCompatible($prototype->getTentativeReturnType(), $methodVariant->getNativeReturnType(), true)) { + if (!$this->methodParameterComparisonHelper->isReturnTypeCompatible($realPrototype->getTentativeReturnType(), $methodVariant->getNativeReturnType(), true)) { $messages[] = RuleErrorBuilder::message(sprintf( 'Return type %s of method %s::%s() is not covariant with tentative return type %s of method %s::%s().', $methodReturnType->describe(VerbosityLevel::typeOnly()), $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $prototype->getTentativeReturnType()->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName(), - ))->tip('Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.')->nonIgnorable()->build(); + $realPrototype->getTentativeReturnType()->describe(VerbosityLevel::typeOnly()), + $realPrototype->getDeclaringClass()->getDisplayName($this->genericPrototypeMessage), + $realPrototype->getName(), + )) + ->tip('Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.') + ->nonIgnorable() + ->identifier('method.tentativeReturnType') + ->build(); } } - $messages = array_merge($messages, $this->methodParameterComparisonHelper->compare($prototype, $method)); + $messages = array_merge($messages, $this->methodParameterComparisonHelper->compare($prototype, $prototypeDeclaringClass, $method)); if (!$prototypeVariant instanceof FunctionVariantWithPhpDocs) { return $this->addErrors($messages, $node, $scope); } $prototypeReturnType = $prototypeVariant->getNativeReturnType(); + $reportReturnType = true; + if ($this->phpVersion->hasTentativeReturnTypes()) { + $reportReturnType = !$realPrototype instanceof MethodPrototypeReflection || $realPrototype->getTentativeReturnType() === null || $prototype->isInternal()->no(); + } else { + if ($realPrototype instanceof MethodPrototypeReflection && $realPrototype->isInternal()) { + if ($prototype->isInternal()->yes() && $prototypeDeclaringClass->getName() !== $realPrototype->getDeclaringClass()->getName()) { + $realPrototypeVariant = $realPrototype->getVariants()[0]; + if ( + $prototypeReturnType instanceof MixedType + && !$prototypeReturnType->isExplicitMixed() + && (!$realPrototypeVariant->getReturnType() instanceof MixedType || $realPrototypeVariant->getReturnType()->isExplicitMixed()) + ) { + $reportReturnType = false; + } + } + + if ($reportReturnType && $prototype->isInternal()->yes()) { + $reportReturnType = !$this->hasReturnTypeWillChangeAttribute($node->getOriginalNode()); + } + } + } - if (!$this->methodParameterComparisonHelper->isReturnTypeCompatible($prototypeReturnType, $methodReturnType, $this->phpVersion->supportsReturnCovariance())) { + if ( + $reportReturnType + && !$this->methodParameterComparisonHelper->isReturnTypeCompatible($prototypeReturnType, $methodReturnType, $this->phpVersion->supportsReturnCovariance()) + ) { if ($this->phpVersion->supportsReturnCovariance()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Return type %s of method %s::%s() is not covariant with return type %s of method %s::%s().', @@ -171,9 +273,12 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $prototypeReturnType->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), - ))->nonIgnorable()->build(); + )) + ->nonIgnorable() + ->identifier('method.childReturnType') + ->build(); } else { $messages[] = RuleErrorBuilder::message(sprintf( 'Return type %s of method %s::%s() is not compatible with return type %s of method %s::%s().', @@ -181,9 +286,12 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $prototypeReturnType->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), - ))->nonIgnorable()->build(); + )) + ->nonIgnorable() + ->identifier('method.childReturnType') + ->build(); } } @@ -191,8 +299,8 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param RuleError[] $errors - * @return (string|RuleError)[] + * @param list $errors + * @return list */ private function addErrors( array $errors, @@ -224,4 +332,90 @@ private function hasReturnTypeWillChangeAttribute(Node\Stmt\ClassMethod $method) return false; } + private function hasOverrideAttribute(Node\Stmt\ClassMethod $method): bool + { + foreach ($method->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toLowerString() === 'override') { + return true; + } + } + } + + return false; + } + + /** + * @return array{ExtendedMethodReflection, ClassReflection, bool}|null + */ + private function findPrototype(ClassReflection $classReflection, string $methodName): ?array + { + foreach ($classReflection->getImmediateInterfaces() as $immediateInterface) { + if ($immediateInterface->hasNativeMethod($methodName)) { + $method = $immediateInterface->getNativeMethod($methodName); + return [$method, $method->getDeclaringClass(), true]; + } + } + + if ($this->phpVersion->supportsAbstractTraitMethods()) { + foreach ($classReflection->getTraits(true) as $trait) { + $nativeTraitReflection = $trait->getNativeReflection(); + if (!$nativeTraitReflection->hasMethod($methodName)) { + continue; + } + + $methodReflection = $nativeTraitReflection->getMethod($methodName); + $isAbstract = $methodReflection->isAbstract(); + if ($isAbstract) { + $declaringTrait = $trait->getNativeMethod($methodName)->getDeclaringClass(); + return [ + $this->phpClassReflectionExtension->createUserlandMethodReflection( + $trait, + $classReflection, + new NativeBuiltinMethodReflection($methodReflection), + $declaringTrait->getName(), + ), + $declaringTrait, + false, + ]; + } + } + } + + $parentClass = $classReflection->getParentClass(); + if ($parentClass === null) { + return null; + } + + if (!$parentClass->hasNativeMethod($methodName)) { + return null; + } + + $method = $parentClass->getNativeMethod($methodName); + if ($method->isPrivate()) { + return null; + } + + $declaringClass = $method->getDeclaringClass(); + if ($declaringClass->hasConstructor()) { + if ($method->getName() === $declaringClass->getConstructor()->getName()) { + $prototype = $method->getPrototype(); + if ($prototype instanceof PhpMethodReflection || $prototype instanceof MethodPrototypeReflection || $prototype instanceof NativeMethodReflection) { + $abstract = $prototype->isAbstract(); + if (is_bool($abstract)) { + if (!$abstract) { + return null; + } + } elseif (!$abstract->yes()) { + return null; + } + } + } elseif (strtolower($methodName) === '__construct') { + return null; + } + } + + return [$method, $method->getDeclaringClass(), true]; + } + } diff --git a/src/Rules/Methods/ReturnTypeRule.php b/src/Rules/Methods/ReturnTypeRule.php index e1fae76875..d1140961a5 100644 --- a/src/Rules/Methods/ReturnTypeRule.php +++ b/src/Rules/Methods/ReturnTypeRule.php @@ -8,8 +8,20 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Rules\FunctionReturnTypeCheck; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Rules\TipRuleError; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\ArrayType; +use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\IntersectionType; +use PHPStan\Type\ObjectType; +use function count; use function sprintf; +use function strtolower; /** * @implements Rule @@ -41,9 +53,10 @@ public function processNode(Node $node, Scope $scope): array return []; } - return $this->returnTypeCheck->checkReturnType( + $returnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType(); + $errors = $this->returnTypeCheck->checkReturnType( $scope, - ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType(), + $returnType, $node->expr, $node, sprintf( @@ -68,6 +81,43 @@ public function processNode(Node $node, Scope $scope): array ), $method->isGenerator(), ); + + if ( + count($errors) === 1 + && $errors[0]->getIdentifier() === 'return.type' + && !$errors[0] instanceof TipRuleError + && $errors[0] instanceof LineRuleError + && $method->getDeclaringClass()->isSubclassOf(Rule::class) + && strtolower($method->getName()) === 'processnode' + && $node->expr !== null + ) { + $ruleErrorType = new ObjectType(RuleError::class); + $identifierRuleErrorType = new ObjectType(IdentifierRuleError::class); + $listOfIdentifierRuleErrors = new IntersectionType([ + new ArrayType(IntegerRangeType::fromInterval(0, null), $identifierRuleErrorType), + new AccessoryArrayListType(), + ]); + if (!$listOfIdentifierRuleErrors->isSuperTypeOf($returnType)->yes()) { + return $errors; + } + + $returnValueType = $scope->getType($node->expr)->getIterableValueType(); + $builder = RuleErrorBuilder::message($errors[0]->getMessage()) + ->line($errors[0]->getLine()) + ->identifier($errors[0]->getIdentifier()); + if (!$returnValueType->isString()->no()) { + $builder->tip('Rules can no longer return plain strings. See: https://phpstan.org/blog/using-rule-error-builder'); + } elseif ( + $ruleErrorType->isSuperTypeOf($returnValueType)->yes() + && !$identifierRuleErrorType->isSuperTypeOf($returnValueType)->yes() + ) { + $builder->tip('Error is missing an identifier. See: https://phpstan.org/blog/using-rule-error-builder'); + } + + $errors = [$builder->build()]; + } + + return $errors; } } diff --git a/src/Rules/Methods/StaticMethodCallCheck.php b/src/Rules/Methods/StaticMethodCallCheck.php index 64fb1adf87..d50f8dfd32 100644 --- a/src/Rules/Methods/StaticMethodCallCheck.php +++ b/src/Rules/Methods/StaticMethodCallCheck.php @@ -2,30 +2,30 @@ namespace PHPStan\Rules\Methods; +use DOMDocument; use PhpParser\Node\Expr; use PhpParser\Node\Name; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\Native\NativeMethodReflection; use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; -use PHPStan\Rules\RuleError; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\GenericClassStringType; -use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\StaticType; use PHPStan\Type\StringType; -use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; use function array_merge; use function in_array; @@ -38,7 +38,7 @@ class StaticMethodCallCheck public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $checkFunctionNameCase, private bool $reportMagicMethods, ) @@ -47,7 +47,7 @@ public function __construct( /** * @param Name|Expr $class - * @return array{RuleError[], MethodReflection|null} + * @return array{list, ExtendedMethodReflection|null} */ public function check( Scope $scope, @@ -58,6 +58,11 @@ public function check( $errors = []; $isAbstract = false; if ($class instanceof Name) { + $classStringType = $scope->getType(new Expr\ClassConstFetch($class, 'class')); + if ($classStringType->hasMethod($methodName)->yes()) { + return [[], null]; + } + $className = (string) $class; $lowercasedClassName = strtolower($className); if (in_array($lowercasedClassName, ['self', 'static'], true)) { @@ -68,7 +73,7 @@ public function check( 'Calling %s::%s() outside of class scope.', $className, $methodName, - ))->build(), + ))->identifier(sprintf('outOfClass.%s', $lowercasedClassName))->build(), ], null, ]; @@ -82,7 +87,7 @@ public function check( 'Calling %s::%s() outside of class scope.', $className, $methodName, - ))->build(), + ))->identifier(sprintf('outOfClass.parent'))->build(), ], null, ]; @@ -97,7 +102,7 @@ public function check( $scope->getFunctionName(), $methodName, $scope->getClassReflection()->getDisplayName(), - ))->build(), + ))->identifier('class.noParent')->build(), ], null, ]; @@ -120,14 +125,14 @@ public function check( 'Call to static method %s() on an unknown class %s.', $methodName, $className, - ))->discoveringSymbolsTip()->build(), + ))->identifier('class.notFound')->discoveringSymbolsTip()->build(), ], null, ]; - } else { - $errors = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($className, $class)]); } + $errors = $this->classCheck->checkClassNames([new ClassNameNodePair($className, $class)]); + $classType = $scope->resolveTypeByName($class); } @@ -136,6 +141,9 @@ public function check( $nativeMethodReflection = $classReflection->getNativeMethod($methodName); if ($nativeMethodReflection instanceof PhpMethodReflection || $nativeMethodReflection instanceof NativeMethodReflection) { $isAbstract = $nativeMethodReflection->isAbstract(); + if ($isAbstract instanceof TrinaryLogic) { + $isAbstract = $isAbstract->yes(); + } } } } else { @@ -153,7 +161,7 @@ public function check( if ($classType instanceof GenericClassStringType) { $classType = $classType->getGenericType(); - if (!(new ObjectWithoutClassType())->isSuperTypeOf($classType)->yes()) { + if (!$classType->isObject()->yes()) { return [[], null]; } } elseif ($classType->isString()->yes()) { @@ -161,7 +169,7 @@ public function check( } $typeForDescribe = $classType; - if ($classType instanceof ThisType) { + if ($classType instanceof StaticType) { $typeForDescribe = $classType->getStaticObjectType(); } $classType = TypeCombinator::remove($classType, new StringType()); @@ -173,7 +181,7 @@ public function check( 'Cannot call static method %s() on %s.', $methodName, $typeForDescribe->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('staticMethod.nonObject')->build(), ]), null, ]; @@ -181,8 +189,7 @@ public function check( if (!$classType->hasMethod($methodName)->yes()) { if (!$this->reportMagicMethods) { - $directClassNames = TypeUtils::getDirectClassNames($classType); - foreach ($directClassNames as $className) { + foreach ($classType->getObjectClassNames() as $className) { if (!$this->reflectionProvider->hasClass($className)) { continue; } @@ -200,7 +207,7 @@ public function check( 'Call to an undefined static method %s::%s().', $typeForDescribe->describe(VerbosityLevel::typeOnly()), $methodName, - ))->build(), + ))->identifier('staticMethod.notFound')->build(), ]), null, ]; @@ -209,23 +216,31 @@ public function check( $method = $classType->getMethod($methodName, $scope); if (!$method->isStatic()) { $function = $scope->getFunction(); + + $scopeIsInMethodClassOrSubClass = TrinaryLogic::createFromBoolean($scope->isInClass())->lazyAnd( + $classType->getObjectClassNames(), + static fn (string $objectClassName) => TrinaryLogic::createFromBoolean( + $scope->isInClass() + && ($scope->getClassReflection()->getName() === $objectClassName || $scope->getClassReflection()->isSubclassOf($objectClassName)), + ), + ); if ( !$function instanceof MethodReflection || $function->isStatic() - || !$scope->isInClass() - || ( - $classType instanceof TypeWithClassName - && $scope->getClassReflection()->getName() !== $classType->getClassName() - && !$scope->getClassReflection()->isSubclassOf($classType->getClassName()) - ) + || $scopeIsInMethodClassOrSubClass->no() ) { + // per php-src docs, this method can be called statically, even if declared non-static + if (strtolower($method->getName()) === 'loadhtml' && $method->getDeclaringClass()->getName() === DOMDocument::class) { + return [[], null]; + } + return [ array_merge($errors, [ RuleErrorBuilder::message(sprintf( 'Static call to instance method %s::%s().', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - ))->build(), + ))->identifier('method.staticCall')->build(), ]), $method, ]; @@ -240,7 +255,9 @@ public function check( $method->isStatic() ? 'static method' : 'method', $method->getName(), $method->getDeclaringClass()->getDisplayName(), - ))->build(), + )) + ->identifier(sprintf('staticMethod.%s', $method->isPrivate() ? 'private' : 'protected')) + ->build(), ]); } @@ -252,6 +269,9 @@ public function check( $method->isStatic() ? ' static' : '', $method->getDeclaringClass()->getDisplayName(), $method->getName(), + ))->identifier(sprintf( + '%s.callToAbstract', + $method->isStatic() ? 'staticMethod' : 'method', ))->build(), ], $method, @@ -272,7 +292,7 @@ public function check( 'Call to %s with incorrect case: %s', $lowercasedMethodName, $methodName, - ))->build(); + ))->identifier('staticMethod.nameCase')->build(); } return [$errors, $method]; diff --git a/src/Rules/Methods/StaticMethodCallableRule.php b/src/Rules/Methods/StaticMethodCallableRule.php index 72ddeda460..a9bae8d9d5 100644 --- a/src/Rules/Methods/StaticMethodCallableRule.php +++ b/src/Rules/Methods/StaticMethodCallableRule.php @@ -32,6 +32,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message('First-class callables are supported only on PHP 8.1 and later.') ->nonIgnorable() + ->identifier('callable.notSupported') ->build(), ]; } @@ -55,7 +56,9 @@ public function processNode(Node $node, Scope $scope): array $messagesMethodName = SprintfHelper::escapeFormatString($declaringClass->getDisplayName() . '::' . $methodReflection->getName() . '()'); - $errors[] = RuleErrorBuilder::message(sprintf('Creating callable from a non-native static method %s.', $messagesMethodName))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Creating callable from a non-native static method %s.', $messagesMethodName)) + ->identifier('callable.nonNativeMethod') + ->build(); return $errors; } diff --git a/src/Rules/Missing/MissingReturnRule.php b/src/Rules/Missing/MissingReturnRule.php index 54ea24405e..71102e4f23 100644 --- a/src/Rules/Missing/MissingReturnRule.php +++ b/src/Rules/Missing/MissingReturnRule.php @@ -11,13 +11,12 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateMixedType; -use PHPStan\Type\GenericTypeVariableResolver; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; use function sprintf; @@ -74,22 +73,21 @@ public function processNode(Node $node, Scope $scope): array } if ($statementResult->hasYield()) { - if ($returnType instanceof TypeWithClassName && $this->checkPhpDocMissingReturn) { - $generatorReturnType = GenericTypeVariableResolver::getType( - $returnType, - Generator::class, - 'TReturn', - ); - if ($generatorReturnType !== null) { + if ($this->checkPhpDocMissingReturn) { + $generatorReturnType = $returnType->getTemplateType(Generator::class, 'TReturn'); + if (!$generatorReturnType instanceof ErrorType) { $returnType = $generatorReturnType; - if ($returnType instanceof VoidType) { + if ($returnType->isVoid()->yes()) { return []; } if (!$returnType instanceof MixedType) { return [ RuleErrorBuilder::message( sprintf('%s should return %s but return statement is missing.', $description, $returnType->describe(VerbosityLevel::typeOnly())), - )->line($node->getNode()->getStartLine())->build(), + ) + ->line($node->getNode()->getStartLine()) + ->identifier('return.missing') + ->build(), ]; } } @@ -112,6 +110,8 @@ public function processNode(Node $node, Scope $scope): array $errorBuilder->nonIgnorable(); } + $errorBuilder->identifier('return.never'); + return [ $errorBuilder->build(), ]; @@ -137,6 +137,8 @@ public function processNode(Node $node, Scope $scope): array $errorBuilder->nonIgnorable(); } + $errorBuilder->identifier('return.missing'); + return [ $errorBuilder->build(), ]; diff --git a/src/Rules/MissingTypehintCheck.php b/src/Rules/MissingTypehintCheck.php index baafa2cb9b..ab88049e96 100644 --- a/src/Rules/MissingTypehintCheck.php +++ b/src/Rules/MissingTypehintCheck.php @@ -6,10 +6,10 @@ use Generator; use Iterator; use IteratorAggregate; -use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\CallableType; +use PHPStan\Type\ClosureType; use PHPStan\Type\ConditionalType; use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\Generic\GenericObjectType; @@ -20,12 +20,12 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; -use PHPStan\Type\TypeWithClassName; use Traversable; use function array_keys; use function array_merge; use function in_array; use function sprintf; +use function strtolower; class MissingTypehintCheck { @@ -45,7 +45,6 @@ class MissingTypehintCheck * @param string[] $skipCheckGenericClasses */ public function __construct( - private ReflectionProvider $reflectionProvider, private bool $disableCheckMissingIterableValueType, private bool $checkMissingIterableValueType, private bool $checkGenericClassInNonGenericObjectType, @@ -86,23 +85,11 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array if ($type->isIterable()->yes()) { $iterableValue = $type->getIterableValueType(); if ($iterableValue instanceof MixedType && !$iterableValue->isExplicitMixed()) { - if ( - $type instanceof TypeWithClassName - && !in_array($type->getClassName(), self::ITERABLE_GENERIC_CLASS_NAMES, true) - && $this->reflectionProvider->hasClass($type->getClassName()) - ) { - $classReflection = $this->reflectionProvider->getClass($type->getClassName()); - if ($classReflection->isGeneric()) { - return $type; - } - } $iterablesWithMissingValueTypehint[] = $type; } - if (!$type instanceof IntersectionType) { - return $traverse($type); + if ($type instanceof IntersectionType && !$type->isList()->yes()) { + return $type; } - - return $type; } return $traverse($type); }); @@ -152,7 +139,7 @@ public function getNonGenericObjectTypesWithGenericClass(Type $type): array throw new ShouldNotHappenException(); } $objectTypes[] = [ - sprintf('%s %s', $classReflection->isInterface() ? 'interface' : 'class', $classReflection->getDisplayName(false)), + sprintf('%s %s', strtolower($classReflection->getClassTypeDescription()), $classReflection->getDisplayName(false)), array_keys($classReflection->getTemplateTypeMap()->getTypes()), ]; return $type; @@ -176,8 +163,10 @@ public function getCallablesWithMissingSignature(Type $type): array $result = []; TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$result): Type { if ( - ($type instanceof CallableType && $type->isCommonCallable()) || - ($type instanceof ObjectType && $type->getClassName() === Closure::class)) { + ($type instanceof CallableType && $type->isCommonCallable()) + || ($type instanceof ClosureType && $type->isCommonCallable()) + || ($type instanceof ObjectType && $type->getClassName() === Closure::class) + ) { $result[] = $type; } return $traverse($type); diff --git a/src/Rules/Names/UsedNamesRule.php b/src/Rules/Names/UsedNamesRule.php new file mode 100644 index 0000000000..a1afbb742b --- /dev/null +++ b/src/Rules/Names/UsedNamesRule.php @@ -0,0 +1,150 @@ + + */ +final class UsedNamesRule implements Rule +{ + + public function getNodeType(): string + { + return FileNode::class; + } + + /** + * @param FileNode $node + */ + public function processNode(Node $node, Scope $scope): array + { + $usedNames = []; + $errors = []; + foreach ($node->getNodes() as $oneNode) { + if ($oneNode instanceof Namespace_) { + $namespaceName = $oneNode->name !== null ? $oneNode->name->toString() : ''; + foreach ($oneNode->stmts as $stmt) { + foreach ($this->findErrorsForNode($stmt, $namespaceName, $usedNames) as $error) { + $errors[] = $error; + } + } + continue; + } + + foreach ($this->findErrorsForNode($oneNode, '', $usedNames) as $error) { + $errors[] = $error; + } + } + + return $errors; + } + + /** + * @param array $usedNames + * @return list + */ + private function findErrorsForNode(Node $node, string $namespace, array &$usedNames): array + { + $lowerNamespace = strtolower($namespace); + if ($node instanceof Use_) { + if ($this->shouldBeIgnored($node)) { + return []; + } + return $this->findErrorsInUses($node->uses, '', $lowerNamespace, $usedNames); + } + + if ($node instanceof GroupUse) { + if ($this->shouldBeIgnored($node)) { + return []; + } + $useGroupPrefix = $node->prefix->toString(); + return $this->findErrorsInUses($node->uses, $useGroupPrefix, $lowerNamespace, $usedNames); + } + + if ($node instanceof ClassLike) { + if ($node->name === null) { + return []; + } + $type = 'class'; + if ($node instanceof Interface_) { + $type = 'interface'; + } elseif ($node instanceof Trait_) { + $type = 'trait'; + } elseif ($node instanceof Enum_) { + $type = 'enum'; + } + $name = $node->name->toLowerString(); + if (in_array($name, $usedNames[$lowerNamespace] ?? [], true)) { + return [ + RuleErrorBuilder::message(sprintf( + 'Cannot declare %s %s because the name is already in use.', + $type, + $namespace !== '' ? $namespace . '\\' . $node->name->toString() : $node->name->toString(), + )) + ->identifier(sprintf('%s.nameInUse', $type)) + ->line($node->getLine()) + ->nonIgnorable() + ->build(), + ]; + } + $usedNames[$lowerNamespace][] = $name; + return []; + } + + return []; + } + + /** + * @param UseUse[] $uses + * @param array $usedNames + * @return list + */ + private function findErrorsInUses(array $uses, string $useGroupPrefix, string $lowerNamespace, array &$usedNames): array + { + $errors = []; + foreach ($uses as $use) { + if ($this->shouldBeIgnored($use)) { + continue; + } + $useAlias = $use->getAlias()->toLowerString(); + if (in_array($useAlias, $usedNames[$lowerNamespace] ?? [], true)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Cannot use %s as %s because the name is already in use.', + $useGroupPrefix !== '' ? $useGroupPrefix . '\\' . $use->name->toString() : $use->name->toString(), + $use->getAlias()->toString(), + )) + ->identifier('use.nameInUse') + ->line($use->getLine()) + ->nonIgnorable() + ->build(); + continue; + } + $usedNames[$lowerNamespace][] = $useAlias; + } + return $errors; + } + + private function shouldBeIgnored(Use_|GroupUse|UseUse $use): bool + { + return in_array($use->type, [Use_::TYPE_FUNCTION, Use_::TYPE_CONSTANT], true); + } + +} diff --git a/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php b/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php index c4cd4231cf..b961e235fc 100644 --- a/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php +++ b/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php @@ -6,10 +6,10 @@ use PhpParser\Node\Stmt\Use_; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; use function count; @@ -24,7 +24,7 @@ class ExistingNamesInGroupUseRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $checkFunctionNameCase, ) { @@ -42,7 +42,7 @@ public function processNode(Node $node, Scope $scope): array $error = null; /** @var Node\Name $name */ - $name = Node\Name::concat($node->prefix, $use->name, ['startLine' => $use->getLine()]); + $name = Node\Name::concat($node->prefix, $use->name, ['startLine' => $use->getStartLine()]); if ( $node->type === Use_::TYPE_CONSTANT || $use->type === Use_::TYPE_CONSTANT @@ -69,19 +69,27 @@ public function processNode(Node $node, Scope $scope): array return $errors; } - private function checkConstant(Node\Name $name): ?RuleError + private function checkConstant(Node\Name $name): ?IdentifierRuleError { if (!$this->reflectionProvider->hasConstant($name, null)) { - return RuleErrorBuilder::message(sprintf('Used constant %s not found.', (string) $name))->discoveringSymbolsTip()->line($name->getLine())->build(); + return RuleErrorBuilder::message(sprintf('Used constant %s not found.', (string) $name)) + ->discoveringSymbolsTip() + ->line($name->getStartLine()) + ->identifier('constant.notFound') + ->build(); } return null; } - private function checkFunction(Node\Name $name): ?RuleError + private function checkFunction(Node\Name $name): ?IdentifierRuleError { if (!$this->reflectionProvider->hasFunction($name, null)) { - return RuleErrorBuilder::message(sprintf('Used function %s not found.', (string) $name))->discoveringSymbolsTip()->line($name->getLine())->build(); + return RuleErrorBuilder::message(sprintf('Used function %s not found.', (string) $name)) + ->discoveringSymbolsTip() + ->line($name->getStartLine()) + ->identifier('function.notFound') + ->build(); } if ($this->checkFunctionNameCase) { @@ -96,16 +104,19 @@ private function checkFunction(Node\Name $name): ?RuleError 'Function %s used with incorrect case: %s.', $realName, $usedName, - ))->line($name->getLine())->build(); + )) + ->line($name->getStartLine()) + ->identifier('function.nameCase') + ->build(); } } return null; } - private function checkClass(Node\Name $name): ?RuleError + private function checkClass(Node\Name $name): ?IdentifierRuleError { - $errors = $this->classCaseSensitivityCheck->checkClassNames([ + $errors = $this->classCheck->checkClassNames([ new ClassNameNodePair((string) $name, $name), ]); if (count($errors) === 0) { diff --git a/src/Rules/Namespaces/ExistingNamesInUseRule.php b/src/Rules/Namespaces/ExistingNamesInUseRule.php index 3bb023ad25..92415f012c 100644 --- a/src/Rules/Namespaces/ExistingNamesInUseRule.php +++ b/src/Rules/Namespaces/ExistingNamesInUseRule.php @@ -5,10 +5,10 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; use function array_map; @@ -23,7 +23,7 @@ class ExistingNamesInUseRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $checkFunctionNameCase, ) { @@ -59,7 +59,7 @@ public function processNode(Node $node, Scope $scope): array /** * @param Node\Stmt\UseUse[] $uses - * @return RuleError[] + * @return list */ private function checkConstants(array $uses): array { @@ -69,7 +69,11 @@ private function checkConstants(array $uses): array continue; } - $errors[] = RuleErrorBuilder::message(sprintf('Used constant %s not found.', (string) $use->name))->line($use->name->getLine())->discoveringSymbolsTip()->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Used constant %s not found.', (string) $use->name)) + ->line($use->name->getStartLine()) + ->identifier('constant.notFound') + ->discoveringSymbolsTip() + ->build(); } return $errors; @@ -77,14 +81,18 @@ private function checkConstants(array $uses): array /** * @param Node\Stmt\UseUse[] $uses - * @return RuleError[] + * @return list */ private function checkFunctions(array $uses): array { $errors = []; foreach ($uses as $use) { if (!$this->reflectionProvider->hasFunction($use->name, null)) { - $errors[] = RuleErrorBuilder::message(sprintf('Used function %s not found.', (string) $use->name))->line($use->name->getLine())->discoveringSymbolsTip()->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Used function %s not found.', (string) $use->name)) + ->line($use->name->getStartLine()) + ->identifier('function.notFound') + ->discoveringSymbolsTip() + ->build(); } elseif ($this->checkFunctionNameCase) { $functionReflection = $this->reflectionProvider->getFunction($use->name, null); $realName = $functionReflection->getName(); @@ -97,7 +105,10 @@ private function checkFunctions(array $uses): array 'Function %s used with incorrect case: %s.', $realName, $usedName, - ))->line($use->name->getLine())->build(); + )) + ->line($use->name->getStartLine()) + ->identifier('function.nameCase') + ->build(); } } } @@ -107,11 +118,11 @@ private function checkFunctions(array $uses): array /** * @param Node\Stmt\UseUse[] $uses - * @return RuleError[] + * @return list */ private function checkClasses(array $uses): array { - return $this->classCaseSensitivityCheck->checkClassNames( + return $this->classCheck->checkClassNames( array_map(static fn (Node\Stmt\UseUse $use): ClassNameNodePair => new ClassNameNodePair((string) $use->name, $use->name), $uses), ); } diff --git a/src/Rules/NonIgnorableRuleError.php b/src/Rules/NonIgnorableRuleError.php index 4b79dac56d..f0a4fbeee3 100644 --- a/src/Rules/NonIgnorableRuleError.php +++ b/src/Rules/NonIgnorableRuleError.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules; +/** @api */ interface NonIgnorableRuleError extends RuleError { diff --git a/src/Rules/Operators/InvalidAssignVarRule.php b/src/Rules/Operators/InvalidAssignVarRule.php index 7f13093f8f..4f6e564849 100644 --- a/src/Rules/Operators/InvalidAssignVarRule.php +++ b/src/Rules/Operators/InvalidAssignVarRule.php @@ -39,19 +39,28 @@ public function processNode(Node $node, Scope $scope): array if ($this->nullsafeCheck->containsNullSafe($node->var)) { return [ - RuleErrorBuilder::message('Nullsafe operator cannot be on left side of assignment.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Nullsafe operator cannot be on left side of assignment.') + ->identifier('nullsafe.assign') + ->nonIgnorable() + ->build(), ]; } if ($node instanceof AssignRef && $this->nullsafeCheck->containsNullSafe($node->expr)) { return [ - RuleErrorBuilder::message('Nullsafe operator cannot be on right side of assignment by reference.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Nullsafe operator cannot be on right side of assignment by reference.') + ->identifier('nullsafe.byRef') + ->nonIgnorable() + ->build(), ]; } if ($this->containsNonAssignableExpression($node->var)) { return [ - RuleErrorBuilder::message('Expression on left side of assignment is not assignable.')->nonIgnorable()->build(), + RuleErrorBuilder::message('Expression on left side of assignment is not assignable.') + ->identifier('assign.invalidExpr') + ->nonIgnorable() + ->build(), ]; } diff --git a/src/Rules/Operators/InvalidBinaryOperationRule.php b/src/Rules/Operators/InvalidBinaryOperationRule.php index 5acfb28dc7..4fb2167030 100644 --- a/src/Rules/Operators/InvalidBinaryOperationRule.php +++ b/src/Rules/Operators/InvalidBinaryOperationRule.php @@ -50,6 +50,7 @@ public function processNode(Node $node, Scope $scope): array $leftVariable = new Node\Expr\Variable($leftName); $rightVariable = new Node\Expr\Variable($rightName); if ($node instanceof Node\Expr\AssignOp) { + $identifier = 'assignOp'; $newNode = clone $node; $newNode->setAttribute('phpstan_cache_printer', null); $left = $node->var; @@ -57,6 +58,7 @@ public function processNode(Node $node, Scope $scope): array $newNode->var = $leftVariable; $newNode->expr = $rightVariable; } else { + $identifier = 'binaryOp'; $newNode = clone $node; $newNode->setAttribute('phpstan_cache_printer', null); $left = $node->left; @@ -96,8 +98,8 @@ public function processNode(Node $node, Scope $scope): array } $scope = $scope - ->assignVariable($leftName, $leftType) - ->assignVariable($rightName, $rightType); + ->assignVariable($leftName, $leftType, $leftType) + ->assignVariable($rightName, $rightType, $rightType); if (!$scope->getType($newNode) instanceof ErrorType) { return []; @@ -109,7 +111,10 @@ public function processNode(Node $node, Scope $scope): array substr(substr($this->exprPrinter->printExpr($newNode), strlen($leftName) + 2), 0, -(strlen($rightName) + 2)), $scope->getType($left)->describe(VerbosityLevel::value()), $scope->getType($right)->describe(VerbosityLevel::value()), - ))->line($left->getLine())->build(), + )) + ->line($left->getStartLine()) + ->identifier(sprintf('%s.invalid', $identifier)) + ->build(), ]; } diff --git a/src/Rules/Operators/InvalidComparisonOperationRule.php b/src/Rules/Operators/InvalidComparisonOperationRule.php index 24110f32dd..43b8760503 100644 --- a/src/Rules/Operators/InvalidComparisonOperationRule.php +++ b/src/Rules/Operators/InvalidComparisonOperationRule.php @@ -7,16 +7,17 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; -use PHPStan\Type\NullType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function get_class; use function sprintf; /** @@ -48,6 +49,10 @@ public function processNode(Node $node, Scope $scope): array return []; } + if ($this->isNumberType($scope, $node->left) && $this->isNumberType($scope, $node->right)) { + return []; + } + if ( ($this->isNumberType($scope, $node->left) && ( $this->isPossiblyNullableObjectType($scope, $node->right) || $this->isPossiblyNullableArrayType($scope, $node->right) @@ -56,13 +61,42 @@ public function processNode(Node $node, Scope $scope): array $this->isPossiblyNullableObjectType($scope, $node->left) || $this->isPossiblyNullableArrayType($scope, $node->left) )) ) { + switch (get_class($node)) { + case Node\Expr\BinaryOp\Equal::class: + $nodeType = 'equal'; + break; + case Node\Expr\BinaryOp\NotEqual::class: + $nodeType = 'notEqual'; + break; + case Node\Expr\BinaryOp\Greater::class: + $nodeType = 'greater'; + break; + case Node\Expr\BinaryOp\GreaterOrEqual::class: + $nodeType = 'greaterOrEqual'; + break; + case Node\Expr\BinaryOp\Smaller::class: + $nodeType = 'smaller'; + break; + case Node\Expr\BinaryOp\SmallerOrEqual::class: + $nodeType = 'smallerOrEqual'; + break; + case Node\Expr\BinaryOp\Spaceship::class: + $nodeType = 'spaceship'; + break; + default: + throw new ShouldNotHappenException(); + } + return [ RuleErrorBuilder::message(sprintf( 'Comparison operation "%s" between %s and %s results in an error.', $node->getOperatorSigil(), $scope->getType($node->left)->describe(VerbosityLevel::value()), $scope->getType($node->right)->describe(VerbosityLevel::value()), - ))->line($node->left->getLine())->build(), + )) + ->line($node->left->getStartLine()) + ->identifier(sprintf('%s.invalid', $nodeType)) + ->build(), ]; } @@ -72,7 +106,7 @@ public function processNode(Node $node, Scope $scope): array private function isNumberType(Scope $scope, Node\Expr $expr): bool { $acceptedType = new UnionType([new IntegerType(), new FloatType()]); - $onlyNumber = static fn (Type $type): bool => $acceptedType->accepts($type, true)->yes(); + $onlyNumber = static fn (Type $type): bool => $acceptedType->isSuperTypeOf($type)->yes(); $type = $this->ruleLevelHelper->findTypeToCheck($scope, $expr, '', $onlyNumber)->getType(); @@ -83,7 +117,8 @@ private function isNumberType(Scope $scope, Node\Expr $expr): bool return false; } - return !$acceptedType->isSuperTypeOf($type)->no(); + // SimpleXMLElement can be cast to number union type + return !$acceptedType->isSuperTypeOf($type)->no() || $acceptedType->equals($type->toNumber()); } private function isPossiblyNullableObjectType(Scope $scope, Node\Expr $expr): bool @@ -101,7 +136,7 @@ private function isPossiblyNullableObjectType(Scope $scope, Node\Expr $expr): bo return false; } - if (TypeCombinator::containsNull($type) && !$type instanceof NullType) { + if (TypeCombinator::containsNull($type) && !$type->isNull()->yes()) { $type = TypeCombinator::removeNull($type); } @@ -122,7 +157,7 @@ private function isPossiblyNullableArrayType(Scope $scope, Node\Expr $expr): boo static fn (Type $type): bool => $type->isArray()->yes(), )->getType(); - if (TypeCombinator::containsNull($type) && !$type instanceof NullType) { + if (TypeCombinator::containsNull($type) && !$type->isNull()->yes()) { $type = TypeCombinator::removeNull($type); } diff --git a/src/Rules/Operators/InvalidIncDecOperationRule.php b/src/Rules/Operators/InvalidIncDecOperationRule.php index 1bd9d46d0e..b3e4a29263 100644 --- a/src/Rules/Operators/InvalidIncDecOperationRule.php +++ b/src/Rules/Operators/InvalidIncDecOperationRule.php @@ -6,8 +6,10 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\ErrorType; use PHPStan\Type\VerbosityLevel; +use function get_class; use function sprintf; /** @@ -36,6 +38,23 @@ public function processNode(Node $node, Scope $scope): array return []; } + switch (get_class($node)) { + case Node\Expr\PreInc::class: + $nodeType = 'preInc'; + break; + case Node\Expr\PostInc::class: + $nodeType = 'postInc'; + break; + case Node\Expr\PreDec::class: + $nodeType = 'preDec'; + break; + case Node\Expr\PostDec::class: + $nodeType = 'postDec'; + break; + default: + throw new ShouldNotHappenException(); + } + $operatorString = $node instanceof Node\Expr\PreInc || $node instanceof Node\Expr\PostInc ? '++' : '--'; if ( @@ -48,7 +67,10 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Cannot use %s on a non-variable.', $operatorString, - ))->line($node->var->getLine())->build(), + )) + ->line($node->var->getStartLine()) + ->identifier(sprintf('%s.expr', $nodeType)) + ->build(), ]; } @@ -66,7 +88,10 @@ public function processNode(Node $node, Scope $scope): array 'Cannot use %s on %s.', $operatorString, $varType->describe(VerbosityLevel::value()), - ))->line($node->var->getLine())->build(), + )) + ->line($node->var->getStartLine()) + ->identifier(sprintf('%s.type', $nodeType)) + ->build(), ]; } diff --git a/src/Rules/Operators/InvalidUnaryOperationRule.php b/src/Rules/Operators/InvalidUnaryOperationRule.php index 7c3ac12ecf..000b0529f2 100644 --- a/src/Rules/Operators/InvalidUnaryOperationRule.php +++ b/src/Rules/Operators/InvalidUnaryOperationRule.php @@ -45,7 +45,10 @@ public function processNode(Node $node, Scope $scope): array 'Unary operation "%s" on %s results in an error.', $operator, $scope->getType($node->expr)->describe(VerbosityLevel::value()), - ))->line($node->expr->getLine())->build(), + )) + ->line($node->expr->getStartLine()) + ->identifier('unaryOp.invalid') + ->build(), ]; } diff --git a/src/Rules/PhpDoc/AssertRuleHelper.php b/src/Rules/PhpDoc/AssertRuleHelper.php new file mode 100644 index 0000000000..e7d3f8e73b --- /dev/null +++ b/src/Rules/PhpDoc/AssertRuleHelper.php @@ -0,0 +1,95 @@ + + */ + public function check(ExtendedMethodReflection|FunctionReflection $reflection, ParametersAcceptor $acceptor): array + { + $parametersByName = []; + foreach ($acceptor->getParameters() as $parameter) { + $parametersByName[$parameter->getName()] = $parameter->getType(); + } + + if ($reflection instanceof ExtendedMethodReflection) { + $class = $reflection->getDeclaringClass(); + $parametersByName['this'] = new ObjectType($class->getName(), null, $class); + } + + $context = InitializerExprContext::createEmpty(); + + $errors = []; + foreach ($reflection->getAsserts()->getAll() as $assert) { + $parameterName = substr($assert->getParameter()->getParameterName(), 1); + if (!array_key_exists($parameterName, $parametersByName)) { + $errors[] = RuleErrorBuilder::message(sprintf('Assert references unknown parameter $%s.', $parameterName)) + ->identifier('parameter.notFound') + ->build(); + continue; + } + + if (!$assert->isExplicit()) { + continue; + } + + $assertedExpr = $assert->getParameter()->getExpr(new TypeExpr($parametersByName[$parameterName])); + $assertedExprType = $this->initializerExprTypeResolver->getType($assertedExpr, $context); + if ($assertedExprType instanceof ErrorType) { + continue; + } + + $assertedType = $assert->getType(); + + $isSuperType = $assertedType->isSuperTypeOf($assertedExprType); + if ($isSuperType->maybe()) { + continue; + } + + $assertedExprString = $assert->getParameter()->describe(); + + if ($assert->isNegated() ? $isSuperType->yes() : $isSuperType->no()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Asserted %stype %s for %s with type %s can never happen.', + $assert->isNegated() ? 'negated ' : '', + $assertedType->describe(VerbosityLevel::precise()), + $assertedExprString, + $assertedExprType->describe(VerbosityLevel::precise()), + ))->identifier('assert.impossibleType')->build(); + } elseif ($assert->isNegated() ? $isSuperType->no() : $isSuperType->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Asserted %stype %s for %s with type %s does not narrow down the type.', + $assert->isNegated() ? 'negated ' : '', + $assertedType->describe(VerbosityLevel::precise()), + $assertedExprString, + $assertedExprType->describe(VerbosityLevel::precise()), + ))->identifier('assert.alreadyNarrowedType')->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php b/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php index 2a99dcb20c..7ad14e65b6 100644 --- a/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php +++ b/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php @@ -2,16 +2,18 @@ namespace PHPStan\Rules\PhpDoc; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Rules\RuleError; +use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ConditionalType; use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\Generic\TemplateType; +use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use PHPStan\Type\VerbosityLevel; use function array_key_exists; +use function count; use function sprintf; use function substr; @@ -19,17 +21,44 @@ class ConditionalReturnTypeRuleHelper { /** - * @return RuleError[] + * @return list */ - public function check(ParametersAcceptor $acceptor): array + public function check(ParametersAcceptorWithPhpDocs $acceptor): array { - $templateTypeMap = $acceptor->getTemplateTypeMap(); + $conditionalTypes = []; $parametersByName = []; foreach ($acceptor->getParameters() as $parameter) { + TypeTraverser::map($parameter->getType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type { + if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) { + $conditionalTypes[] = $type; + } + + return $traverse($type); + }); + + if ($parameter->getOutType() !== null) { + TypeTraverser::map($parameter->getOutType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type { + if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) { + $conditionalTypes[] = $type; + } + + return $traverse($type); + }); + } + + if ($parameter->getClosureThisType() !== null) { + TypeTraverser::map($parameter->getClosureThisType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type { + if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) { + $conditionalTypes[] = $type; + } + + return $traverse($type); + }); + } + $parametersByName[$parameter->getName()] = $parameter; } - $conditionalTypes = []; TypeTraverser::map($acceptor->getReturnType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type { if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) { $conditionalTypes[] = $type; @@ -42,14 +71,31 @@ public function check(ParametersAcceptor $acceptor): array foreach ($conditionalTypes as $conditionalType) { if ($conditionalType instanceof ConditionalType) { $subjectType = $conditionalType->getSubject(); - if (!$subjectType instanceof TemplateType || $templateTypeMap->getType($subjectType->getName()) === null) { - $errors[] = RuleErrorBuilder::message(sprintf('Conditional return type uses subject type %s which is not part of PHPDoc @template tags.', $subjectType->describe(VerbosityLevel::typeOnly())))->build(); + if ($subjectType instanceof StaticType) { + continue; + } + $templateTypes = []; + TypeTraverser::map($subjectType, static function (Type $type, callable $traverse) use (&$templateTypes): Type { + if ($type instanceof TemplateType) { + $templateTypes[] = $type; + return $type; + } + + return $traverse($type); + }); + + if (count($templateTypes) === 0) { + $errors[] = RuleErrorBuilder::message(sprintf('Conditional return type uses subject type %s which is not part of PHPDoc @template tags.', $subjectType->describe(VerbosityLevel::typeOnly()))) + ->identifier('conditionalType.subjectNotFound') + ->build(); continue; } } else { $parameterName = substr($conditionalType->getParameterName(), 1); if (!array_key_exists($parameterName, $parametersByName)) { - $errors[] = RuleErrorBuilder::message(sprintf('Conditional return type references unknown parameter $%s.', $parameterName))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Conditional return type references unknown parameter $%s.', $parameterName)) + ->identifier('parameter.notFound') + ->build(); continue; } $subjectType = $parametersByName[$parameterName]->getType(); @@ -69,7 +115,11 @@ public function check(ParametersAcceptor $acceptor): array $conditionalType->isNegated() ? ($isTargetSuperType->yes() ? 'false' : 'true') : ($isTargetSuperType->yes() ? 'true' : 'false'), - ))->build(); + )) + ->identifier(sprintf('conditionalType.always%s', $conditionalType->isNegated() + ? ($isTargetSuperType->yes() ? 'False' : 'True') + : ($isTargetSuperType->yes() ? 'True' : 'False'))) + ->build(); } return $errors; diff --git a/src/Rules/PhpDoc/FunctionAssertRule.php b/src/Rules/PhpDoc/FunctionAssertRule.php new file mode 100644 index 0000000000..0995b7574a --- /dev/null +++ b/src/Rules/PhpDoc/FunctionAssertRule.php @@ -0,0 +1,37 @@ + + */ +class FunctionAssertRule implements Rule +{ + + public function __construct(private AssertRuleHelper $helper) + { + } + + public function getNodeType(): string + { + return InFunctionNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $function = $node->getFunctionReflection(); + $variants = $function->getVariants(); + if (count($variants) !== 1) { + return []; + } + + return $this->helper->check($function, $variants[0]); + } + +} diff --git a/src/Rules/PhpDoc/FunctionConditionalReturnTypeRule.php b/src/Rules/PhpDoc/FunctionConditionalReturnTypeRule.php index 5e017174fb..5610c04925 100644 --- a/src/Rules/PhpDoc/FunctionConditionalReturnTypeRule.php +++ b/src/Rules/PhpDoc/FunctionConditionalReturnTypeRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InFunctionNode; use PHPStan\Rules\Rule; -use PHPStan\ShouldNotHappenException; use function count; /** @@ -26,12 +25,8 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - if ($method === null) { - throw new ShouldNotHappenException(); - } - - $variants = $method->getVariants(); + $function = $node->getFunctionReflection(); + $variants = $function->getVariants(); if (count($variants) !== 1) { return []; } diff --git a/src/Rules/PhpDoc/GenericCallableRuleHelper.php b/src/Rules/PhpDoc/GenericCallableRuleHelper.php new file mode 100644 index 0000000000..1fdb7a17c0 --- /dev/null +++ b/src/Rules/PhpDoc/GenericCallableRuleHelper.php @@ -0,0 +1,117 @@ + $functionTemplateTags + * + * @return list + */ + public function check( + Node $node, + Scope $scope, + string $location, + Type $callableType, + ?string $functionName, + array $functionTemplateTags, + ?ClassReflection $classReflection, + ): array + { + $errors = []; + + TypeTraverser::map($callableType, function (Type $type, callable $traverse) use (&$errors, $node, $scope, $location, $functionName, $functionTemplateTags, $classReflection) { + if (!($type instanceof CallableType || $type instanceof ClosureType)) { + return $traverse($type); + } + + $typeDescription = $type->describe(VerbosityLevel::precise()); + + $errors = $this->templateTypeCheck->check( + $scope, + $node, + TemplateTypeScope::createWithAnonymousFunction(), + $type->getTemplateTags(), + sprintf('PHPDoc tag %s template of %s cannot have existing class %%s as its name.', $location, $typeDescription), + sprintf('PHPDoc tag %s template of %s cannot have existing type alias %%s as its name.', $location, $typeDescription), + sprintf('PHPDoc tag %s template %%s of %s has invalid bound type %%s.', $location, $typeDescription), + sprintf('PHPDoc tag %s template %%s of %s with bound type %%s is not supported.', $location, $typeDescription), + ); + + $templateTags = $type->getTemplateTags(); + + $classDescription = null; + if ($classReflection !== null) { + $classDescription = $classReflection->getDisplayName(); + } + + if ($functionName !== null) { + $functionDescription = sprintf('function %s', $functionName); + if ($classReflection !== null) { + $functionDescription = sprintf('method %s::%s', $classDescription, $functionName); + } + + foreach (array_keys($functionTemplateTags) as $name) { + if (!isset($templateTags[$name])) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s template %s of %s shadows @template %s for %s.', + $location, + $name, + $typeDescription, + $name, + $functionDescription, + ))->identifier('callable.shadowTemplate')->build(); + } + } + + if ($classReflection !== null) { + foreach (array_keys($classReflection->getTemplateTags()) as $name) { + if (!isset($templateTags[$name])) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s template %s of %s shadows @template %s for class %s.', + $location, + $name, + $typeDescription, + $name, + $classDescription, + ))->identifier('callable.shadowTemplate')->build(); + } + } + + return $traverse($type); + }); + + return $errors; + } + +} diff --git a/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php index cd96f6e8a0..3b89aea5c3 100644 --- a/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php @@ -5,15 +5,14 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; -use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\InitializerExprContext; -use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Rules\Generics\GenericObjectTypeCheck; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\ParserNodeTypeToPHPStanType; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function array_merge; use function sprintf; @@ -27,7 +26,6 @@ class IncompatibleClassConstantPhpDocTypeRule implements Rule public function __construct( private GenericObjectTypeCheck $genericObjectTypeCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, - private InitializerExprTypeResolver $initializerExprTypeResolver, ) { } @@ -43,31 +41,31 @@ public function processNode(Node $node, Scope $scope): array throw new ShouldNotHappenException(); } + $nativeType = null; + if ($node->type !== null) { + $nativeType = ParserNodeTypeToPHPStanType::resolve($node->type, $scope->getClassReflection()); + } + $errors = []; foreach ($node->consts as $const) { $constantName = $const->name->toString(); - $errors = array_merge($errors, $this->processSingleConstant($scope->getClassReflection(), $constantName)); + $errors = array_merge($errors, $this->processSingleConstant($scope->getClassReflection(), $nativeType, $constantName)); } return $errors; } /** - * @return RuleError[] + * @return list */ - private function processSingleConstant(ClassReflection $classReflection, string $constantName): array + private function processSingleConstant(ClassReflection $classReflection, ?Type $nativeType, string $constantName): array { $constantReflection = $classReflection->getConstant($constantName); - if (!$constantReflection instanceof ClassConstantReflection) { + $phpDocType = $constantReflection->getPhpDocType(); + if ($phpDocType === null) { return []; } - if (!$constantReflection->hasPhpDocType()) { - return []; - } - - $phpDocType = $constantReflection->getValueType(); - $errors = []; if ( $this->unresolvableTypeHelper->containsUnresolvableType($phpDocType) @@ -76,28 +74,26 @@ private function processSingleConstant(ClassReflection $classReflection, string 'PHPDoc tag @var for constant %s::%s contains unresolvable type.', $constantReflection->getDeclaringClass()->getName(), $constantName, - ))->build(); - } else { - $nativeType = $this->initializerExprTypeResolver->getType($constantReflection->getValueExpr(), InitializerExprContext::fromClassReflection($constantReflection->getDeclaringClass())); - $isSuperType = $phpDocType->isSuperTypeOf($nativeType); - $verbosity = VerbosityLevel::getRecommendedLevelByType($phpDocType, $nativeType); + ))->identifier('classConstant.unresolvableType')->build(); + } elseif ($nativeType !== null) { + $isSuperType = $nativeType->isSuperTypeOf($phpDocType); if ($isSuperType->no()) { $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @var for constant %s::%s with type %s is incompatible with value %s.', + 'PHPDoc tag @var for constant %s::%s with type %s is incompatible with native type %s.', $constantReflection->getDeclaringClass()->getDisplayName(), $constantName, - $phpDocType->describe($verbosity), - $nativeType->describe(VerbosityLevel::value()), - ))->build(); + $phpDocType->describe(VerbosityLevel::typeOnly()), + $nativeType->describe(VerbosityLevel::typeOnly()), + ))->identifier('classConstant.phpDocType')->build(); } elseif ($isSuperType->maybe()) { $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @var for constant %s::%s with type %s is not subtype of value %s.', + 'PHPDoc tag @var for constant %s::%s with type %s is not subtype of native type %s.', $constantReflection->getDeclaringClass()->getDisplayName(), $constantName, - $phpDocType->describe($verbosity), - $nativeType->describe(VerbosityLevel::value()), - ))->build(); + $phpDocType->describe(VerbosityLevel::typeOnly()), + $nativeType->describe(VerbosityLevel::typeOnly()), + ))->identifier('classConstant.phpDocType')->build(); } } @@ -126,6 +122,16 @@ private function processSingleConstant(ClassReflection $classReflection, string $className, $escapedConstantName, ), + sprintf( + 'Call-site variance of %%s in generic type %%s in PHPDoc tag @var for constant %s::%s is in conflict with %%s template type %%s of %%s %%s.', + $className, + $escapedConstantName, + ), + sprintf( + 'Call-site variance of %%s in generic type %%s in PHPDoc tag @var for constant %s::%s is redundant, template type %%s of %%s %%s has the same variance.', + $className, + $escapedConstantName, + ), )); } diff --git a/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php index e17cccb90d..e37db8ae89 100644 --- a/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php @@ -6,11 +6,12 @@ use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; +use PHPStan\PhpDoc\Tag\ParamOutTag; +use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\ArrayType; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Type; @@ -30,6 +31,7 @@ public function __construct( private FileTypeMapper $fileTypeMapper, private GenericObjectTypeCheck $genericObjectTypeCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, + private GenericCallableRuleHelper $genericCallableRuleHelper, ) { } @@ -41,11 +43,6 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $docComment = $node->getDocComment(); - if ($docComment === null) { - return []; - } - if ($node instanceof Node\Stmt\ClassMethod) { $functionName = $node->name->name; } elseif ($node instanceof Node\Stmt\Function_) { @@ -54,6 +51,11 @@ public function processNode(Node $node, Scope $scope): array return []; } + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( $scope->getFile(), $scope->isInClass() ? $scope->getClassReflection()->getName() : null, @@ -62,79 +64,125 @@ public function processNode(Node $node, Scope $scope): array $docComment->getText(), ); $nativeParameterTypes = $this->getNativeParameterTypes($node, $scope); - $nativeReturnType = $this->getNativeReturnType($node, $scope); + $byRefParameters = $this->getByRefParameters($node); $errors = []; - foreach ($resolvedPhpDoc->getParamTags() as $parameterName => $phpDocParamTag) { - $phpDocParamType = $phpDocParamTag->getType(); - if (!isset($nativeParameterTypes[$parameterName])) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @param references unknown parameter: $%s', - $parameterName, - ))->identifier('phpDoc.unknownParameter')->metadata(['parameterName' => $parameterName])->build(); + foreach ([$resolvedPhpDoc->getParamTags(), $resolvedPhpDoc->getParamOutTags()] as $parameters) { + foreach ($parameters as $parameterName => $phpDocParamTag) { + $phpDocParamType = $phpDocParamTag->getType(); + $tagName = $phpDocParamTag instanceof ParamTag ? '@param' : '@param-out'; - } elseif ( - $this->unresolvableTypeHelper->containsUnresolvableType($phpDocParamType) - ) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @param for parameter $%s contains unresolvable type.', - $parameterName, - ))->build(); + if (!isset($nativeParameterTypes[$parameterName])) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s references unknown parameter: $%s', + $tagName, + $parameterName, + ))->identifier('parameter.notFound')->build(); - } else { - $nativeParamType = $nativeParameterTypes[$parameterName]; - if ( - $phpDocParamTag->isVariadic() - && $phpDocParamType instanceof ArrayType - && !$nativeParamType instanceof ArrayType + } elseif ( + $this->unresolvableTypeHelper->containsUnresolvableType($phpDocParamType) ) { - $phpDocParamType = $phpDocParamType->getItemType(); - } - $isParamSuperType = $nativeParamType->isSuperTypeOf($phpDocParamType); + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s for parameter $%s contains unresolvable type.', + $tagName, + $parameterName, + ))->identifier('parameter.unresolvableType')->build(); - $escapedParameterName = SprintfHelper::escapeFormatString($parameterName); + } else { + $nativeParamType = $nativeParameterTypes[$parameterName]; + if ( + $phpDocParamTag instanceof ParamTag + && $phpDocParamTag->isVariadic() + && $phpDocParamType->isArray()->yes() + && $nativeParamType->isArray()->no() + ) { + $phpDocParamType = $phpDocParamType->getIterableValueType(); + } + $isParamSuperType = $nativeParamType->isSuperTypeOf($phpDocParamType); - $errors = array_merge($errors, $this->genericObjectTypeCheck->check( - $phpDocParamType, - sprintf( - 'PHPDoc tag @param for parameter $%s contains generic type %%s but %%s %%s is not generic.', - $escapedParameterName, - ), - sprintf( - 'Generic type %%s in PHPDoc tag @param for parameter $%s does not specify all template types of %%s %%s: %%s', - $escapedParameterName, - ), - sprintf( - 'Generic type %%s in PHPDoc tag @param for parameter $%s specifies %%d template types, but %%s %%s supports only %%d: %%s', - $escapedParameterName, - ), - sprintf( - 'Type %%s in generic type %%s in PHPDoc tag @param for parameter $%s is not subtype of template type %%s of %%s %%s.', - $escapedParameterName, - ), - )); + $escapedParameterName = SprintfHelper::escapeFormatString($parameterName); + $escapedTagName = SprintfHelper::escapeFormatString($tagName); - if ($isParamSuperType->no()) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @param for parameter $%s with type %s is incompatible with native type %s.', - $parameterName, - $phpDocParamType->describe(VerbosityLevel::typeOnly()), - $nativeParamType->describe(VerbosityLevel::typeOnly()), - ))->build(); + $errors = array_merge($errors, $this->genericObjectTypeCheck->check( + $phpDocParamType, + sprintf( + 'PHPDoc tag %s for parameter $%s contains generic type %%s but %%s %%s is not generic.', + $escapedTagName, + $escapedParameterName, + ), + sprintf( + 'Generic type %%s in PHPDoc tag %s for parameter $%s does not specify all template types of %%s %%s: %%s', + $escapedTagName, + $escapedParameterName, + ), + sprintf( + 'Generic type %%s in PHPDoc tag %s for parameter $%s specifies %%d template types, but %%s %%s supports only %%d: %%s', + $escapedTagName, + $escapedParameterName, + ), + sprintf( + 'Type %%s in generic type %%s in PHPDoc tag %s for parameter $%s is not subtype of template type %%s of %%s %%s.', + $escapedTagName, + $escapedParameterName, + ), + sprintf( + 'Call-site variance of %%s in generic type %%s in PHPDoc tag %s for parameter $%s is in conflict with %%s template type %%s of %%s %%s.', + $escapedTagName, + $escapedParameterName, + ), + sprintf( + 'Call-site variance of %%s in generic type %%s in PHPDoc tag %s for parameter $%s is redundant, template type %%s of %%s %%s has the same variance.', + $escapedTagName, + $escapedParameterName, + ), + )); - } elseif ($isParamSuperType->maybe()) { - $errorBuilder = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @param for parameter $%s with type %s is not subtype of native type %s.', - $parameterName, - $phpDocParamType->describe(VerbosityLevel::typeOnly()), - $nativeParamType->describe(VerbosityLevel::typeOnly()), + $errors = array_merge($errors, $this->genericCallableRuleHelper->check( + $node, + $scope, + sprintf('%s for parameter $%s', $escapedTagName, $escapedParameterName), + $phpDocParamType, + $functionName, + $resolvedPhpDoc->getTemplateTags(), + $scope->isInClass() ? $scope->getClassReflection() : null, )); - if ($phpDocParamType instanceof TemplateType) { - $errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocParamType->getName(), $nativeParamType->describe(VerbosityLevel::typeOnly()))); + + if ($phpDocParamTag instanceof ParamOutTag) { + if (!$byRefParameters[$parameterName]) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Parameter $%s for PHPDoc tag %s is not passed by reference.', + $parameterName, + $tagName, + ))->identifier('parameter.notByRef')->build(); + + } + continue; } - $errors[] = $errorBuilder->build(); + if ($isParamSuperType->no()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s for parameter $%s with type %s is incompatible with native type %s.', + $tagName, + $parameterName, + $phpDocParamType->describe(VerbosityLevel::typeOnly()), + $nativeParamType->describe(VerbosityLevel::typeOnly()), + ))->identifier('parameter.phpDocType')->build(); + + } elseif ($isParamSuperType->maybe()) { + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s for parameter $%s with type %s is not subtype of native type %s.', + $tagName, + $parameterName, + $phpDocParamType->describe(VerbosityLevel::typeOnly()), + $nativeParamType->describe(VerbosityLevel::typeOnly()), + ))->identifier('parameter.phpDocType'); + if ($phpDocParamType instanceof TemplateType) { + $errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocParamType->getName(), $nativeParamType->describe(VerbosityLevel::typeOnly()))); + } + + $errors[] = $errorBuilder->build(); + } } } } @@ -145,9 +193,10 @@ public function processNode(Node $node, Scope $scope): array if ( $this->unresolvableTypeHelper->containsUnresolvableType($phpDocReturnType) ) { - $errors[] = RuleErrorBuilder::message('PHPDoc tag @return contains unresolvable type.')->build(); + $errors[] = RuleErrorBuilder::message('PHPDoc tag @return contains unresolvable type.')->identifier('return.unresolvableType')->build(); } else { + $nativeReturnType = $this->getNativeReturnType($node, $scope); $isReturnSuperType = $nativeReturnType->isSuperTypeOf($phpDocReturnType); $errors = array_merge($errors, $this->genericObjectTypeCheck->check( $phpDocReturnType, @@ -155,26 +204,38 @@ public function processNode(Node $node, Scope $scope): array 'Generic type %s in PHPDoc tag @return does not specify all template types of %s %s: %s', 'Generic type %s in PHPDoc tag @return specifies %d template types, but %s %s supports only %d: %s', 'Type %s in generic type %s in PHPDoc tag @return is not subtype of template type %s of %s %s.', + 'Call-site variance of %s in generic type %s in PHPDoc tag @return is in conflict with %s template type %s of %s %s.', + 'Call-site variance of %s in generic type %s in PHPDoc tag @return is redundant, template type %s of %s %s has the same variance.', )); if ($isReturnSuperType->no()) { $errors[] = RuleErrorBuilder::message(sprintf( 'PHPDoc tag @return with type %s is incompatible with native type %s.', $phpDocReturnType->describe(VerbosityLevel::typeOnly()), $nativeReturnType->describe(VerbosityLevel::typeOnly()), - ))->build(); + ))->identifier('return.phpDocType')->build(); } elseif ($isReturnSuperType->maybe()) { $errorBuilder = RuleErrorBuilder::message(sprintf( 'PHPDoc tag @return with type %s is not subtype of native type %s.', $phpDocReturnType->describe(VerbosityLevel::typeOnly()), $nativeReturnType->describe(VerbosityLevel::typeOnly()), - )); + ))->identifier('return.phpDocType'); if ($phpDocReturnType instanceof TemplateType) { $errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocReturnType->getName(), $nativeReturnType->describe(VerbosityLevel::typeOnly()))); } $errors[] = $errorBuilder->build(); } + + $errors = array_merge($errors, $this->genericCallableRuleHelper->check( + $node, + $scope, + '@return', + $phpDocReturnType, + $functionName, + $resolvedPhpDoc->getTemplateTags(), + $scope->isInClass() ? $scope->getClassReflection() : null, + )); } } @@ -202,6 +263,22 @@ private function getNativeParameterTypes(Node\FunctionLike $node, Scope $scope): return $nativeParameterTypes; } + /** + * @return array + */ + private function getByRefParameters(Node\FunctionLike $node): array + { + $nativeParameterTypes = []; + foreach ($node->getParams() as $parameter) { + if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { + throw new ShouldNotHappenException(); + } + $nativeParameterTypes[$parameter->var->name] = $parameter->byRef; + } + + return $nativeParameterTypes; + } + private function getNativeReturnType(Node\FunctionLike $node, Scope $scope): Type { return $scope->getFunctionType($node->getReturnType(), false, false); diff --git a/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php index e3929366c5..4713a0cd08 100644 --- a/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php @@ -9,7 +9,6 @@ use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\ParserNodeTypeToPHPStanType; use PHPStan\Type\VerbosityLevel; @@ -25,6 +24,7 @@ class IncompatiblePropertyPhpDocTypeRule implements Rule public function __construct( private GenericObjectTypeCheck $genericObjectTypeCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, + private GenericCallableRuleHelper $genericCallableRuleHelper, ) { } @@ -36,21 +36,20 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - - $propertyName = $node->getName(); $phpDocType = $node->getPhpDocType(); if ($phpDocType === null) { return []; } + $propertyName = $node->getName(); + $description = 'PHPDoc tag @var'; if ($node->isPromoted()) { $description = 'PHPDoc type'; } + $classReflection = $node->getClassReflection(); + $messages = []; if ( $this->unresolvableTypeHelper->containsUnresolvableType($phpDocType) @@ -58,32 +57,32 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( '%s for property %s::$%s contains unresolvable type.', $description, - $scope->getClassReflection()->getDisplayName(), + $classReflection->getDisplayName(), $propertyName, - ))->build(); + ))->identifier('property.unresolvableType')->build(); } - $nativeType = ParserNodeTypeToPHPStanType::resolve($node->getNativeType(), $scope->getClassReflection()); + $nativeType = ParserNodeTypeToPHPStanType::resolve($node->getNativeType(), $classReflection); $isSuperType = $nativeType->isSuperTypeOf($phpDocType); if ($isSuperType->no()) { $messages[] = RuleErrorBuilder::message(sprintf( '%s for property %s::$%s with type %s is incompatible with native type %s.', $description, - $scope->getClassReflection()->getDisplayName(), + $classReflection->getDisplayName(), $propertyName, $phpDocType->describe(VerbosityLevel::typeOnly()), $nativeType->describe(VerbosityLevel::typeOnly()), - ))->build(); + ))->identifier('property.phpDocType')->build(); } elseif ($isSuperType->maybe()) { $errorBuilder = RuleErrorBuilder::message(sprintf( '%s for property %s::$%s with type %s is not subtype of native type %s.', $description, - $scope->getClassReflection()->getDisplayName(), + $classReflection->getDisplayName(), $propertyName, $phpDocType->describe(VerbosityLevel::typeOnly()), $nativeType->describe(VerbosityLevel::typeOnly()), - )); + ))->identifier('property.phpDocType'); if ($phpDocType instanceof TemplateType) { $errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocType->getName(), $nativeType->describe(VerbosityLevel::typeOnly()))); @@ -92,9 +91,21 @@ public function processNode(Node $node, Scope $scope): array $messages[] = $errorBuilder->build(); } - $className = SprintfHelper::escapeFormatString($scope->getClassReflection()->getDisplayName()); + $className = SprintfHelper::escapeFormatString($classReflection->getDisplayName()); $escapedPropertyName = SprintfHelper::escapeFormatString($propertyName); + if ($node->isPromoted() === false) { + $messages = array_merge($messages, $this->genericCallableRuleHelper->check( + $node, + $scope, + '@var', + $phpDocType, + null, + [], + $classReflection, + )); + } + $messages = array_merge($messages, $this->genericObjectTypeCheck->check( $phpDocType, sprintf( @@ -121,6 +132,18 @@ public function processNode(Node $node, Scope $scope): array $className, $escapedPropertyName, ), + sprintf( + 'Call-site variance of %%s in generic type %%s in %s for property %s::$%s is in conflict with %%s template type %%s of %%s %%s.', + $description, + $className, + $escapedPropertyName, + ), + sprintf( + 'Call-site variance of %%s in generic type %%s in %s for property %s::$%s is redundant, template type %%s of %%s %%s has the same variance.', + $description, + $className, + $escapedPropertyName, + ), )); return $messages; diff --git a/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php b/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php new file mode 100644 index 0000000000..22be14246c --- /dev/null +++ b/src/Rules/PhpDoc/IncompatibleSelfOutTypeRule.php @@ -0,0 +1,52 @@ + + */ +class IncompatibleSelfOutTypeRule implements Rule +{ + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $method = $node->getMethodReflection(); + $selfOutType = $method->getSelfOutType(); + + if ($selfOutType === null) { + return []; + } + + $classReflection = $method->getDeclaringClass(); + $classType = new ObjectType($classReflection->getName(), null, $classReflection); + + if ($classType->isSuperTypeOf($selfOutType)->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Self-out type %s of method %s::%s is not subtype of %s.', + $selfOutType->describe(VerbosityLevel::precise()), + $classReflection->getName(), + $method->getName(), + $classType->describe(VerbosityLevel::precise()), + ))->identifier('selfOut.type')->build(), + ]; + } + +} diff --git a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php index 43fafa9e53..5d4f65420c 100644 --- a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php +++ b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\VirtualNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; @@ -11,7 +12,7 @@ use PHPStan\Rules\RuleErrorBuilder; use function in_array; use function sprintf; -use function strpos; +use function str_starts_with; /** * @implements Rule @@ -21,29 +22,49 @@ class InvalidPHPStanDocTagRule implements Rule private const POSSIBLE_PHPSTAN_TAGS = [ '@phpstan-param', + '@phpstan-param-out', '@phpstan-var', - '@phpstan-template', '@phpstan-extends', '@phpstan-implements', '@phpstan-use', '@phpstan-template', + '@phpstan-template-contravariant', '@phpstan-template-covariant', '@phpstan-return', '@phpstan-throws', + '@phpstan-ignore', '@phpstan-ignore-next-line', '@phpstan-ignore-line', '@phpstan-method', '@phpstan-pure', '@phpstan-impure', + '@phpstan-immutable', '@phpstan-type', '@phpstan-import-type', '@phpstan-property', '@phpstan-property-read', '@phpstan-property-write', '@phpstan-consistent-constructor', + '@phpstan-assert', + '@phpstan-assert-if-true', + '@phpstan-assert-if-false', + '@phpstan-self-out', + '@phpstan-this-out', + '@phpstan-allow-private-mutation', + '@phpstan-readonly', + '@phpstan-readonly-allow-private-mutation', + '@phpstan-require-extends', + '@phpstan-require-implements', + '@phpstan-param-immediately-invoked-callable', + '@phpstan-param-later-invoked-callable', + '@phpstan-param-closure-this', ]; - public function __construct(private Lexer $phpDocLexer, private PhpDocParser $phpDocParser) + public function __construct( + private Lexer $phpDocLexer, + private PhpDocParser $phpDocParser, + private bool $checkAllInvalidPhpDocs, + ) { } @@ -54,15 +75,29 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if ( - !$node instanceof Node\Stmt\ClassLike - && !$node instanceof Node\FunctionLike - && !$node instanceof Node\Stmt\Foreach_ - && !$node instanceof Node\Stmt\Property - && !$node instanceof Node\Expr\Assign - && !$node instanceof Node\Expr\AssignRef - ) { - return []; + if (!$this->checkAllInvalidPhpDocs) { + if ( + !$node instanceof Node\Stmt\ClassLike + && !$node instanceof Node\FunctionLike + && !$node instanceof Node\Stmt\Foreach_ + && !$node instanceof Node\Stmt\Property + && !$node instanceof Node\Expr\Assign + && !$node instanceof Node\Expr\AssignRef + && !$node instanceof Node\Stmt\ClassConst + ) { + return []; + } + } else { + // mirrored with InvalidPhpDocTagValueRule + if ($node instanceof VirtualNode) { + return []; + } + if ($node instanceof Node\Stmt\Expression) { + return []; + } + if ($node instanceof Node\Expr && !$node instanceof Node\Expr\Assign && !$node instanceof Node\Expr\AssignRef) { + return []; + } } $docComment = $node->getDocComment(); @@ -75,7 +110,7 @@ public function processNode(Node $node, Scope $scope): array $errors = []; foreach ($phpDocNode->getTags() as $phpDocTag) { - if (strpos($phpDocTag->name, '@phpstan-') !== 0 + if (!str_starts_with($phpDocTag->name, '@phpstan-') || in_array($phpDocTag->name, self::POSSIBLE_PHPSTAN_TAGS, true) ) { continue; @@ -84,7 +119,9 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Unknown PHPDoc tag: %s', $phpDocTag->name, - ))->build(); + )) + ->line(PhpDocLineHelper::detectLine($node, $phpDocTag)) + ->identifier('phpDoc.phpstanTag')->build(); } return $errors; diff --git a/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php b/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php index bcf90bec7e..6734c54e4c 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php @@ -2,16 +2,20 @@ namespace PHPStan\Rules\PhpDoc; +use Nette\Utils\Strings; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\VirtualNode; use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode; +use PHPStan\PhpDocParser\Ast\Type\InvalidTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; -use function strpos; +use function str_starts_with; /** * @implements Rule @@ -19,7 +23,12 @@ class InvalidPhpDocTagValueRule implements Rule { - public function __construct(private Lexer $phpDocLexer, private PhpDocParser $phpDocParser) + public function __construct( + private Lexer $phpDocLexer, + private PhpDocParser $phpDocParser, + private bool $checkAllInvalidPhpDocs, + private bool $invalidPhpDocTagLine, + ) { } @@ -30,16 +39,29 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if ( - !$node instanceof Node\Stmt\ClassLike - && !$node instanceof Node\FunctionLike - && !$node instanceof Node\Stmt\Foreach_ - && !$node instanceof Node\Stmt\Property - && !$node instanceof Node\Expr\Assign - && !$node instanceof Node\Expr\AssignRef - && !$node instanceof Node\Stmt\ClassConst - ) { - return []; + if (!$this->checkAllInvalidPhpDocs) { + if ( + !$node instanceof Node\Stmt\ClassLike + && !$node instanceof Node\FunctionLike + && !$node instanceof Node\Stmt\Foreach_ + && !$node instanceof Node\Stmt\Property + && !$node instanceof Node\Expr\Assign + && !$node instanceof Node\Expr\AssignRef + && !$node instanceof Node\Stmt\ClassConst + ) { + return []; + } + } else { + // mirrored with InvalidPHPStanDocTagRule + if ($node instanceof VirtualNode) { + return []; + } + if ($node instanceof Node\Stmt\Expression) { + return []; + } + if ($node instanceof Node\Expr && !$node instanceof Node\Expr\Assign && !$node instanceof Node\Expr\AssignRef) { + return []; + } } $docComment = $node->getDocComment(); @@ -53,11 +75,26 @@ public function processNode(Node $node, Scope $scope): array $errors = []; foreach ($phpDocNode->getTags() as $phpDocTag) { - if (!($phpDocTag->value instanceof InvalidTagValueNode)) { + if (str_starts_with($phpDocTag->name, '@phan-') || str_starts_with($phpDocTag->name, '@psalm-')) { continue; } - if (strpos($phpDocTag->name, '@psalm-') === 0) { + if ($phpDocTag->value instanceof TypeAliasTagValueNode) { + if (!$phpDocTag->value->type instanceof InvalidTypeNode) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s %s has invalid value: %s', + $phpDocTag->name, + $phpDocTag->value->alias, + $this->trimExceptionMessage($phpDocTag->value->type->getException()->getMessage()), + )) + ->line(PhpDocLineHelper::detectLine($node, $phpDocTag)) + ->identifier('phpDoc.parseError')->build(); + + continue; + } elseif (!($phpDocTag->value instanceof InvalidTagValueNode)) { continue; } @@ -65,11 +102,22 @@ public function processNode(Node $node, Scope $scope): array 'PHPDoc tag %s has invalid value (%s): %s', $phpDocTag->name, $phpDocTag->value->value, - $phpDocTag->value->exception->getMessage(), - ))->build(); + $this->trimExceptionMessage($phpDocTag->value->exception->getMessage()), + )) + ->line(PhpDocLineHelper::detectLine($node, $phpDocTag)) + ->identifier('phpDoc.parseError')->build(); } return $errors; } + private function trimExceptionMessage(string $message): string + { + if ($this->invalidPhpDocTagLine) { + return $message; + } + + return Strings::replace($message, '~( on line \d+)$~', ''); + } + } diff --git a/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php b/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php index fa4a086d05..dd11a16488 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php @@ -6,7 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\MissingTypehintCheck; @@ -29,7 +29,7 @@ class InvalidPhpDocVarTagTypeRule implements Rule public function __construct( private FileTypeMapper $fileTypeMapper, private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private GenericObjectTypeCheck $genericObjectTypeCheck, private MissingTypehintCheck $missingTypehintCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, @@ -79,7 +79,10 @@ public function processNode(Node $node, Scope $scope): array if ( $this->unresolvableTypeHelper->containsUnresolvableType($varTagType) ) { - $errors[] = RuleErrorBuilder::message(sprintf('%s contains unresolvable type.', $identifier))->line($docComment->getStartLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('%s contains unresolvable type.', $identifier)) + ->line($docComment->getStartLine()) + ->identifier('varTag.unresolvableType') + ->build(); continue; } @@ -90,7 +93,10 @@ public function processNode(Node $node, Scope $scope): array '%s has no value type specified in iterable type %s.', $identifier, $iterableTypeDescription, - ))->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); } } @@ -101,6 +107,8 @@ public function processNode(Node $node, Scope $scope): array sprintf('Generic type %%s in %s does not specify all template types of %%s %%s: %%s', $escapedIdentifier), sprintf('Generic type %%s in %s specifies %%d template types, but %%s %%s supports only %%d: %%s', $escapedIdentifier), sprintf('Type %%s in generic type %%s in %s is not subtype of template type %%s of %%s %%s.', $escapedIdentifier), + sprintf('Call-site variance of %%s in generic type %%s in %s is in conflict with %%s template type %%s of %%s %%s.', $escapedIdentifier), + sprintf('Call-site variance of %%s in generic type %%s in %s is redundant, template type %%s of %%s %%s has the same variance.', $escapedIdentifier), )); foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($varTagType) as [$innerName, $genericTypeNames]) { @@ -109,7 +117,10 @@ public function processNode(Node $node, Scope $scope): array $identifier, $innerName, implode(', ', $genericTypeNames), - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + )) + ->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP) + ->identifier('missingType.generics') + ->build(); } $referencedClasses = $varTagType->getReferencedClasses(); @@ -119,24 +130,30 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( sprintf('%s has invalid type %%s.', $identifier), $referencedClass, - ))->build(); + ))->identifier('varTag.trait')->build(); } continue; } + if ($scope->isInClassExists($referencedClass)) { + continue; + } + $errors[] = RuleErrorBuilder::message(sprintf( sprintf('%s contains unknown class %%s.', $identifier), $referencedClass, - ))->discoveringSymbolsTip()->build(); - } - - if (!$this->checkClassCaseSensitivity) { - continue; + )) + ->identifier('class.notFound') + ->discoveringSymbolsTip() + ->build(); } $errors = array_merge( $errors, - $this->classCaseSensitivityCheck->checkClassNames(array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $node), $referencedClasses)), + $this->classCheck->checkClassNames( + array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $node), $referencedClasses), + $this->checkClassCaseSensitivity, + ), ); } diff --git a/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php b/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php index f3d40be686..1efb15ffaa 100644 --- a/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php +++ b/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php @@ -8,8 +8,10 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\ObjectType; +use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; use Throwable; use function sprintf; @@ -30,15 +32,15 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { + if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) { + return []; // is handled by virtual nodes + } + $docComment = $node->getDocComment(); if ($docComment === null) { return []; } - if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) { - return []; // is handled by virtual nodes - } - $functionName = null; if ($scope->getFunction() !== null) { $functionName = $scope->getFunction()->getName(); @@ -57,12 +59,11 @@ public function processNode(Node $node, Scope $scope): array } $phpDocThrowsType = $resolvedPhpDoc->getThrowsTag()->getType(); - if ((new VoidType())->isSuperTypeOf($phpDocThrowsType)->yes()) { + if ($phpDocThrowsType->isVoid()->yes()) { return []; } - $isThrowsSuperType = (new ObjectType(Throwable::class))->isSuperTypeOf($phpDocThrowsType); - if ($isThrowsSuperType->yes()) { + if ($this->isThrowsValid($phpDocThrowsType)) { return []; } @@ -70,8 +71,36 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'PHPDoc tag @throws with type %s is not subtype of Throwable', $phpDocThrowsType->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('throws.notThrowable')->build(), ]; } + private function isThrowsValid(Type $phpDocThrowsType): bool + { + $throwType = new ObjectType(Throwable::class); + if ($phpDocThrowsType instanceof UnionType) { + foreach ($phpDocThrowsType->getTypes() as $innerType) { + if (!$this->isThrowsValid($innerType)) { + return false; + } + } + + return true; + } + + $toIntersectWith = []; + foreach ($phpDocThrowsType->getObjectClassReflections() as $classReflection) { + if (!$classReflection->isInterface()) { + continue; + } + foreach ($classReflection->getRequireExtendsTags() as $requireExtendsTag) { + $toIntersectWith[] = $requireExtendsTag->getType(); + } + } + + return $throwType->isSuperTypeOf( + TypeCombinator::intersect($phpDocThrowsType, ...$toIntersectWith), + )->yes(); + } + } diff --git a/src/Rules/PhpDoc/MethodAssertRule.php b/src/Rules/PhpDoc/MethodAssertRule.php new file mode 100644 index 0000000000..5c24f9bca8 --- /dev/null +++ b/src/Rules/PhpDoc/MethodAssertRule.php @@ -0,0 +1,37 @@ + + */ +class MethodAssertRule implements Rule +{ + + public function __construct(private AssertRuleHelper $helper) + { + } + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $method = $node->getMethodReflection(); + $variants = $method->getVariants(); + if (count($variants) !== 1) { + return []; + } + + return $this->helper->check($method, $variants[0]); + } + +} diff --git a/src/Rules/PhpDoc/MethodConditionalReturnTypeRule.php b/src/Rules/PhpDoc/MethodConditionalReturnTypeRule.php index c82ad133f3..6925576665 100644 --- a/src/Rules/PhpDoc/MethodConditionalReturnTypeRule.php +++ b/src/Rules/PhpDoc/MethodConditionalReturnTypeRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; use PHPStan\Rules\Rule; -use PHPStan\ShouldNotHappenException; use function count; /** @@ -26,11 +25,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - if ($method === null) { - throw new ShouldNotHappenException(); - } - + $method = $node->getMethodReflection(); $variants = $method->getVariants(); if (count($variants) !== 1) { return []; diff --git a/src/Rules/PhpDoc/PhpDocLineHelper.php b/src/Rules/PhpDoc/PhpDocLineHelper.php new file mode 100644 index 0000000000..d1d4b52e8b --- /dev/null +++ b/src/Rules/PhpDoc/PhpDocLineHelper.php @@ -0,0 +1,28 @@ +getAttribute('startLine'); + $phpDoc = $node->getDocComment(); + + if ($phpDocTagLine === null || $phpDoc === null) { + return $node->getLine(); + } + + return $phpDoc->getStartLine() + $phpDocTagLine - 1; + } + +} diff --git a/src/Rules/PhpDoc/RequireExtendsCheck.php b/src/Rules/PhpDoc/RequireExtendsCheck.php new file mode 100644 index 0000000000..14f6d43647 --- /dev/null +++ b/src/Rules/PhpDoc/RequireExtendsCheck.php @@ -0,0 +1,83 @@ + $extendsTags + * @return list + */ + public function checkExtendsTags(Node $node, array $extendsTags): array + { + $errors = []; + + if (count($extendsTags) > 1) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends can only be used once.')) + ->identifier('requireExtends.duplicate') + ->build(); + } + + foreach ($extendsTags as $extendsTag) { + $type = $extendsTag->getType(); + if (!$type instanceof ObjectType) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly()))) + ->identifier('requireExtends.nonObject') + ->build(); + continue; + } + + $class = $type->getClassName(); + $referencedClassReflection = $type->getClassReflection(); + + if ($referencedClassReflection === null) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends contains unknown class %s.', $class)) + ->discoveringSymbolsTip() + ->identifier('class.notFound') + ->build(); + continue; + } + + if (!$referencedClassReflection->isClass()) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends cannot contain non-class type %s.', $class)) + ->identifier(sprintf('requireExtends.%s', strtolower($referencedClassReflection->getClassTypeDescription()))) + ->build(); + } elseif ($referencedClassReflection->isFinal()) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends cannot contain final class %s.', $class)) + ->identifier('requireExtends.finalClass') + ->build(); + } else { + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames([ + new ClassNameNodePair($class, $node), + ], $this->checkClassCaseSensitivity), + ); + } + } + + return $errors; + } + +} diff --git a/src/Rules/PhpDoc/RequireExtendsDefinitionClassRule.php b/src/Rules/PhpDoc/RequireExtendsDefinitionClassRule.php new file mode 100644 index 0000000000..27d8b3d6a3 --- /dev/null +++ b/src/Rules/PhpDoc/RequireExtendsDefinitionClassRule.php @@ -0,0 +1,50 @@ + + */ +class RequireExtendsDefinitionClassRule implements Rule +{ + + public function __construct( + private RequireExtendsCheck $requireExtendsCheck, + ) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + $extendsTags = $classReflection->getRequireExtendsTags(); + + if (count($extendsTags) === 0) { + return []; + } + + if (!$classReflection->isInterface()) { + return [ + RuleErrorBuilder::message('PHPDoc tag @phpstan-require-extends is only valid on trait or interface.') + ->identifier(sprintf('requireExtends.on%s', $classReflection->getClassTypeDescription())) + ->build(), + ]; + } + + return $this->requireExtendsCheck->checkExtendsTags($node, $extendsTags); + } + +} diff --git a/src/Rules/PhpDoc/RequireExtendsDefinitionTraitRule.php b/src/Rules/PhpDoc/RequireExtendsDefinitionTraitRule.php new file mode 100644 index 0000000000..174e7c9385 --- /dev/null +++ b/src/Rules/PhpDoc/RequireExtendsDefinitionTraitRule.php @@ -0,0 +1,43 @@ + + */ +class RequireExtendsDefinitionTraitRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private RequireExtendsCheck $requireExtendsCheck, + ) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Trait_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ( + $node->namespacedName === null + || !$this->reflectionProvider->hasClass($node->namespacedName->toString()) + ) { + return []; + } + + $traitReflection = $this->reflectionProvider->getClass($node->namespacedName->toString()); + $extendsTags = $traitReflection->getRequireExtendsTags(); + + return $this->requireExtendsCheck->checkExtendsTags($node, $extendsTags); + } + +} diff --git a/src/Rules/PhpDoc/RequireImplementsDefinitionClassRule.php b/src/Rules/PhpDoc/RequireImplementsDefinitionClassRule.php new file mode 100644 index 0000000000..03e9422e2a --- /dev/null +++ b/src/Rules/PhpDoc/RequireImplementsDefinitionClassRule.php @@ -0,0 +1,40 @@ + + */ +class RequireImplementsDefinitionClassRule implements Rule +{ + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + $implementsTags = $classReflection->getRequireImplementsTags(); + + if (count($implementsTags) === 0) { + return []; + } + + return [ + RuleErrorBuilder::message('PHPDoc tag @phpstan-require-implements is only valid on trait.') + ->identifier(sprintf('requireImplements.on%s', $classReflection->getClassTypeDescription())) + ->build(), + ]; + } + +} diff --git a/src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php b/src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php new file mode 100644 index 0000000000..2c18b2e3ef --- /dev/null +++ b/src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php @@ -0,0 +1,86 @@ + + */ +class RequireImplementsDefinitionTraitRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private ClassNameCheck $classCheck, + private bool $checkClassCaseSensitivity, + ) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Trait_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ( + $node->namespacedName === null + || !$this->reflectionProvider->hasClass($node->namespacedName->toString()) + ) { + return []; + } + + $traitReflection = $this->reflectionProvider->getClass($node->namespacedName->toString()); + $implementsTags = $traitReflection->getRequireImplementsTags(); + + $errors = []; + foreach ($implementsTags as $implementsTag) { + $type = $implementsTag->getType(); + if (!$type instanceof ObjectType) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-implements contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly()))) + ->identifier('requireImplements.nonObject') + ->build(); + continue; + } + + $class = $type->getClassName(); + $referencedClassReflection = $type->getClassReflection(); + if ($referencedClassReflection === null) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-implements contains unknown class %s.', $class)) + ->discoveringSymbolsTip() + ->identifier('class.notFound') + ->build(); + continue; + } + + if (!$referencedClassReflection->isInterface()) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-implements cannot contain non-interface type %s.', $class)) + ->identifier(sprintf('requireImplements.%s', strtolower($referencedClassReflection->getClassTypeDescription()))) + ->build(); + } else { + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames([ + new ClassNameNodePair($class, $node), + ], $this->checkClassCaseSensitivity), + ); + } + } + + return $errors; + } + +} diff --git a/src/Rules/PhpDoc/VarTagChangedExpressionTypeRule.php b/src/Rules/PhpDoc/VarTagChangedExpressionTypeRule.php new file mode 100644 index 0000000000..3968f05792 --- /dev/null +++ b/src/Rules/PhpDoc/VarTagChangedExpressionTypeRule.php @@ -0,0 +1,30 @@ + + */ +class VarTagChangedExpressionTypeRule implements Rule +{ + + public function __construct(private VarTagTypeRuleHelper $varTagTypeRuleHelper) + { + } + + public function getNodeType(): string + { + return VarTagChangedExpressionTypeNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return $this->varTagTypeRuleHelper->checkExprType($scope, $node->getExpr(), $node->getVarTag()->getType()); + } + +} diff --git a/src/Rules/PhpDoc/VarTagTypeRuleHelper.php b/src/Rules/PhpDoc/VarTagTypeRuleHelper.php new file mode 100644 index 0000000000..d01b17396b --- /dev/null +++ b/src/Rules/PhpDoc/VarTagTypeRuleHelper.php @@ -0,0 +1,192 @@ + + */ + public function checkVarType(Scope $scope, Node\Expr $var, Node\Expr $expr, array $varTags, array $assignedVariables): array + { + $errors = []; + + if ($var instanceof Expr\Variable && is_string($var->name)) { + if (array_key_exists($var->name, $varTags)) { + $varTagType = $varTags[$var->name]->getType(); + } elseif (count($assignedVariables) === 1 && array_key_exists(0, $varTags)) { + $varTagType = $varTags[0]->getType(); + } else { + return []; + } + + return $this->checkExprType($scope, $expr, $varTagType); + } elseif ($var instanceof Expr\List_ || $var instanceof Expr\Array_) { + foreach ($var->items as $i => $arrayItem) { + if ($arrayItem === null) { + continue; + } + if ($arrayItem->key === null) { + $dimExpr = new Node\Scalar\LNumber($i); + } else { + $dimExpr = $arrayItem->key; + } + + $itemErrors = $this->checkVarType($scope, $arrayItem->value, new GetOffsetValueTypeExpr($expr, $dimExpr), $varTags, $assignedVariables); + foreach ($itemErrors as $error) { + $errors[] = $error; + } + } + } + + return $errors; + } + + /** + * @return list + */ + public function checkExprType(Scope $scope, Node\Expr $expr, Type $varTagType): array + { + $errors = []; + $exprNativeType = $scope->getNativeType($expr); + $containsPhpStanType = $this->containsPhpStanType($varTagType); + if ($this->shouldVarTagTypeBeReported($expr, $exprNativeType, $varTagType)) { + $verbosity = VerbosityLevel::getRecommendedLevelByType($exprNativeType, $varTagType); + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @var with type %s is not subtype of native type %s.', + $varTagType->describe($verbosity), + $exprNativeType->describe($verbosity), + ))->identifier('varTag.nativeType')->build(); + } else { + $exprType = $scope->getType($expr); + if ( + $this->shouldVarTagTypeBeReported($expr, $exprType, $varTagType) + && ($this->checkTypeAgainstPhpDocType || $containsPhpStanType) + ) { + $verbosity = VerbosityLevel::getRecommendedLevelByType($exprType, $varTagType); + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @var with type %s is not subtype of type %s.', + $varTagType->describe($verbosity), + $exprType->describe($verbosity), + ))->identifier('varTag.type')->build(); + } + } + + if (count($errors) === 0 && $containsPhpStanType) { + $exprType = $scope->getType($expr); + if (!$exprType->equals($varTagType)) { + $verbosity = VerbosityLevel::getRecommendedLevelByType($exprType, $varTagType); + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @var assumes the expression with type %s is always %s but it\'s error-prone and dangerous.', + $exprType->describe($verbosity), + $varTagType->describe($verbosity), + ))->identifier('phpstanApi.varTagAssumption')->build(); + } + } + + return $errors; + } + + private function containsPhpStanType(Type $type): bool + { + $classReflections = TypeUtils::toBenevolentUnion($type)->getObjectClassReflections(); + foreach ($classReflections as $classReflection) { + if (!$classReflection->isSubclassOf(Type::class)) { + continue; + } + + return true; + } + + return false; + } + + private function shouldVarTagTypeBeReported(Node\Expr $expr, Type $type, Type $varTagType): bool + { + if ($expr instanceof Expr\Array_) { + if ($expr->items === []) { + $type = new ArrayType(new MixedType(), new MixedType()); + } + + return $type->isSuperTypeOf($varTagType)->no(); + } + + if ($expr instanceof Expr\ConstFetch) { + return $type->isSuperTypeOf($varTagType)->no(); + } + + if ($expr instanceof Node\Scalar) { + return $type->isSuperTypeOf($varTagType)->no(); + } + + if ($expr instanceof Expr\New_) { + if ($type instanceof GenericObjectType) { + $type = new ObjectType($type->getClassName()); + } + } + + return $this->checkType($type, $varTagType); + } + + private function checkType(Type $type, Type $varTagType, int $depth = 0): bool + { + if ($this->strictWideningCheck) { + return !$type->isSuperTypeOf($varTagType)->yes(); + } + + if ($type->isConstantArray()->yes()) { + if ($type->isIterableAtLeastOnce()->no()) { + $type = new ArrayType(new MixedType(), new MixedType()); + return $type->isSuperTypeOf($varTagType)->no(); + } + } + + if ($type->isIterable()->yes() && $varTagType->isIterable()->yes()) { + if ($type->isSuperTypeOf($varTagType)->no()) { + return true; + } + + $innerType = $type->getIterableValueType(); + $innerVarTagType = $varTagType->getIterableValueType(); + + if ($type->equals($innerType) || $varTagType->equals($innerVarTagType)) { + return !$innerType->isSuperTypeOf($innerVarTagType)->yes(); + } + + return $this->checkType($innerType, $innerVarTagType, $depth + 1); + } + + if ($type->isConstantValue()->yes() && $depth === 0) { + return $type->isSuperTypeOf($varTagType)->no(); + } + + return !$type->isSuperTypeOf($varTagType)->yes(); + } + +} diff --git a/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php b/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php index c1f3dac121..031edf1743 100644 --- a/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php +++ b/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php @@ -6,13 +6,15 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; +use PHPStan\Node\Expr\GetIterableKeyTypeExpr; +use PHPStan\Node\Expr\GetIterableValueTypeExpr; use PHPStan\Node\InClassMethodNode; use PHPStan\Node\InClassNode; use PHPStan\Node\InFunctionNode; use PHPStan\Node\VirtualNode; use PHPStan\PhpDoc\Tag\VarTag; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; @@ -34,6 +36,8 @@ class WrongVariableNameInVarTagRule implements Rule public function __construct( private FileTypeMapper $fileTypeMapper, + private VarTagTypeRuleHelper $varTagTypeRuleHelper, + private bool $checkTypeAgainstNativeType, ) { } @@ -78,11 +82,11 @@ public function processNode(Node $node, Scope $scope): array } if ($node instanceof Node\Stmt\Foreach_) { - return $this->processForeach($node->expr, $node->keyVar, $node->valueVar, $varTags); + return $this->processForeach($scope, $node->expr, $node->keyVar, $node->valueVar, $varTags); } if ($node instanceof Node\Stmt\Static_) { - return $this->processStatic($node->vars, $varTags); + return $this->processStatic($scope, $node->vars, $varTags); } if ($node instanceof Node\Stmt\Expression) { @@ -116,7 +120,7 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'PHPDoc tag @var above %s has no effect.', $description, - ))->build(), + ))->identifier('varTag.misplaced')->build(), ]; } @@ -125,24 +129,31 @@ public function processNode(Node $node, Scope $scope): array /** * @param VarTag[] $varTags - * @return RuleError[] + * @return list */ - private function processAssign(Scope $scope, Node\Expr $var, array $varTags): array + private function processAssign(Scope $scope, Node\Expr $var, Node\Expr $expr, array $varTags): array { $errors = []; $hasMultipleMessage = false; $assignedVariables = $this->getAssignedVariables($var); + if ($this->checkTypeAgainstNativeType) { + foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $var, $expr, $varTags, $assignedVariables) as $error) { + $errors[] = $error; + } + } foreach (array_keys($varTags) as $key) { if (is_int($key)) { if (count($varTags) !== 1) { if (!$hasMultipleMessage) { - $errors[] = RuleErrorBuilder::message('Multiple PHPDoc @var tags above single variable assignment are not supported.')->build(); + $errors[] = RuleErrorBuilder::message('Multiple PHPDoc @var tags above single variable assignment are not supported.') + ->identifier('varTag.multipleTags') + ->build(); $hasMultipleMessage = true; } } elseif (count($assignedVariables) !== 1) { $errors[] = RuleErrorBuilder::message( 'PHPDoc tag @var above assignment does not specify variable name.', - )->build(); + )->identifier('varTag.noVariable')->build(); } continue; } @@ -160,9 +171,11 @@ private function processAssign(Scope $scope, Node\Expr $var, array $varTags): ar 'Variable $%s in PHPDoc tag @var does not match assigned variable $%s.', $key, $assignedVariables[0], - ))->build(); + ))->identifier('varTag.differentVariable')->build(); } else { - $errors[] = RuleErrorBuilder::message(sprintf('Variable $%s in PHPDoc tag @var does not exist.', $key))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Variable $%s in PHPDoc tag @var does not exist.', $key)) + ->identifier('varTag.variableNotFound') + ->build(); } } @@ -200,9 +213,9 @@ private function getAssignedVariables(Expr $expr): array /** * @param VarTag[] $varTags - * @return RuleError[] + * @return list */ - private function processForeach(Node\Expr $iterateeExpr, ?Node\Expr $keyVar, Node\Expr $valueVar, array $varTags): array + private function processForeach(Scope $scope, Node\Expr $iterateeExpr, ?Node\Expr $keyVar, Node\Expr $valueVar, array $varTags): array { $variableNames = []; if ($iterateeExpr instanceof Node\Expr\Variable && is_string($iterateeExpr->name)) { @@ -221,7 +234,7 @@ private function processForeach(Node\Expr $iterateeExpr, ?Node\Expr $keyVar, Nod } $errors[] = RuleErrorBuilder::message( 'PHPDoc tag @var above foreach loop does not specify variable name.', - )->build(); + )->identifier('varTag.noVariable')->build(); continue; } @@ -233,18 +246,45 @@ private function processForeach(Node\Expr $iterateeExpr, ?Node\Expr $keyVar, Nod 'Variable $%s in PHPDoc tag @var does not match any variable in the foreach loop: %s', $name, implode(', ', array_map(static fn (string $name): string => sprintf('$%s', $name), $variableNames)), - ))->build(); + ))->identifier('varTag.differentVariable')->build(); + } + + if ($this->checkTypeAgainstNativeType) { + foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $iterateeExpr, $iterateeExpr, $varTags, $variableNames) as $error) { + $errors[] = $error; + } + if ($keyVar !== null) { + foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $keyVar, new GetIterableKeyTypeExpr($iterateeExpr), $varTags, $variableNames) as $error) { + $errors[] = $error; + } + } + foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $valueVar, new GetIterableValueTypeExpr($iterateeExpr), $varTags, $variableNames) as $error) { + $errors[] = $error; + } } return $errors; } + /** + * @param VarTag[] $varTags + * @return list + */ + private function processExpression(Scope $scope, Expr $expr, array $varTags): array + { + if ($expr instanceof Node\Expr\Assign || $expr instanceof Node\Expr\AssignRef) { + return $this->processAssign($scope, $expr->var, $expr->expr, $varTags); + } + + return $this->processStmt($scope, $varTags, null); + } + /** * @param Node\Stmt\StaticVar[] $vars * @param VarTag[] $varTags - * @return RuleError[] + * @return list */ - private function processStatic(array $vars, array $varTags): array + private function processStatic(Scope $scope, array $vars, array $varTags): array { $variableNames = []; foreach ($vars as $var) { @@ -252,7 +292,7 @@ private function processStatic(array $vars, array $varTags): array continue; } - $variableNames[$var->var->name] = true; + $variableNames[] = $var->var->name; } $errors = []; @@ -264,40 +304,38 @@ private function processStatic(array $vars, array $varTags): array $errors[] = RuleErrorBuilder::message( 'PHPDoc tag @var above multiple static variables does not specify variable name.', - )->build(); + )->identifier('varTag.noVariable')->build(); continue; } - if (isset($variableNames[$name])) { + if (in_array($name, $variableNames, true)) { continue; } $errors[] = RuleErrorBuilder::message(sprintf( 'Variable $%s in PHPDoc tag @var does not match any static variable: %s', $name, - implode(', ', array_map(static fn (string $name): string => sprintf('$%s', $name), array_keys($variableNames))), - ))->build(); + implode(', ', array_map(static fn (string $name): string => sprintf('$%s', $name), $variableNames)), + ))->identifier('varTag.differentVariable')->build(); } - return $errors; - } - - /** - * @param VarTag[] $varTags - * @return RuleError[] - */ - private function processExpression(Scope $scope, Expr $expr, array $varTags): array - { - if ($expr instanceof Node\Expr\Assign || $expr instanceof Node\Expr\AssignRef) { - return $this->processAssign($scope, $expr->var, $varTags); + if ($this->checkTypeAgainstNativeType) { + foreach ($vars as $var) { + if ($var->default === null) { + continue; + } + foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $var->var, $var->default, $varTags, $variableNames) as $error) { + $errors[] = $error; + } + } } - return $this->processStmt($scope, $varTags, null); + return $errors; } /** * @param VarTag[] $varTags - * @return RuleError[] + * @return list */ private function processStmt(Scope $scope, array $varTags, ?Expr $defaultExpr): array { @@ -314,12 +352,16 @@ private function processStmt(Scope $scope, array $varTags, ?Expr $defaultExpr): continue; } - $errors[] = RuleErrorBuilder::message(sprintf('Variable $%s in PHPDoc tag @var does not exist.', $name))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Variable $%s in PHPDoc tag @var does not exist.', $name)) + ->identifier('varTag.variableNotFound') + ->build(); } if (count($variableLessVarTags) !== 1 || $defaultExpr === null) { if (count($variableLessVarTags) > 0) { - $errors[] = RuleErrorBuilder::message('PHPDoc tag @var does not specify variable name.')->build(); + $errors[] = RuleErrorBuilder::message('PHPDoc tag @var does not specify variable name.') + ->identifier('varTag.noVariable') + ->build(); } } @@ -328,7 +370,7 @@ private function processStmt(Scope $scope, array $varTags, ?Expr $defaultExpr): /** * @param VarTag[] $varTags - * @return RuleError[] + * @return list */ private function processGlobal(Scope $scope, Node\Stmt\Global_ $node, array $varTags): array { @@ -353,7 +395,7 @@ private function processGlobal(Scope $scope, Node\Stmt\Global_ $node, array $var $errors[] = RuleErrorBuilder::message( 'PHPDoc tag @var above multiple global variables does not specify variable name.', - )->build(); + )->identifier('varTag.noVariable')->build(); continue; } @@ -365,7 +407,7 @@ private function processGlobal(Scope $scope, Node\Stmt\Global_ $node, array $var 'Variable $%s in PHPDoc tag @var does not match any global variable: %s', $name, implode(', ', array_map(static fn (string $name): string => sprintf('$%s', $name), array_keys($variableNames))), - ))->build(); + ))->identifier('varTag.differentVariable')->build(); } return $errors; diff --git a/src/Rules/Playground/FunctionNeverRule.php b/src/Rules/Playground/FunctionNeverRule.php new file mode 100644 index 0000000000..94171408dd --- /dev/null +++ b/src/Rules/Playground/FunctionNeverRule.php @@ -0,0 +1,52 @@ + + */ +class FunctionNeverRule implements Rule +{ + + public function __construct(private NeverRuleHelper $helper) + { + } + + public function getNodeType(): string + { + return FunctionReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (count($node->getReturnStatements()) > 0) { + return []; + } + + $function = $node->getFunctionReflection(); + + $returnType = ParametersAcceptorSelector::selectSingle($function->getVariants())->getReturnType(); + $helperResult = $this->helper->shouldReturnNever($node, $returnType); + if ($helperResult === false) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Function %s() always %s, it should have return type "never".', + $function->getName(), + count($helperResult) === 0 ? 'throws an exception' : 'terminates script execution', + ))->identifier('phpstanPlayground.never')->build(), + ]; + } + +} diff --git a/src/Rules/Playground/MethodNeverRule.php b/src/Rules/Playground/MethodNeverRule.php new file mode 100644 index 0000000000..443a31112f --- /dev/null +++ b/src/Rules/Playground/MethodNeverRule.php @@ -0,0 +1,53 @@ + + */ +class MethodNeverRule implements Rule +{ + + public function __construct(private NeverRuleHelper $helper) + { + } + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (count($node->getReturnStatements()) > 0) { + return []; + } + + $method = $node->getMethodReflection(); + + $returnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType(); + $helperResult = $this->helper->shouldReturnNever($node, $returnType); + if ($helperResult === false) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Method %s::%s() always %s, it should have return type "never".', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + count($helperResult) === 0 ? 'throws an exception' : 'terminates script execution', + ))->identifier('phpstanPlayground.never')->build(), + ]; + } + +} diff --git a/src/Rules/Playground/NeverRuleHelper.php b/src/Rules/Playground/NeverRuleHelper.php new file mode 100644 index 0000000000..9da99b88e9 --- /dev/null +++ b/src/Rules/Playground/NeverRuleHelper.php @@ -0,0 +1,42 @@ +|false + */ + public function shouldReturnNever(ReturnStatementsNode $node, Type $returnType): array|false + { + if ($returnType instanceof NeverType && $returnType->isExplicit()) { + return false; + } + + if ($node->isGenerator()) { + return false; + } + + $other = []; + foreach ($node->getExecutionEnds() as $executionEnd) { + if ($executionEnd->getStatementResult()->isAlwaysTerminating()) { + if (!$executionEnd->getNode() instanceof Node\Stmt\Throw_) { + $other[] = $executionEnd->getNode(); + } + + continue; + } + + return false; + } + + return $other; + } + +} diff --git a/src/Rules/Playground/NoPhpCodeRule.php b/src/Rules/Playground/NoPhpCodeRule.php new file mode 100644 index 0000000000..2042177a2c --- /dev/null +++ b/src/Rules/Playground/NoPhpCodeRule.php @@ -0,0 +1,41 @@ + + */ +class NoPhpCodeRule implements Rule +{ + + public function getNodeType(): string + { + return FileNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (count($node->getNodes()) !== 1) { + return []; + } + + $html = $node->getNodes()[0]; + if (!$html instanceof Node\Stmt\InlineHTML) { + return []; + } + + return [ + RuleErrorBuilder::message('The example does not contain any PHP code. Did you forget the opening identifier('phpstanPlayground.noPhp') + ->build(), + ]; + } + +} diff --git a/src/Rules/Playground/NotAnalysedTraitRule.php b/src/Rules/Playground/NotAnalysedTraitRule.php new file mode 100644 index 0000000000..7efa804442 --- /dev/null +++ b/src/Rules/Playground/NotAnalysedTraitRule.php @@ -0,0 +1,62 @@ + + */ +class NotAnalysedTraitRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $traitDeclarationData = $node->get(TraitDeclarationCollector::class); + $traitUseData = $node->get(TraitUseCollector::class); + + $declaredTraits = []; + foreach ($traitDeclarationData as $file => $declaration) { + foreach ($declaration as [$name, $line]) { + $declaredTraits[strtolower($name)] = [$file, $name, $line]; + } + } + + foreach ($traitUseData as $usedNamesData) { + foreach ($usedNamesData as $usedNames) { + foreach ($usedNames as $usedName) { + unset($declaredTraits[strtolower($usedName)]); + } + } + } + + $errors = []; + foreach ($declaredTraits as [$file, $name, $line]) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Trait %s is used zero times and is not analysed.', + $name, + )) + ->identifier('phpstanPlayground.traitUnused') + ->file($file) + ->line($line) + ->tip('See: https://phpstan.org/blog/how-phpstan-analyses-traits') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php b/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php index 5f7e92fbac..d68fca010a 100644 --- a/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php +++ b/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php @@ -57,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array 'Unsafe access to private property %s::$%s through static::.', $property->getDeclaringClass()->getDisplayName(), $propertyName, - ))->build(), + ))->identifier('staticClassAccess.privateProperty')->build(), ]; } diff --git a/src/Rules/Properties/AccessPropertiesRule.php b/src/Rules/Properties/AccessPropertiesRule.php index b1ad394fe0..0a8d6cdbcf 100644 --- a/src/Rules/Properties/AccessPropertiesRule.php +++ b/src/Rules/Properties/AccessPropertiesRule.php @@ -9,14 +9,14 @@ use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; +use PHPStan\Type\StaticType; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; use function array_map; use function array_merge; @@ -48,7 +48,7 @@ public function processNode(Node $node, Scope $scope): array if ($node->name instanceof Identifier) { $names = [$node->name->name]; } else { - $names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), TypeUtils::getConstantStrings($scope->getType($node->name))); + $names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $scope->getType($node->name)->getConstantStrings()); } $errors = []; @@ -60,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ private function processSingleProperty(Scope $scope, PropertyFetch $node, string $name): array { @@ -79,13 +79,18 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string return []; } + $typeForDescribe = $type; + if ($type instanceof StaticType) { + $typeForDescribe = $type->getStaticObjectType(); + } + if ($type->canAccessProperties()->no() || $type->canAccessProperties()->maybe() && !$scope->isUndefinedExpressionAllowed($node)) { return [ RuleErrorBuilder::message(sprintf( 'Cannot access property $%s on %s.', $name, - $type->describe(VerbosityLevel::typeOnly()), - ))->build(), + $typeForDescribe->describe(VerbosityLevel::typeOnly()), + ))->identifier('property.nonObject')->build(), ]; } @@ -95,11 +100,11 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string } if (!$has->yes()) { - if ($scope->isSpecified($node)) { + if ($scope->hasExpressionType($node)->yes()) { return []; } - $classNames = $typeResult->getReferencedClasses(); + $classNames = $type->getObjectClassNames(); if (!$this->reportMagicProperties) { foreach ($classNames as $className) { if (!$this->reflectionProvider->hasClass($className)) { @@ -117,17 +122,19 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string } if (count($classNames) === 1) { - $referencedClass = $typeResult->getReferencedClasses()[0]; - $propertyClassReflection = $this->reflectionProvider->getClass($referencedClass); + $propertyClassReflection = $this->reflectionProvider->getClass($classNames[0]); $parentClassReflection = $propertyClassReflection->getParentClass(); while ($parentClassReflection !== null) { if ($parentClassReflection->hasProperty($name)) { + if ($scope->canAccessProperty($parentClassReflection->getProperty($name, $scope))) { + return []; + } return [ RuleErrorBuilder::message(sprintf( 'Access to private property $%s of parent class %s.', $name, $parentClassReflection->getDisplayName(), - ))->build(), + ))->identifier('property.private')->build(), ]; } @@ -137,11 +144,13 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string $ruleErrorBuilder = RuleErrorBuilder::message(sprintf( 'Access to an undefined property %s::$%s.', - $type->describe(VerbosityLevel::typeOnly()), + $typeForDescribe->describe(VerbosityLevel::typeOnly()), $name, - )); + ))->identifier('property.notFound'); if ($typeResult->getTip() !== null) { $ruleErrorBuilder->tip($typeResult->getTip()); + } else { + $ruleErrorBuilder->tip('Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'); } return [ @@ -157,7 +166,7 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string $propertyReflection->isPrivate() ? 'private' : 'protected', $type->describe(VerbosityLevel::typeOnly()), $name, - ))->build(), + ))->identifier(sprintf('property.%s', $propertyReflection->isPrivate() ? 'private' : 'protected'))->build(), ]; } diff --git a/src/Rules/Properties/AccessStaticPropertiesRule.php b/src/Rules/Properties/AccessStaticPropertiesRule.php index ff40da244a..9d929c39a2 100644 --- a/src/Rules/Properties/AccessStaticPropertiesRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesRule.php @@ -9,10 +9,10 @@ use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\ShouldNotHappenException; @@ -26,6 +26,7 @@ use PHPStan\Type\VerbosityLevel; use function array_map; use function array_merge; +use function count; use function in_array; use function sprintf; use function strtolower; @@ -39,7 +40,7 @@ class AccessStaticPropertiesRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, ) { } @@ -54,7 +55,7 @@ public function processNode(Node $node, Scope $scope): array if ($node->name instanceof Node\VarLikeIdentifier) { $names = [$node->name->name]; } else { - $names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), TypeUtils::getConstantStrings($scope->getType($node->name))); + $names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $scope->getType($node->name)->getConstantStrings()); } $errors = []; @@ -66,7 +67,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, string $name): array { @@ -81,7 +82,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, 'Accessing %s::$%s outside of class scope.', $class, $name, - ))->build(), + ))->identifier(sprintf('outOfClass.%s', $lowercasedClass))->build(), ]; } $classType = $scope->resolveTypeByName($node->class); @@ -92,7 +93,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, 'Accessing %s::$%s outside of class scope.', $class, $name, - ))->build(), + ))->identifier('outOfClass.parent')->build(), ]; } if ($scope->getClassReflection()->getParentClass() === null) { @@ -103,7 +104,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, $scope->getFunctionName(), $name, $scope->getClassReflection()->getDisplayName(), - ))->build(), + ))->identifier('class.noParent')->build(), ]; } @@ -129,12 +130,15 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, 'Access to static property $%s on an unknown class %s.', $name, $class, - ))->discoveringSymbolsTip()->build(), + )) + ->discoveringSymbolsTip() + ->identifier('class.notFound') + ->build(), ]; - } else { - $messages = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($class, $node->class)]); } + $messages = $this->classCheck->checkClassNames([new ClassNameNodePair($class, $node->class)]); + $classType = $scope->resolveTypeByName($node->class); } } else { @@ -170,7 +174,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, 'Cannot access static property $%s on %s.', $name, $typeForDescribe->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('staticProperty.nonObject')->build(), ]); } @@ -180,16 +184,39 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, } if (!$has->yes()) { - if ($scope->isSpecified($node)) { + if ($scope->hasExpressionType($node)->yes()) { return $messages; } + $classNames = $classType->getObjectClassNames(); + if (count($classNames) === 1) { + $propertyClassReflection = $this->reflectionProvider->getClass($classNames[0]); + $parentClassReflection = $propertyClassReflection->getParentClass(); + + while ($parentClassReflection !== null) { + if ($parentClassReflection->hasProperty($name)) { + if ($scope->canAccessProperty($parentClassReflection->getProperty($name, $scope))) { + return []; + } + return [ + RuleErrorBuilder::message(sprintf( + 'Access to private static property $%s of parent class %s.', + $name, + $parentClassReflection->getDisplayName(), + ))->identifier('staticProperty.private')->build(), + ]; + } + + $parentClassReflection = $parentClassReflection->getParentClass(); + } + } + return array_merge($messages, [ RuleErrorBuilder::message(sprintf( 'Access to an undefined static property %s::$%s.', $typeForDescribe->describe(VerbosityLevel::typeOnly()), $name, - ))->build(), + ))->identifier('staticProperty.notFound')->build(), ]); } @@ -207,7 +234,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, 'Static access to instance property %s::$%s.', $property->getDeclaringClass()->getDisplayName(), $name, - ))->build(), + ))->identifier('property.staticAccess')->build(), ]); } @@ -218,7 +245,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, $property->isPrivate() ? 'private' : 'protected', $name, $property->getDeclaringClass()->getDisplayName(), - ))->build(), + ))->identifier(sprintf('staticProperty.%s', $property->isPrivate() ? 'private' : 'protected'))->build(), ]); } diff --git a/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php b/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php index b27e01de6b..ad0d4badc7 100644 --- a/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php +++ b/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php @@ -8,7 +8,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -30,16 +29,13 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - - $classReflection = $scope->getClassReflection(); $default = $node->getDefault(); if ($default === null) { return []; } + $classReflection = $node->getClassReflection(); + $propertyReflection = $classReflection->getNativeProperty($node->getName()); $propertyType = $propertyReflection->getWritableType(); if ($propertyReflection->getNativeType() instanceof MixedType) { @@ -48,7 +44,8 @@ public function processNode(Node $node, Scope $scope): array } } $defaultValueType = $scope->getType($default); - if ($this->ruleLevelHelper->accepts($propertyType, $defaultValueType, true)) { + $accepts = $this->ruleLevelHelper->acceptsWithReason($propertyType, $defaultValueType, true); + if ($accepts->result) { return []; } @@ -62,7 +59,10 @@ public function processNode(Node $node, Scope $scope): array $node->getName(), $propertyType->describe($verbosityLevel), $defaultValueType->describe($verbosityLevel), - ))->build(), + )) + ->identifier('property.defaultValue') + ->acceptsReasonsTip($accepts->reasons) + ->build(), ]; } diff --git a/src/Rules/Properties/ExistingClassesInPropertiesRule.php b/src/Rules/Properties/ExistingClassesInPropertiesRule.php index 1f4780b33b..2b138d5bbd 100644 --- a/src/Rules/Properties/ExistingClassesInPropertiesRule.php +++ b/src/Rules/Properties/ExistingClassesInPropertiesRule.php @@ -7,12 +7,11 @@ use PHPStan\Node\ClassPropertyNode; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use function array_map; use function array_merge; use function sprintf; @@ -25,7 +24,7 @@ class ExistingClassesInPropertiesRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, private PhpVersion $phpVersion, private bool $checkClassCaseSensitivity, @@ -41,11 +40,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - - $propertyReflection = $scope->getClassReflection()->getNativeProperty($node->getName()); + $propertyReflection = $node->getClassReflection()->getNativeProperty($node->getName()); if ($this->checkThisOnly) { $referencedClasses = $propertyReflection->getNativeType()->getReferencedClasses(); } else { @@ -64,7 +59,7 @@ public function processNode(Node $node, Scope $scope): array $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), $referencedClass, - ))->build(); + ))->identifier('property.trait')->build(); } continue; } @@ -74,15 +69,16 @@ public function processNode(Node $node, Scope $scope): array $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), $referencedClass, - ))->discoveringSymbolsTip()->build(); + ))->identifier('class.notFound')->discoveringSymbolsTip()->build(); } - if ($this->checkClassCaseSensitivity) { - $errors = array_merge( - $errors, - $this->classCaseSensitivityCheck->checkClassNames(array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $node), $referencedClasses)), - ); - } + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames( + array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $node), $referencedClasses), + $this->checkClassCaseSensitivity, + ), + ); if ( $this->phpVersion->supportsPureIntersectionTypes() @@ -92,7 +88,7 @@ public function processNode(Node $node, Scope $scope): array 'Property %s::$%s has unresolvable native type.', $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->build(); + ))->identifier('property.unresolvableNativeType')->build(); } return $errors; diff --git a/src/Rules/Properties/InvalidCallablePropertyTypeRule.php b/src/Rules/Properties/InvalidCallablePropertyTypeRule.php new file mode 100644 index 0000000000..2a8ed41bd8 --- /dev/null +++ b/src/Rules/Properties/InvalidCallablePropertyTypeRule.php @@ -0,0 +1,65 @@ + + */ +class InvalidCallablePropertyTypeRule implements Rule +{ + + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + $propertyReflection = $classReflection->getNativeProperty($node->getName()); + + if (!$propertyReflection->hasNativeType()) { + return []; + } + + $nativeType = $propertyReflection->getNativeType(); + $callableTypes = []; + + TypeTraverser::map($nativeType, static function (Type $type, callable $traverse) use (&$callableTypes): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if ($type instanceof CallableType) { + $callableTypes[] = $type; + } + + return $type; + }); + + if ($callableTypes === []) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Property %s::$%s cannot have callable in its type declaration.', + $classReflection->getDisplayName(), + $node->getName(), + ))->identifier('property.callableType')->nonIgnorable()->build(), + ]; + } + +} diff --git a/src/Rules/Properties/MissingPropertyTypehintRule.php b/src/Rules/Properties/MissingPropertyTypehintRule.php index 367809b83f..f103d9752a 100644 --- a/src/Rules/Properties/MissingPropertyTypehintRule.php +++ b/src/Rules/Properties/MissingPropertyTypehintRule.php @@ -8,7 +8,6 @@ use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; use function implode; @@ -31,11 +30,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - - $propertyReflection = $scope->getClassReflection()->getNativeProperty($node->getName()); + $propertyReflection = $node->getClassReflection()->getNativeProperty($node->getName()); if ($propertyReflection->isPromoted()) { return []; @@ -49,7 +44,7 @@ public function processNode(Node $node, Scope $scope): array 'Property %s::$%s has no type specified.', $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->build(), + ))->identifier('missingType.property')->build(), ]; } @@ -61,7 +56,10 @@ public function processNode(Node $node, Scope $scope): array $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), $iterableTypeDescription, - ))->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); + )) + ->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP) + ->identifier('missingType.iterableValue') + ->build(); } foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($propertyType) as [$name, $genericTypeNames]) { @@ -71,7 +69,10 @@ public function processNode(Node $node, Scope $scope): array $node->getName(), $name, implode(', ', $genericTypeNames), - ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + )) + ->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP) + ->identifier('missingType.generics') + ->build(); } foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($propertyType) as $callableType) { @@ -80,7 +81,7 @@ public function processNode(Node $node, Scope $scope): array $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), $callableType->describe(VerbosityLevel::typeOnly()), - ))->build(); + ))->identifier('missingType.callable')->build(); } return $messages; diff --git a/src/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRule.php b/src/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRule.php index 9a52f0f99c..72b30052e6 100644 --- a/src/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRule.php +++ b/src/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRule.php @@ -8,7 +8,6 @@ use PHPStan\Reflection\ConstructorsHelper; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use function sprintf; /** @@ -30,10 +29,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); [$properties, $prematureAccess, $additionalAssigns] = $node->getUninitializedProperties($scope, $this->constructorsHelper->getConstructors($classReflection)); $errors = []; @@ -45,10 +41,13 @@ public function processNode(Node $node, Scope $scope): array 'Class %s has an uninitialized @readonly property $%s. Assign it in the constructor.', $classReflection->getDisplayName(), $propertyName, - ))->line($propertyNode->getLine())->build(); + )) + ->line($propertyNode->getStartLine()) + ->identifier('property.uninitializedReadonlyByPhpDoc') + ->build(); } - foreach ($prematureAccess as [$propertyName, $line, $propertyNode]) { + foreach ($prematureAccess as [$propertyName, $line, $propertyNode, $file, $fileDescription]) { if (!$propertyNode->isReadOnlyByPhpDoc() || $propertyNode->isReadOnly()) { continue; } @@ -56,7 +55,11 @@ public function processNode(Node $node, Scope $scope): array 'Access to an uninitialized @readonly property %s::$%s.', $classReflection->getDisplayName(), $propertyName, - ))->line($line)->build(); + )) + ->identifier('property.uninitializedReadonlyByPhpDoc') + ->line($line) + ->file($file, $fileDescription) + ->build(); } foreach ($additionalAssigns as [$propertyName, $line, $propertyNode]) { @@ -67,7 +70,7 @@ public function processNode(Node $node, Scope $scope): array '@readonly property %s::$%s is already assigned.', $classReflection->getDisplayName(), $propertyName, - ))->line($line)->build(); + ))->identifier('assign.readOnlyPropertyByPhpDoc')->line($line)->build(); } return $errors; diff --git a/src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php b/src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php index caba4ba063..54d64878ad 100644 --- a/src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php +++ b/src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php @@ -8,7 +8,6 @@ use PHPStan\Reflection\ConstructorsHelper; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use function sprintf; /** @@ -30,10 +29,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); [$properties, $prematureAccess, $additionalAssigns] = $node->getUninitializedProperties($scope, $this->constructorsHelper->getConstructors($classReflection)); $errors = []; @@ -45,10 +41,13 @@ public function processNode(Node $node, Scope $scope): array 'Class %s has an uninitialized readonly property $%s. Assign it in the constructor.', $classReflection->getDisplayName(), $propertyName, - ))->line($propertyNode->getLine())->build(); + )) + ->line($propertyNode->getStartLine()) + ->identifier('property.uninitializedReadonly') + ->build(); } - foreach ($prematureAccess as [$propertyName, $line, $propertyNode]) { + foreach ($prematureAccess as [$propertyName, $line, $propertyNode, $file, $fileDescription]) { if (!$propertyNode->isReadOnly()) { continue; } @@ -56,7 +55,11 @@ public function processNode(Node $node, Scope $scope): array 'Access to an uninitialized readonly property %s::$%s.', $classReflection->getDisplayName(), $propertyName, - ))->line($line)->build(); + )) + ->line($line) + ->file($file, $fileDescription) + ->identifier('property.uninitializedReadonly') + ->build(); } foreach ($additionalAssigns as [$propertyName, $line, $propertyNode]) { @@ -67,7 +70,10 @@ public function processNode(Node $node, Scope $scope): array 'Readonly property %s::$%s is already assigned.', $classReflection->getDisplayName(), $propertyName, - ))->line($line)->build(); + )) + ->line($line) + ->identifier('assign.readOnlyProperty') + ->build(); } return $errors; diff --git a/src/Rules/Properties/NullsafePropertyFetchRule.php b/src/Rules/Properties/NullsafePropertyFetchRule.php index 92fcf0e7fc..1195f47022 100644 --- a/src/Rules/Properties/NullsafePropertyFetchRule.php +++ b/src/Rules/Properties/NullsafePropertyFetchRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\NullType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -27,13 +26,8 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $nullType = new NullType(); $calledOnType = $scope->getType($node->var); - if ($calledOnType->equals($nullType)) { - return []; - } - - if (!$calledOnType->isSuperTypeOf($nullType)->no()) { + if (!$calledOnType->isNull()->no()) { return []; } @@ -42,7 +36,9 @@ public function processNode(Node $node, Scope $scope): array } return [ - RuleErrorBuilder::message(sprintf('Using nullsafe property access on non-nullable type %s. Use -> instead.', $calledOnType->describe(VerbosityLevel::typeOnly())))->build(), + RuleErrorBuilder::message(sprintf('Using nullsafe property access on non-nullable type %s. Use -> instead.', $calledOnType->describe(VerbosityLevel::typeOnly()))) + ->identifier('nullsafe.neverNull') + ->build(), ]; } diff --git a/src/Rules/Properties/OverridingPropertyRule.php b/src/Rules/Properties/OverridingPropertyRule.php index e8749b62f0..ca10cd4434 100644 --- a/src/Rules/Properties/OverridingPropertyRule.php +++ b/src/Rules/Properties/OverridingPropertyRule.php @@ -9,7 +9,6 @@ use PHPStan\Reflection\Php\PhpPropertyReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\ParserNodeTypeToPHPStanType; use PHPStan\Type\VerbosityLevel; use function array_merge; @@ -36,11 +35,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); $prototype = $this->findPrototype($classReflection, $node->getName()); if ($prototype === null) { return []; @@ -55,7 +50,7 @@ public function processNode(Node $node, Scope $scope): array $node->getName(), $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->nonIgnorable()->build(); + ))->identifier('property.nonStatic')->nonIgnorable()->build(); } } elseif ($node->isStatic()) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -64,7 +59,7 @@ public function processNode(Node $node, Scope $scope): array $node->getName(), $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->nonIgnorable()->build(); + ))->identifier('property.static')->nonIgnorable()->build(); } if ($prototype->isReadOnly()) { @@ -75,7 +70,7 @@ public function processNode(Node $node, Scope $scope): array $node->getName(), $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->nonIgnorable()->build(); + ))->identifier('property.readWrite')->nonIgnorable()->build(); } } elseif ($node->isReadOnly()) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -84,7 +79,7 @@ public function processNode(Node $node, Scope $scope): array $node->getName(), $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->nonIgnorable()->build(); + ))->identifier('property.readOnly')->nonIgnorable()->build(); } if ($prototype->isPublic()) { @@ -96,7 +91,7 @@ public function processNode(Node $node, Scope $scope): array $node->getName(), $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->nonIgnorable()->build(); + ))->identifier('property.visibility')->nonIgnorable()->build(); } } elseif ($node->isPrivate()) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -105,7 +100,7 @@ public function processNode(Node $node, Scope $scope): array $node->getName(), $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->nonIgnorable()->build(); + ))->identifier('property.visibility')->nonIgnorable()->build(); } $typeErrors = []; @@ -119,9 +114,9 @@ public function processNode(Node $node, Scope $scope): array $node->getName(), $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()), $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()), - ))->nonIgnorable()->build(); + ))->identifier('property.missingNativeType')->nonIgnorable()->build(); } else { - $nativeType = ParserNodeTypeToPHPStanType::resolve($node->getNativeType(), $scope->getClassReflection()); + $nativeType = ParserNodeTypeToPHPStanType::resolve($node->getNativeType(), $classReflection); if (!$prototype->getNativeType()->equals($nativeType)) { $typeErrors[] = RuleErrorBuilder::message(sprintf( 'Type %s of property %s::$%s is not the same as type %s of overridden property %s::$%s.', @@ -131,7 +126,7 @@ public function processNode(Node $node, Scope $scope): array $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()), $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->nonIgnorable()->build(); + ))->identifier('property.nativeType')->nonIgnorable()->build(); } } } elseif ($node->getNativeType() !== null) { @@ -139,10 +134,10 @@ public function processNode(Node $node, Scope $scope): array 'Property %s::$%s (%s) overriding property %s::$%s should not have a native type.', $classReflection->getDisplayName(), $node->getName(), - ParserNodeTypeToPHPStanType::resolve($node->getNativeType(), $scope->getClassReflection())->describe(VerbosityLevel::typeOnly()), + ParserNodeTypeToPHPStanType::resolve($node->getNativeType(), $classReflection)->describe(VerbosityLevel::typeOnly()), $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->nonIgnorable()->build(); + ))->identifier('property.extraNativeType')->nonIgnorable()->build(); } $errors = array_merge($errors, $typeErrors); @@ -170,7 +165,7 @@ public function processNode(Node $node, Scope $scope): array $prototype->getReadableType()->describe($verbosity), $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->tip(sprintf( + ))->identifier('property.phpDocType')->tip(sprintf( "You can fix 3rd party PHPDoc types with stub files:\n %s\n This error can be turned off by setting\n %s", 'https://phpstan.org/user-guide/stub-files', 'reportMaybesInPropertyPhpDocTypes: false in your %configurationFile%.', @@ -184,7 +179,7 @@ public function processNode(Node $node, Scope $scope): array $prototype->getReadableType()->describe($verbosity), $prototype->getDeclaringClass()->getDisplayName(), $node->getName(), - ))->tip(sprintf( + ))->identifier('property.phpDocType')->tip(sprintf( "You can fix 3rd party PHPDoc types with stub files:\n %s", 'https://phpstan.org/user-guide/stub-files', ))->build(); diff --git a/src/Rules/Properties/PropertiesInInterfaceRule.php b/src/Rules/Properties/PropertiesInInterfaceRule.php new file mode 100644 index 0000000000..35ce4c2c8c --- /dev/null +++ b/src/Rules/Properties/PropertiesInInterfaceRule.php @@ -0,0 +1,36 @@ + + */ +class PropertiesInInterfaceRule implements Rule +{ + + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->getClassReflection()->isInterface()) { + return []; + } + + return [ + RuleErrorBuilder::message('Interfaces may not include properties.') + ->nonIgnorable() + ->identifier('property.inInterface') + ->build(), + ]; + } + +} diff --git a/src/Rules/Properties/PropertyDescriptor.php b/src/Rules/Properties/PropertyDescriptor.php index 4bd700c718..e45011b361 100644 --- a/src/Rules/Properties/PropertyDescriptor.php +++ b/src/Rules/Properties/PropertyDescriptor.php @@ -3,33 +3,39 @@ namespace PHPStan\Rules\Properties; use PhpParser\Node; +use PHPStan\Analyser\Scope; use PHPStan\Reflection\PropertyReflection; +use PHPStan\Type\ObjectType; +use PHPStan\Type\VerbosityLevel; use function sprintf; class PropertyDescriptor { - public function describePropertyByName(PropertyReflection $property, string $propertyName): string - { - if (!$property->isStatic()) { - return sprintf('Property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $propertyName); - } - - return sprintf('Static property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $propertyName); - } - /** * @param Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch */ - public function describeProperty(PropertyReflection $property, $propertyFetch): string + public function describeProperty(PropertyReflection $property, Scope $scope, $propertyFetch): string { + if ($propertyFetch instanceof Node\Expr\PropertyFetch) { + $fetchedOnType = $scope->getType($propertyFetch->var); + $declaringClassType = new ObjectType($property->getDeclaringClass()->getName()); + if ($declaringClassType->isSuperTypeOf($fetchedOnType)->yes()) { + $classDescription = $property->getDeclaringClass()->getDisplayName(); + } else { + $classDescription = $fetchedOnType->describe(VerbosityLevel::typeOnly()); + } + } else { + $classDescription = $property->getDeclaringClass()->getDisplayName(); + } + /** @var Node\Identifier $name */ $name = $propertyFetch->name; if (!$property->isStatic()) { - return sprintf('Property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $name->name); + return sprintf('Property %s::$%s', $classDescription, $name->name); } - return sprintf('Static property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $name->name); + return sprintf('Static property %s::$%s', $classDescription, $name->name); } } diff --git a/src/Rules/Properties/PropertyReflectionFinder.php b/src/Rules/Properties/PropertyReflectionFinder.php index 1dd0cc4748..3f6cc8b000 100644 --- a/src/Rules/Properties/PropertyReflectionFinder.php +++ b/src/Rules/Properties/PropertyReflectionFinder.php @@ -9,7 +9,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; use function array_map; class PropertyReflectionFinder @@ -25,7 +24,7 @@ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): a if ($propertyFetch->name instanceof Node\Identifier) { $names = [$propertyFetch->name->name]; } else { - $names = array_map(static fn (ConstantStringType $name): string => $name->getValue(), TypeUtils::getConstantStrings($scope->getType($propertyFetch->name))); + $names = array_map(static fn (ConstantStringType $name): string => $name->getValue(), $scope->getType($propertyFetch->name)->getConstantStrings()); } $reflections = []; @@ -58,7 +57,7 @@ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): a if ($propertyFetch->name instanceof VarLikeIdentifier) { $names = [$propertyFetch->name->name]; } else { - $names = array_map(static fn (ConstantStringType $name): string => $name->getValue(), TypeUtils::getConstantStrings($scope->getType($propertyFetch->name))); + $names = array_map(static fn (ConstantStringType $name): string => $name->getValue(), $scope->getType($propertyFetch->name)->getConstantStrings()); } $reflections = []; diff --git a/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRule.php b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRule.php index 51d5b0a665..ce0b1d1f05 100644 --- a/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRule.php +++ b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRule.php @@ -46,7 +46,9 @@ public function processNode(Node $node, Scope $scope): array } $declaringClass = $nativeReflection->getDeclaringClass(); - $errors[] = RuleErrorBuilder::message(sprintf('@readonly property %s::$%s is assigned by reference.', $declaringClass->getDisplayName(), $propertyReflection->getName()))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('@readonly property %s::$%s is assigned by reference.', $declaringClass->getDisplayName(), $propertyReflection->getName())) + ->identifier('property.readOnlyByPhpDocAssignByRef') + ->build(); } return $errors; diff --git a/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php index 4eeeb13f50..9d72bb3895 100644 --- a/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php +++ b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php @@ -10,7 +10,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\ThisType; +use PHPStan\Type\TypeUtils; use function in_array; use function sprintf; use function strtolower; @@ -57,13 +57,17 @@ public function processNode(Node $node, Scope $scope): array $declaringClass = $nativeReflection->getDeclaringClass(); if (!$scope->isInClass()) { - $errors[] = RuleErrorBuilder::message(sprintf('@readonly property %s::$%s is assigned outside of its declaring class.', $declaringClass->getDisplayName(), $propertyReflection->getName()))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('@readonly property %s::$%s is assigned outside of its declaring class.', $declaringClass->getDisplayName(), $propertyReflection->getName())) + ->identifier('property.readOnlyByPhpDocAssignOutOfClass') + ->build(); continue; } $scopeClassReflection = $scope->getClassReflection(); if ($scopeClassReflection->getName() !== $declaringClass->getName()) { - $errors[] = RuleErrorBuilder::message(sprintf('@readonly property %s::$%s is assigned outside of its declaring class.', $declaringClass->getDisplayName(), $propertyReflection->getName()))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('@readonly property %s::$%s is assigned outside of its declaring class.', $declaringClass->getDisplayName(), $propertyReflection->getName())) + ->identifier('property.readOnlyByPhpDocAssignOutOfClass') + ->build(); continue; } @@ -76,8 +80,10 @@ public function processNode(Node $node, Scope $scope): array in_array($scopeMethod->getName(), $this->constructorsHelper->getConstructors($scopeClassReflection), true) || strtolower($scopeMethod->getName()) === '__unserialize' ) { - if (!$scope->getType($propertyFetch->var) instanceof ThisType) { - $errors[] = RuleErrorBuilder::message(sprintf('@readonly property %s::$%s is not assigned on $this.', $declaringClass->getDisplayName(), $propertyReflection->getName()))->build(); + if (TypeUtils::findThisType($scope->getType($propertyFetch->var)) === null) { + $errors[] = RuleErrorBuilder::message(sprintf('@readonly property %s::$%s is not assigned on $this.', $declaringClass->getDisplayName(), $propertyReflection->getName())) + ->identifier('property.readOnlyByPhpDocAssignNotOnThis') + ->build(); } continue; @@ -87,7 +93,9 @@ public function processNode(Node $node, Scope $scope): array continue; } - $errors[] = RuleErrorBuilder::message(sprintf('@readonly property %s::$%s is assigned outside of the constructor.', $declaringClass->getDisplayName(), $propertyReflection->getName()))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('@readonly property %s::$%s is assigned outside of the constructor.', $declaringClass->getDisplayName(), $propertyReflection->getName())) + ->identifier('property.readOnlyByPhpDocAssignNotInConstructor') + ->build(); } return $errors; diff --git a/src/Rules/Properties/ReadOnlyByPhpDocPropertyRule.php b/src/Rules/Properties/ReadOnlyByPhpDocPropertyRule.php index b6f70ed756..a024d70b14 100644 --- a/src/Rules/Properties/ReadOnlyByPhpDocPropertyRule.php +++ b/src/Rules/Properties/ReadOnlyByPhpDocPropertyRule.php @@ -21,13 +21,15 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->isReadOnlyByPhpDoc() || $node->isReadOnly()) { + if (!($node->isReadOnlyByPhpDoc() && !$node->isAllowedPrivateMutation()) || $node->isReadOnly()) { return []; } $errors = []; if ($node->getDefault() !== null) { - $errors[] = RuleErrorBuilder::message('@readonly property cannot have a default value.')->build(); + $errors[] = RuleErrorBuilder::message('@readonly property cannot have a default value.') + ->identifier('property.readOnlyByPhpDocDefaultValue') + ->build(); } return $errors; diff --git a/src/Rules/Properties/ReadOnlyPropertyAssignRefRule.php b/src/Rules/Properties/ReadOnlyPropertyAssignRefRule.php index 97c5686c69..b50e887e3b 100644 --- a/src/Rules/Properties/ReadOnlyPropertyAssignRefRule.php +++ b/src/Rules/Properties/ReadOnlyPropertyAssignRefRule.php @@ -46,7 +46,9 @@ public function processNode(Node $node, Scope $scope): array } $declaringClass = $nativeReflection->getDeclaringClass(); - $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned by reference.', $declaringClass->getDisplayName(), $propertyReflection->getName()))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned by reference.', $declaringClass->getDisplayName(), $propertyReflection->getName())) + ->identifier('property.readOnlyAssignByRef') + ->build(); } return $errors; diff --git a/src/Rules/Properties/ReadOnlyPropertyAssignRule.php b/src/Rules/Properties/ReadOnlyPropertyAssignRule.php index 2cc255dd4a..47d132b639 100644 --- a/src/Rules/Properties/ReadOnlyPropertyAssignRule.php +++ b/src/Rules/Properties/ReadOnlyPropertyAssignRule.php @@ -10,7 +10,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\ThisType; +use PHPStan\Type\TypeUtils; use function in_array; use function sprintf; use function strtolower; @@ -57,13 +57,17 @@ public function processNode(Node $node, Scope $scope): array $declaringClass = $nativeReflection->getDeclaringClass(); if (!$scope->isInClass()) { - $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned outside of its declaring class.', $declaringClass->getDisplayName(), $propertyReflection->getName()))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned outside of its declaring class.', $declaringClass->getDisplayName(), $propertyReflection->getName())) + ->identifier('property.readOnlyAssignOutOfClass') + ->build(); continue; } $scopeClassReflection = $scope->getClassReflection(); if ($scopeClassReflection->getName() !== $declaringClass->getName()) { - $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned outside of its declaring class.', $declaringClass->getDisplayName(), $propertyReflection->getName()))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned outside of its declaring class.', $declaringClass->getDisplayName(), $propertyReflection->getName())) + ->identifier('property.readOnlyAssignOutOfClass') + ->build(); continue; } @@ -76,14 +80,18 @@ public function processNode(Node $node, Scope $scope): array in_array($scopeMethod->getName(), $this->constructorsHelper->getConstructors($scopeClassReflection), true) || strtolower($scopeMethod->getName()) === '__unserialize' ) { - if (!$scope->getType($propertyFetch->var) instanceof ThisType) { - $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is not assigned on $this.', $declaringClass->getDisplayName(), $propertyReflection->getName()))->build(); + if (TypeUtils::findThisType($scope->getType($propertyFetch->var)) === null) { + $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is not assigned on $this.', $declaringClass->getDisplayName(), $propertyReflection->getName())) + ->identifier('property.readOnlyAssignNotOnThis') + ->build(); } continue; } - $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned outside of the constructor.', $declaringClass->getDisplayName(), $propertyReflection->getName()))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned outside of the constructor.', $declaringClass->getDisplayName(), $propertyReflection->getName())) + ->identifier('property.readOnlyAssignNotInConstructor') + ->build(); } return $errors; diff --git a/src/Rules/Properties/ReadOnlyPropertyRule.php b/src/Rules/Properties/ReadOnlyPropertyRule.php index 64c72974d7..a622964ed5 100644 --- a/src/Rules/Properties/ReadOnlyPropertyRule.php +++ b/src/Rules/Properties/ReadOnlyPropertyRule.php @@ -32,19 +32,28 @@ public function processNode(Node $node, Scope $scope): array $errors = []; if (!$this->phpVersion->supportsReadOnlyProperties()) { - $errors[] = RuleErrorBuilder::message('Readonly properties are supported only on PHP 8.1 and later.')->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message('Readonly properties are supported only on PHP 8.1 and later.')->nonIgnorable() + ->identifier('property.readOnlyNotSupported') + ->build(); } if ($node->getNativeType() === null) { - $errors[] = RuleErrorBuilder::message('Readonly property must have a native type.')->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message('Readonly property must have a native type.') + ->identifier('property.readOnlyNoNativeType') + ->nonIgnorable() + ->build(); } if ($node->getDefault() !== null) { - $errors[] = RuleErrorBuilder::message('Readonly property cannot have a default value.')->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message('Readonly property cannot have a default value.')->nonIgnorable() + ->identifier('property.readOnlyDefaultValue') + ->build(); } if ($node->isStatic()) { - $errors[] = RuleErrorBuilder::message('Readonly property cannot be static.')->nonIgnorable()->build(); + $errors[] = RuleErrorBuilder::message('Readonly property cannot be static.')->nonIgnorable() + ->identifier('property.readOnlyStatic') + ->build(); } return $errors; diff --git a/src/Rules/Properties/ReadWritePropertiesExtension.php b/src/Rules/Properties/ReadWritePropertiesExtension.php index b8f26e60c7..1b1c695ef9 100644 --- a/src/Rules/Properties/ReadWritePropertiesExtension.php +++ b/src/Rules/Properties/ReadWritePropertiesExtension.php @@ -4,7 +4,24 @@ use PHPStan\Reflection\PropertyReflection; -/** @api */ +/** + * This is the extension interface to implement if you want to describe + * always-read or always-written properties. + * + * To register it in the configuration file use the `phpstan.properties.readWriteExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.properties.readWriteExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/always-read-written-properties + * + * @api + */ interface ReadWritePropertiesExtension { diff --git a/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php b/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php index 315be25535..5e326c12a8 100644 --- a/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php +++ b/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php @@ -59,13 +59,15 @@ public function processNode(Node $node, Scope $scope): array } if (!$propertyReflection->isReadable()) { - $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $node); + $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $node); return [ RuleErrorBuilder::message(sprintf( '%s is not readable.', $propertyDescription, - ))->build(), + )) + ->identifier('property.writeOnly') + ->build(), ]; } diff --git a/src/Rules/Properties/TypesAssignedToPropertiesRule.php b/src/Rules/Properties/TypesAssignedToPropertiesRule.php index e86865cb29..9e15b0b33f 100644 --- a/src/Rules/Properties/TypesAssignedToPropertiesRule.php +++ b/src/Rules/Properties/TypesAssignedToPropertiesRule.php @@ -5,8 +5,9 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\PropertyAssignNode; +use PHPStan\Reflection\PropertyReflection; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\VerbosityLevel; @@ -21,7 +22,6 @@ class TypesAssignedToPropertiesRule implements Rule public function __construct( private RuleLevelHelper $ruleLevelHelper, - private PropertyDescriptor $propertyDescriptor, private PropertyReflectionFinder $propertyReflectionFinder, ) { @@ -48,19 +48,24 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return RuleError[] + * @return list */ private function processSingleProperty( FoundPropertyReflection $propertyReflection, Node\Expr $assignedExpr, ): array { + if (!$propertyReflection->isWritable()) { + return []; + } + $propertyType = $propertyReflection->getWritableType(); $scope = $propertyReflection->getScope(); $assignedValueType = $scope->getType($assignedExpr); - if (!$this->ruleLevelHelper->accepts($propertyType, $assignedValueType, $scope->isDeclareStrictTypes())) { - $propertyDescription = $this->propertyDescriptor->describePropertyByName($propertyReflection, $propertyReflection->getName()); + $accepts = $this->ruleLevelHelper->acceptsWithReason($propertyType, $assignedValueType, $scope->isDeclareStrictTypes()); + if (!$accepts->result) { + $propertyDescription = $this->describePropertyByName($propertyReflection, $propertyReflection->getName()); $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($propertyType, $assignedValueType); return [ @@ -69,11 +74,23 @@ private function processSingleProperty( $propertyDescription, $propertyType->describe($verbosityLevel), $assignedValueType->describe($verbosityLevel), - ))->build(), + )) + ->identifier('assign.propertyType') + ->acceptsReasonsTip($accepts->reasons) + ->build(), ]; } return []; } + private function describePropertyByName(PropertyReflection $property, string $propertyName): string + { + if (!$property->isStatic()) { + return sprintf('Property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $propertyName); + } + + return sprintf('Static property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $propertyName); + } + } diff --git a/src/Rules/Properties/UninitializedPropertyRule.php b/src/Rules/Properties/UninitializedPropertyRule.php index 7131881959..a2995c3886 100644 --- a/src/Rules/Properties/UninitializedPropertyRule.php +++ b/src/Rules/Properties/UninitializedPropertyRule.php @@ -8,7 +8,6 @@ use PHPStan\Reflection\ConstructorsHelper; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use function sprintf; /** @@ -30,10 +29,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); [$properties, $prematureAccess] = $node->getUninitializedProperties($scope, $this->constructorsHelper->getConstructors($classReflection)); $errors = []; @@ -45,10 +41,13 @@ public function processNode(Node $node, Scope $scope): array 'Class %s has an uninitialized property $%s. Give it default value or assign it in the constructor.', $classReflection->getDisplayName(), $propertyName, - ))->line($propertyNode->getLine())->build(); + )) + ->line($propertyNode->getStartLine()) + ->identifier('property.uninitialized') + ->build(); } - foreach ($prematureAccess as [$propertyName, $line, $propertyNode]) { + foreach ($prematureAccess as [$propertyName, $line, $propertyNode, $file, $fileDescription]) { if ($propertyNode->isReadOnly() || $propertyNode->isReadOnlyByPhpDoc()) { continue; } @@ -56,7 +55,11 @@ public function processNode(Node $node, Scope $scope): array 'Access to an uninitialized property %s::$%s.', $classReflection->getDisplayName(), $propertyName, - ))->line($line)->build(); + )) + ->line($line) + ->file($file, $fileDescription) + ->identifier('property.uninitialized') + ->build(); } return $errors; diff --git a/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php b/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php index f25cd413c7..5acc86ba91 100644 --- a/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php +++ b/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php @@ -4,13 +4,14 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\PropertyAssignNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use function sprintf; /** - * @implements Rule + * @implements Rule */ class WritingToReadOnlyPropertiesRule implements Rule { @@ -26,36 +27,20 @@ public function __construct( public function getNodeType(): string { - return Node\Expr::class; + return PropertyAssignNode::class; } public function processNode(Node $node, Scope $scope): array { + $propertyFetch = $node->getPropertyFetch(); if ( - !$node instanceof Node\Expr\Assign - && !$node instanceof Node\Expr\AssignOp - && !$node instanceof Node\Expr\AssignRef - ) { - return []; - } - - if ( - !($node->var instanceof Node\Expr\PropertyFetch) - && !($node->var instanceof Node\Expr\StaticPropertyFetch) - ) { - return []; - } - - if ( - $node->var instanceof Node\Expr\PropertyFetch + $propertyFetch instanceof Node\Expr\PropertyFetch && $this->checkThisOnly - && !$this->ruleLevelHelper->isThis($node->var->var) + && !$this->ruleLevelHelper->isThis($propertyFetch->var) ) { return []; } - /** @var Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch */ - $propertyFetch = $node->var; $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyFetch, $scope); if ($propertyReflection === null) { return []; @@ -66,13 +51,13 @@ public function processNode(Node $node, Scope $scope): array } if (!$propertyReflection->isWritable()) { - $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $propertyFetch); + $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $propertyFetch); return [ RuleErrorBuilder::message(sprintf( '%s is not writable.', $propertyDescription, - ))->build(), + ))->identifier('assign.propertyReadOnly')->build(), ]; } diff --git a/src/Rules/Pure/FunctionPurityCheck.php b/src/Rules/Pure/FunctionPurityCheck.php new file mode 100644 index 0000000000..9bd7046986 --- /dev/null +++ b/src/Rules/Pure/FunctionPurityCheck.php @@ -0,0 +1,146 @@ + + */ + public function check( + string $functionDescription, + string $identifier, + FunctionReflection|ExtendedMethodReflection $functionReflection, + array $parameters, + Type $returnType, + array $impurePoints, + array $throwPoints, + array $statements, + ): array + { + $errors = []; + $isPure = $functionReflection->isPure(); + $isConstructor = false; + if ( + $functionReflection instanceof ExtendedMethodReflection + && $functionReflection->getDeclaringClass()->hasConstructor() + && $functionReflection->getDeclaringClass()->getConstructor()->getName() === $functionReflection->getName() + ) { + $isConstructor = true; + } + + if ($isPure->yes()) { + foreach ($parameters as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + '%s is marked as pure but parameter $%s is passed by reference.', + $functionDescription, + $parameter->getName(), + ))->identifier(sprintf('pure%s.parameterByRef', $identifier))->build(); + } + + if ($returnType->isVoid()->yes() && !$isConstructor) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s is marked as pure but returns void.', + $functionDescription, + ))->identifier(sprintf('pure%s.void', $identifier))->build(); + } + + foreach ($impurePoints as $impurePoint) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s %s in pure %s.', + $impurePoint->isCertain() ? 'Impure' : 'Possibly impure', + $impurePoint->getDescription(), + lcfirst($functionDescription), + )) + ->line($impurePoint->getNode()->getStartLine()) + ->identifier(sprintf( + '%s.%s', + $impurePoint->isCertain() ? 'impure' : 'possiblyImpure', + $impurePoint->getIdentifier(), + )) + ->build(); + } + } elseif ($isPure->no()) { + if (count($impurePoints) === 0) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s is marked as impure but does not have any side effects.', + $functionDescription, + ))->identifier(sprintf('impure%s.pure', $identifier))->build(); + } + } elseif ($returnType->isVoid()->yes()) { + if ( + count($throwPoints) === 0 + && count($impurePoints) === 0 + && !$isConstructor + && (!$functionReflection instanceof ExtendedMethodReflection || $functionReflection->isPrivate()) + && count($functionReflection->getAsserts()->getAll()) === 0 + ) { + $hasByRef = false; + foreach ($parameters as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + $hasByRef = true; + break; + } + + $statements = array_filter($statements, static function (Stmt $stmt): bool { + if ($stmt instanceof Stmt\Nop) { + return false; + } + + if (!$stmt instanceof Stmt\Expression) { + return true; + } + if (!$stmt->expr instanceof FuncCall) { + return true; + } + if (!$stmt->expr->name instanceof Name) { + return true; + } + + return !in_array($stmt->expr->name->toString(), CallToFunctionStatementWithoutSideEffectsRule::PHPSTAN_TESTING_FUNCTIONS, true); + }); + + if (!$hasByRef && count($statements) > 0) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s returns void but does not have any side effects.', + $functionDescription, + ))->identifier('void.pure')->build(); + } + } + } + + return $errors; + } + +} diff --git a/src/Rules/Pure/PureFunctionRule.php b/src/Rules/Pure/PureFunctionRule.php new file mode 100644 index 0000000000..838e9bc67b --- /dev/null +++ b/src/Rules/Pure/PureFunctionRule.php @@ -0,0 +1,44 @@ + + */ +class PureFunctionRule implements Rule +{ + + public function __construct(private FunctionPurityCheck $check) + { + } + + public function getNodeType(): string + { + return FunctionReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $function = $node->getFunctionReflection(); + $variant = ParametersAcceptorSelector::selectSingle($function->getVariants()); + + return $this->check->check( + sprintf('Function %s()', $function->getName()), + 'Function', + $function, + $variant->getParameters(), + $variant->getReturnType(), + $node->getImpurePoints(), + $node->getStatementResult()->getThrowPoints(), + $node->getStatements(), + ); + } + +} diff --git a/src/Rules/Pure/PureMethodRule.php b/src/Rules/Pure/PureMethodRule.php new file mode 100644 index 0000000000..a023720118 --- /dev/null +++ b/src/Rules/Pure/PureMethodRule.php @@ -0,0 +1,44 @@ + + */ +class PureMethodRule implements Rule +{ + + public function __construct(private FunctionPurityCheck $check) + { + } + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $method = $node->getMethodReflection(); + $variant = ParametersAcceptorSelector::selectSingle($method->getVariants()); + + return $this->check->check( + sprintf('Method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName()), + 'Method', + $method, + $variant->getParameters(), + $variant->getReturnType(), + $node->getImpurePoints(), + $node->getStatementResult()->getThrowPoints(), + $node->getStatements(), + ); + } + +} diff --git a/src/Rules/Regexp/RegularExpressionPatternRule.php b/src/Rules/Regexp/RegularExpressionPatternRule.php index 3a6155ecc5..df4f74b67d 100644 --- a/src/Rules/Regexp/RegularExpressionPatternRule.php +++ b/src/Rules/Regexp/RegularExpressionPatternRule.php @@ -9,8 +9,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\TypeUtils; use function in_array; use function sprintf; use function str_starts_with; @@ -38,7 +36,7 @@ public function processNode(Node $node, Scope $scope): array continue; } - $errors[] = RuleErrorBuilder::message(sprintf('Regex pattern is invalid: %s', $errorMessage))->build(); + $errors[] = RuleErrorBuilder::message(sprintf('Regex pattern is invalid: %s', $errorMessage))->identifier('regexp.pattern')->build(); } return $errors; @@ -65,7 +63,7 @@ private function extractPatterns(FuncCall $functionCall, Scope $scope): array $patternStrings = []; - foreach (TypeUtils::getConstantStrings($patternType) as $constantStringType) { + foreach ($patternType->getConstantStrings() as $constantStringType) { if ( !in_array($functionName, [ 'preg_match', @@ -83,7 +81,7 @@ private function extractPatterns(FuncCall $functionCall, Scope $scope): array $patternStrings[] = $constantStringType->getValue(); } - foreach (TypeUtils::getOldConstantArrays($patternType) as $constantArrayType) { + foreach ($patternType->getConstantArrays() as $constantArrayType) { if ( in_array($functionName, [ 'preg_replace', @@ -92,11 +90,9 @@ private function extractPatterns(FuncCall $functionCall, Scope $scope): array ], true) ) { foreach ($constantArrayType->getValueTypes() as $arrayKeyType) { - if (!$arrayKeyType instanceof ConstantStringType) { - continue; + foreach ($arrayKeyType->getConstantStrings() as $constantString) { + $patternStrings[] = $constantString->getValue(); } - - $patternStrings[] = $arrayKeyType->getValue(); } } @@ -105,11 +101,9 @@ private function extractPatterns(FuncCall $functionCall, Scope $scope): array } foreach ($constantArrayType->getKeyTypes() as $arrayKeyType) { - if (!$arrayKeyType instanceof ConstantStringType) { - continue; + foreach ($arrayKeyType->getConstantStrings() as $constantString) { + $patternStrings[] = $constantString->getValue(); } - - $patternStrings[] = $arrayKeyType->getValue(); } } diff --git a/src/Rules/Rule.php b/src/Rules/Rule.php index 8f351cc4e9..e145e4b530 100644 --- a/src/Rules/Rule.php +++ b/src/Rules/Rule.php @@ -6,6 +6,19 @@ use PHPStan\Analyser\Scope; /** + * This is the interface custom rules implement. To register it in the configuration file + * use the `phpstan.rules.rule` service tag: + * + * ``` + * services: + * - + * class: App\MyRule + * tags: + * - phpstan.rules.rule + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/rules + * * @api * @phpstan-template TNodeType of Node */ diff --git a/src/Rules/RuleError.php b/src/Rules/RuleError.php index 6fadd1cd7e..ee2e0f5f6d 100644 --- a/src/Rules/RuleError.php +++ b/src/Rules/RuleError.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules; +/** @api */ interface RuleError { diff --git a/src/Rules/RuleErrorBuilder.php b/src/Rules/RuleErrorBuilder.php index 091ffbf9db..9a99e89636 100644 --- a/src/Rules/RuleErrorBuilder.php +++ b/src/Rules/RuleErrorBuilder.php @@ -2,11 +2,19 @@ namespace PHPStan\Rules; +use PHPStan\Analyser\Error; use PHPStan\ShouldNotHappenException; +use function array_map; use function class_exists; +use function count; +use function implode; +use function is_file; use function sprintf; -/** @api */ +/** + * @api + * @template-covariant T of RuleError + */ class RuleErrorBuilder { @@ -23,6 +31,9 @@ class RuleErrorBuilder /** @var mixed[] */ private array $properties; + /** @var list */ + private array $tips = []; + private function __construct(string $message) { $this->properties['message'] = $message; @@ -30,61 +41,95 @@ private function __construct(string $message) } /** - * @return array + * @return array}> */ public static function getRuleErrorTypes(): array { return [ self::TYPE_MESSAGE => [ RuleError::class, - 'message', - 'string', - 'string', + [ + [ + 'message', + 'string', + 'string', + ], + ], ], self::TYPE_LINE => [ LineRuleError::class, - 'line', - 'int', - 'int', + [ + [ + 'line', + 'int', + 'int', + ], + ], ], self::TYPE_FILE => [ FileRuleError::class, - 'file', - 'string', - 'string', + [ + [ + 'file', + 'string', + 'string', + ], + [ + 'fileDescription', + 'string', + 'string', + ], + ], ], self::TYPE_TIP => [ TipRuleError::class, - 'tip', - 'string', - 'string', + [ + [ + 'tip', + 'string', + 'string', + ], + ], ], self::TYPE_IDENTIFIER => [ IdentifierRuleError::class, - 'identifier', - 'string', - 'string', + [ + [ + 'identifier', + 'string', + 'string', + ], + ], ], self::TYPE_METADATA => [ MetadataRuleError::class, - 'metadata', - 'array', - 'mixed[]', + [ + [ + 'metadata', + 'array', + 'mixed[]', + ], + ], ], self::TYPE_NON_IGNORABLE => [ NonIgnorableRuleError::class, - null, - null, - null, + [], ], ]; } + /** + * @return self + */ public static function message(string $message): self { return new self($message); } + /** + * @phpstan-this-out self + * @return self + */ public function line(int $line): self { $this->properties['line'] = $line; @@ -93,29 +138,92 @@ public function line(int $line): self return $this; } - public function file(string $file): self + /** + * @phpstan-this-out self + * @return self + */ + public function file(string $file, ?string $fileDescription = null): self { + if (!is_file($file)) { + throw new ShouldNotHappenException(sprintf('File %s does not exist.', $file)); + } $this->properties['file'] = $file; + $this->properties['fileDescription'] = $fileDescription ?? $file; $this->type |= self::TYPE_FILE; return $this; } + /** + * @phpstan-this-out self + * @return self + */ public function tip(string $tip): self { - $this->properties['tip'] = $tip; + $this->tips = [$tip]; + $this->type |= self::TYPE_TIP; + + return $this; + } + + /** + * @phpstan-this-out self + * @return self + */ + public function addTip(string $tip): self + { + $this->tips[] = $tip; $this->type |= self::TYPE_TIP; return $this; } + /** + * @phpstan-this-out self + * @return self + */ public function discoveringSymbolsTip(): self { return $this->tip('Learn more at https://phpstan.org/user-guide/discovering-symbols'); } + /** + * @param list $reasons + * @phpstan-this-out self + * @return self + */ + public function acceptsReasonsTip(array $reasons): self + { + foreach ($reasons as $reason) { + $this->addTip($reason); + } + + return $this; + } + + /** + * @phpstan-this-out self + * @return self + */ + public function treatPhpDocTypesAsCertainTip(): self + { + return $this->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + } + + /** + * Sets an error identifier. + * + * List of all current error identifiers in PHPStan: https://phpstan.org/error-identifiers + * + * @phpstan-this-out self + * @return self + */ public function identifier(string $identifier): self { + if (!Error::validateIdentifier($identifier)) { + throw new ShouldNotHappenException(sprintf('Invalid identifier: %s', $identifier)); + } + $this->properties['identifier'] = $identifier; $this->type |= self::TYPE_IDENTIFIER; @@ -124,6 +232,8 @@ public function identifier(string $identifier): self /** * @param mixed[] $metadata + * @phpstan-this-out self + * @return self */ public function metadata(array $metadata): self { @@ -133,6 +243,10 @@ public function metadata(array $metadata): self return $this; } + /** + * @phpstan-this-out self + * @return self + */ public function nonIgnorable(): self { $this->type |= self::TYPE_NON_IGNORABLE; @@ -140,9 +254,12 @@ public function nonIgnorable(): self return $this; } + /** + * @return T + */ public function build(): RuleError { - /** @var class-string $className */ + /** @var class-string $className */ $className = sprintf('PHPStan\\Rules\\RuleErrors\\RuleError%d', $this->type); if (!class_exists($className)) { throw new ShouldNotHappenException(sprintf('Class %s does not exist.', $className)); @@ -153,6 +270,14 @@ public function build(): RuleError $ruleError->{$propertyName} = $value; } + if (count($this->tips) > 0) { + if (count($this->tips) === 1) { + $ruleError->tip = $this->tips[0]; + } else { + $ruleError->tip = implode("\n", array_map(static fn (string $tip) => sprintf('• %s', $tip), $this->tips)); + } + } + return $ruleError; } diff --git a/src/Rules/RuleErrors/RuleError101.php b/src/Rules/RuleErrors/RuleError101.php index a4c08ae140..cd6a40afe3 100644 --- a/src/Rules/RuleErrors/RuleError101.php +++ b/src/Rules/RuleErrors/RuleError101.php @@ -17,6 +17,8 @@ class RuleError101 implements RuleError, FileRuleError, MetadataRuleError, NonIg public string $file; + public string $fileDescription; + /** @var mixed[] */ public array $metadata; @@ -30,6 +32,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + /** * @return mixed[] */ diff --git a/src/Rules/RuleErrors/RuleError103.php b/src/Rules/RuleErrors/RuleError103.php index 04c4ae5083..a88780c212 100644 --- a/src/Rules/RuleErrors/RuleError103.php +++ b/src/Rules/RuleErrors/RuleError103.php @@ -20,6 +20,8 @@ class RuleError103 implements RuleError, LineRuleError, FileRuleError, MetadataR public string $file; + public string $fileDescription; + /** @var mixed[] */ public array $metadata; @@ -38,6 +40,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + /** * @return mixed[] */ diff --git a/src/Rules/RuleErrors/RuleError109.php b/src/Rules/RuleErrors/RuleError109.php index 64d81d29db..22ddff6f25 100644 --- a/src/Rules/RuleErrors/RuleError109.php +++ b/src/Rules/RuleErrors/RuleError109.php @@ -18,6 +18,8 @@ class RuleError109 implements RuleError, FileRuleError, TipRuleError, MetadataRu public string $file; + public string $fileDescription; + public string $tip; /** @var mixed[] */ @@ -33,6 +35,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError111.php b/src/Rules/RuleErrors/RuleError111.php index f3ad512dbc..3024d5fdf6 100644 --- a/src/Rules/RuleErrors/RuleError111.php +++ b/src/Rules/RuleErrors/RuleError111.php @@ -21,6 +21,8 @@ class RuleError111 implements RuleError, LineRuleError, FileRuleError, TipRuleEr public string $file; + public string $fileDescription; + public string $tip; /** @var mixed[] */ @@ -41,6 +43,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError117.php b/src/Rules/RuleErrors/RuleError117.php index d46b42784f..2492802799 100644 --- a/src/Rules/RuleErrors/RuleError117.php +++ b/src/Rules/RuleErrors/RuleError117.php @@ -18,6 +18,8 @@ class RuleError117 implements RuleError, FileRuleError, IdentifierRuleError, Met public string $file; + public string $fileDescription; + public string $identifier; /** @var mixed[] */ @@ -33,6 +35,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getIdentifier(): string { return $this->identifier; diff --git a/src/Rules/RuleErrors/RuleError119.php b/src/Rules/RuleErrors/RuleError119.php index 67ed0183e6..6d6fb6b7a9 100644 --- a/src/Rules/RuleErrors/RuleError119.php +++ b/src/Rules/RuleErrors/RuleError119.php @@ -21,6 +21,8 @@ class RuleError119 implements RuleError, LineRuleError, FileRuleError, Identifie public string $file; + public string $fileDescription; + public string $identifier; /** @var mixed[] */ @@ -41,6 +43,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getIdentifier(): string { return $this->identifier; diff --git a/src/Rules/RuleErrors/RuleError125.php b/src/Rules/RuleErrors/RuleError125.php index b8d1602fef..d64be7de95 100644 --- a/src/Rules/RuleErrors/RuleError125.php +++ b/src/Rules/RuleErrors/RuleError125.php @@ -19,6 +19,8 @@ class RuleError125 implements RuleError, FileRuleError, TipRuleError, Identifier public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -36,6 +38,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError127.php b/src/Rules/RuleErrors/RuleError127.php index a32ffdb849..dfb94ecc15 100644 --- a/src/Rules/RuleErrors/RuleError127.php +++ b/src/Rules/RuleErrors/RuleError127.php @@ -22,6 +22,8 @@ class RuleError127 implements RuleError, LineRuleError, FileRuleError, TipRuleEr public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -44,6 +46,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError13.php b/src/Rules/RuleErrors/RuleError13.php index 51568e83da..3ecb8d6233 100644 --- a/src/Rules/RuleErrors/RuleError13.php +++ b/src/Rules/RuleErrors/RuleError13.php @@ -16,6 +16,8 @@ class RuleError13 implements RuleError, FileRuleError, TipRuleError public string $file; + public string $fileDescription; + public string $tip; public function getMessage(): string @@ -28,6 +30,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError15.php b/src/Rules/RuleErrors/RuleError15.php index 9c57396605..956f7fe0f0 100644 --- a/src/Rules/RuleErrors/RuleError15.php +++ b/src/Rules/RuleErrors/RuleError15.php @@ -19,6 +19,8 @@ class RuleError15 implements RuleError, LineRuleError, FileRuleError, TipRuleErr public string $file; + public string $fileDescription; + public string $tip; public function getMessage(): string @@ -36,6 +38,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError21.php b/src/Rules/RuleErrors/RuleError21.php index b1bb82ca7a..3a6f7eb2d3 100644 --- a/src/Rules/RuleErrors/RuleError21.php +++ b/src/Rules/RuleErrors/RuleError21.php @@ -16,6 +16,8 @@ class RuleError21 implements RuleError, FileRuleError, IdentifierRuleError public string $file; + public string $fileDescription; + public string $identifier; public function getMessage(): string @@ -28,6 +30,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getIdentifier(): string { return $this->identifier; diff --git a/src/Rules/RuleErrors/RuleError23.php b/src/Rules/RuleErrors/RuleError23.php index 1aa68678de..911a7a05fe 100644 --- a/src/Rules/RuleErrors/RuleError23.php +++ b/src/Rules/RuleErrors/RuleError23.php @@ -19,6 +19,8 @@ class RuleError23 implements RuleError, LineRuleError, FileRuleError, Identifier public string $file; + public string $fileDescription; + public string $identifier; public function getMessage(): string @@ -36,6 +38,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getIdentifier(): string { return $this->identifier; diff --git a/src/Rules/RuleErrors/RuleError29.php b/src/Rules/RuleErrors/RuleError29.php index 9f10ef1f20..68f71ae5c7 100644 --- a/src/Rules/RuleErrors/RuleError29.php +++ b/src/Rules/RuleErrors/RuleError29.php @@ -17,6 +17,8 @@ class RuleError29 implements RuleError, FileRuleError, TipRuleError, IdentifierR public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -31,6 +33,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError31.php b/src/Rules/RuleErrors/RuleError31.php index 2df2e100c5..6402df1887 100644 --- a/src/Rules/RuleErrors/RuleError31.php +++ b/src/Rules/RuleErrors/RuleError31.php @@ -20,6 +20,8 @@ class RuleError31 implements RuleError, LineRuleError, FileRuleError, TipRuleErr public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -39,6 +41,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError37.php b/src/Rules/RuleErrors/RuleError37.php index ae5adf983c..fe0d25a3f5 100644 --- a/src/Rules/RuleErrors/RuleError37.php +++ b/src/Rules/RuleErrors/RuleError37.php @@ -16,6 +16,8 @@ class RuleError37 implements RuleError, FileRuleError, MetadataRuleError public string $file; + public string $fileDescription; + /** @var mixed[] */ public array $metadata; @@ -29,6 +31,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + /** * @return mixed[] */ diff --git a/src/Rules/RuleErrors/RuleError39.php b/src/Rules/RuleErrors/RuleError39.php index a3699d7c68..1a50b299fc 100644 --- a/src/Rules/RuleErrors/RuleError39.php +++ b/src/Rules/RuleErrors/RuleError39.php @@ -19,6 +19,8 @@ class RuleError39 implements RuleError, LineRuleError, FileRuleError, MetadataRu public string $file; + public string $fileDescription; + /** @var mixed[] */ public array $metadata; @@ -37,6 +39,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + /** * @return mixed[] */ diff --git a/src/Rules/RuleErrors/RuleError45.php b/src/Rules/RuleErrors/RuleError45.php index b81bfcd3b7..d77f882c50 100644 --- a/src/Rules/RuleErrors/RuleError45.php +++ b/src/Rules/RuleErrors/RuleError45.php @@ -17,6 +17,8 @@ class RuleError45 implements RuleError, FileRuleError, TipRuleError, MetadataRul public string $file; + public string $fileDescription; + public string $tip; /** @var mixed[] */ @@ -32,6 +34,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError47.php b/src/Rules/RuleErrors/RuleError47.php index d4f0af6e2a..3a1158c176 100644 --- a/src/Rules/RuleErrors/RuleError47.php +++ b/src/Rules/RuleErrors/RuleError47.php @@ -20,6 +20,8 @@ class RuleError47 implements RuleError, LineRuleError, FileRuleError, TipRuleErr public string $file; + public string $fileDescription; + public string $tip; /** @var mixed[] */ @@ -40,6 +42,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError5.php b/src/Rules/RuleErrors/RuleError5.php index 58f47a9053..f8205fd24f 100644 --- a/src/Rules/RuleErrors/RuleError5.php +++ b/src/Rules/RuleErrors/RuleError5.php @@ -15,6 +15,8 @@ class RuleError5 implements RuleError, FileRuleError public string $file; + public string $fileDescription; + public function getMessage(): string { return $this->message; @@ -25,4 +27,9 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + } diff --git a/src/Rules/RuleErrors/RuleError53.php b/src/Rules/RuleErrors/RuleError53.php index e11bdaf0cb..cd8418f5b2 100644 --- a/src/Rules/RuleErrors/RuleError53.php +++ b/src/Rules/RuleErrors/RuleError53.php @@ -17,6 +17,8 @@ class RuleError53 implements RuleError, FileRuleError, IdentifierRuleError, Meta public string $file; + public string $fileDescription; + public string $identifier; /** @var mixed[] */ @@ -32,6 +34,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getIdentifier(): string { return $this->identifier; diff --git a/src/Rules/RuleErrors/RuleError55.php b/src/Rules/RuleErrors/RuleError55.php index 3b9aefc997..4eb281839d 100644 --- a/src/Rules/RuleErrors/RuleError55.php +++ b/src/Rules/RuleErrors/RuleError55.php @@ -20,6 +20,8 @@ class RuleError55 implements RuleError, LineRuleError, FileRuleError, Identifier public string $file; + public string $fileDescription; + public string $identifier; /** @var mixed[] */ @@ -40,6 +42,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getIdentifier(): string { return $this->identifier; diff --git a/src/Rules/RuleErrors/RuleError61.php b/src/Rules/RuleErrors/RuleError61.php index a263eafb22..a861ab2f51 100644 --- a/src/Rules/RuleErrors/RuleError61.php +++ b/src/Rules/RuleErrors/RuleError61.php @@ -18,6 +18,8 @@ class RuleError61 implements RuleError, FileRuleError, TipRuleError, IdentifierR public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -35,6 +37,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError63.php b/src/Rules/RuleErrors/RuleError63.php index 80a680ec0d..919587a216 100644 --- a/src/Rules/RuleErrors/RuleError63.php +++ b/src/Rules/RuleErrors/RuleError63.php @@ -21,6 +21,8 @@ class RuleError63 implements RuleError, LineRuleError, FileRuleError, TipRuleErr public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -43,6 +45,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError69.php b/src/Rules/RuleErrors/RuleError69.php index afa9983d5d..75cd512c3e 100644 --- a/src/Rules/RuleErrors/RuleError69.php +++ b/src/Rules/RuleErrors/RuleError69.php @@ -16,6 +16,8 @@ class RuleError69 implements RuleError, FileRuleError, NonIgnorableRuleError public string $file; + public string $fileDescription; + public function getMessage(): string { return $this->message; @@ -26,4 +28,9 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + } diff --git a/src/Rules/RuleErrors/RuleError7.php b/src/Rules/RuleErrors/RuleError7.php index 4ce7c6b45e..af9559cfaa 100644 --- a/src/Rules/RuleErrors/RuleError7.php +++ b/src/Rules/RuleErrors/RuleError7.php @@ -18,6 +18,8 @@ class RuleError7 implements RuleError, LineRuleError, FileRuleError public string $file; + public string $fileDescription; + public function getMessage(): string { return $this->message; @@ -33,4 +35,9 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + } diff --git a/src/Rules/RuleErrors/RuleError71.php b/src/Rules/RuleErrors/RuleError71.php index 06db73390a..652b0f1922 100644 --- a/src/Rules/RuleErrors/RuleError71.php +++ b/src/Rules/RuleErrors/RuleError71.php @@ -19,6 +19,8 @@ class RuleError71 implements RuleError, LineRuleError, FileRuleError, NonIgnorab public string $file; + public string $fileDescription; + public function getMessage(): string { return $this->message; @@ -34,4 +36,9 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + } diff --git a/src/Rules/RuleErrors/RuleError77.php b/src/Rules/RuleErrors/RuleError77.php index 12a34ab040..09edd26a3d 100644 --- a/src/Rules/RuleErrors/RuleError77.php +++ b/src/Rules/RuleErrors/RuleError77.php @@ -17,6 +17,8 @@ class RuleError77 implements RuleError, FileRuleError, TipRuleError, NonIgnorabl public string $file; + public string $fileDescription; + public string $tip; public function getMessage(): string @@ -29,6 +31,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError79.php b/src/Rules/RuleErrors/RuleError79.php index ac103ea3f5..3c1fcf4d23 100644 --- a/src/Rules/RuleErrors/RuleError79.php +++ b/src/Rules/RuleErrors/RuleError79.php @@ -20,6 +20,8 @@ class RuleError79 implements RuleError, LineRuleError, FileRuleError, TipRuleErr public string $file; + public string $fileDescription; + public string $tip; public function getMessage(): string @@ -37,6 +39,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError85.php b/src/Rules/RuleErrors/RuleError85.php index 48a2c071f1..a0d13d45de 100644 --- a/src/Rules/RuleErrors/RuleError85.php +++ b/src/Rules/RuleErrors/RuleError85.php @@ -17,6 +17,8 @@ class RuleError85 implements RuleError, FileRuleError, IdentifierRuleError, NonI public string $file; + public string $fileDescription; + public string $identifier; public function getMessage(): string @@ -29,6 +31,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getIdentifier(): string { return $this->identifier; diff --git a/src/Rules/RuleErrors/RuleError87.php b/src/Rules/RuleErrors/RuleError87.php index 04ccf29f73..386b844a1b 100644 --- a/src/Rules/RuleErrors/RuleError87.php +++ b/src/Rules/RuleErrors/RuleError87.php @@ -20,6 +20,8 @@ class RuleError87 implements RuleError, LineRuleError, FileRuleError, Identifier public string $file; + public string $fileDescription; + public string $identifier; public function getMessage(): string @@ -37,6 +39,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getIdentifier(): string { return $this->identifier; diff --git a/src/Rules/RuleErrors/RuleError93.php b/src/Rules/RuleErrors/RuleError93.php index a9cdb69c6e..88b7282eb2 100644 --- a/src/Rules/RuleErrors/RuleError93.php +++ b/src/Rules/RuleErrors/RuleError93.php @@ -18,6 +18,8 @@ class RuleError93 implements RuleError, FileRuleError, TipRuleError, IdentifierR public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -32,6 +34,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleErrors/RuleError95.php b/src/Rules/RuleErrors/RuleError95.php index 117c4b574d..0fbb2a635b 100644 --- a/src/Rules/RuleErrors/RuleError95.php +++ b/src/Rules/RuleErrors/RuleError95.php @@ -21,6 +21,8 @@ class RuleError95 implements RuleError, LineRuleError, FileRuleError, TipRuleErr public string $file; + public string $fileDescription; + public string $tip; public string $identifier; @@ -40,6 +42,11 @@ public function getFile(): string return $this->file; } + public function getFileDescription(): string + { + return $this->fileDescription; + } + public function getTip(): string { return $this->tip; diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index cba600b029..f3076392f6 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -6,8 +6,11 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\CallableType; +use PHPStan\Type\ClosureType; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateMixedType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; @@ -20,9 +23,10 @@ use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function array_merge; use function count; use function sprintf; -use function strpos; +use function str_contains; class RuleLevelHelper { @@ -34,6 +38,8 @@ public function __construct( private bool $checkUnionTypes, private bool $checkExplicitMixed, private bool $checkImplicitMixed, + private bool $newRuleLevelHelper, + private bool $checkBenevolentUnionTypes, ) { } @@ -47,6 +53,131 @@ public function isThis(Expr $expression): bool /** @api */ public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTypes): bool { + return $this->acceptsWithReason($acceptingType, $acceptedType, $strictTypes)->result; + } + + private function transformCommonType(Type $type): Type + { + if (!$this->checkExplicitMixed && !$this->checkImplicitMixed) { + return $type; + } + + return TypeTraverser::map($type, function (Type $type, callable $traverse) { + if ($type instanceof TemplateMixedType) { + if (!$this->newRuleLevelHelper) { + return $type->toStrictMixedType(); + } + + if ($this->checkExplicitMixed) { + return $type->toStrictMixedType(); + } + } + if ( + $type instanceof MixedType + && ( + ($type->isExplicitMixed() && $this->checkExplicitMixed) + || (!$type->isExplicitMixed() && $this->checkImplicitMixed) + ) + ) { + return new StrictMixedType(); + } + + return $traverse($type); + }); + } + + /** + * @return array{Type, bool} + */ + private function transformAcceptedType(Type $acceptingType, Type $acceptedType): array + { + $checkForUnion = $this->checkUnionTypes; + $acceptedType = TypeTraverser::map($acceptedType, function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type { + if ($acceptedType instanceof CallableType) { + if ($acceptedType->isCommonCallable()) { + return $acceptedType; + } + + return new CallableType( + $acceptedType->getParameters(), + $traverse($this->transformCommonType($acceptedType->getReturnType())), + $acceptedType->isVariadic(), + $acceptedType->getTemplateTypeMap(), + $acceptedType->getResolvedTemplateTypeMap(), + $acceptedType->getTemplateTags(), + $acceptedType->isPure(), + ); + } + + if ($acceptedType instanceof ClosureType) { + if ($acceptedType->isCommonCallable()) { + return $acceptedType; + } + + return new ClosureType( + $acceptedType->getParameters(), + $traverse($this->transformCommonType($acceptedType->getReturnType())), + $acceptedType->isVariadic(), + $acceptedType->getTemplateTypeMap(), + $acceptedType->getResolvedTemplateTypeMap(), + $acceptedType->getCallSiteVarianceMap(), + $acceptedType->getTemplateTags(), + $acceptedType->getThrowPoints(), + $acceptedType->getImpurePoints(), + ); + } + + if ( + !$this->checkNullables + && !$acceptingType instanceof NullType + && !$acceptedType instanceof NullType + && !$acceptedType instanceof BenevolentUnionType + ) { + return $traverse(TypeCombinator::removeNull($acceptedType)); + } + + if ($this->checkBenevolentUnionTypes) { + if ($acceptedType instanceof BenevolentUnionType) { + $checkForUnion = true; + return $traverse(TypeUtils::toStrictUnion($acceptedType)); + } + } + + return $traverse($this->transformCommonType($acceptedType)); + }); + + return [$acceptedType, $checkForUnion]; + } + + public function acceptsWithReason(Type $acceptingType, Type $acceptedType, bool $strictTypes): RuleLevelHelperAcceptsResult + { + if ($this->newRuleLevelHelper) { + [$acceptedType, $checkForUnion] = $this->transformAcceptedType($acceptingType, $acceptedType); + $acceptingType = $this->transformCommonType($acceptingType); + + $accepts = $acceptingType->acceptsWithReason($acceptedType, $strictTypes); + + return new RuleLevelHelperAcceptsResult( + $checkForUnion ? $accepts->yes() : !$accepts->no(), + $accepts->reasons, + ); + } + + $checkForUnion = $this->checkUnionTypes; + + if ($this->checkBenevolentUnionTypes) { + $traverse = static function (Type $type, callable $traverse) use (&$checkForUnion): Type { + if ($type instanceof BenevolentUnionType) { + $checkForUnion = true; + return TypeUtils::toStrictUnion($type); + } + + return $traverse($type); + }; + + $acceptedType = TypeTraverser::map($acceptedType, $traverse); + } + if ( $this->checkExplicitMixed ) { @@ -96,36 +227,77 @@ public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTyp $acceptedType = TypeCombinator::removeNull($acceptedType); } - $accepts = $acceptingType->accepts($acceptedType, $strictTypes); - if (!$accepts->yes() && $acceptingType instanceof UnionType) { + $accepts = $acceptingType->acceptsWithReason($acceptedType, $strictTypes); + if ($accepts->yes()) { + return new RuleLevelHelperAcceptsResult(true, $accepts->reasons); + } + if ($acceptingType instanceof UnionType) { + $reasons = []; foreach ($acceptingType->getTypes() as $innerType) { - if (self::accepts($innerType, $acceptedType, $strictTypes)) { - return true; + $accepts = self::acceptsWithReason($innerType, $acceptedType, $strictTypes); + if ($accepts->result) { + return $accepts; } + + $reasons = array_merge($reasons, $accepts->reasons); } - return false; + return new RuleLevelHelperAcceptsResult(false, $reasons); } if ( $acceptedType->isArray()->yes() && $acceptingType->isArray()->yes() - && !$acceptingType->isIterableAtLeastOnce()->yes() - && count(TypeUtils::getOldConstantArrays($acceptedType)) === 0 - && count(TypeUtils::getOldConstantArrays($acceptingType)) === 0 + && ( + $acceptedType->isConstantArray()->no() + || !$acceptedType->isIterableAtLeastOnce()->no() + ) + && $acceptingType->isConstantArray()->no() ) { - return self::accepts( + if ($acceptingType->isIterableAtLeastOnce()->yes() && !$acceptedType->isIterableAtLeastOnce()->yes()) { + $verbosity = VerbosityLevel::getRecommendedLevelByType($acceptingType, $acceptedType); + return new RuleLevelHelperAcceptsResult(false, [ + sprintf( + '%s %s empty.', + $acceptedType->describe($verbosity), + $acceptedType->isIterableAtLeastOnce()->no() ? 'is' : 'might be', + ), + ]); + } + + if ( + $acceptingType->isList()->yes() + && !$acceptedType->isList()->yes() + ) { + $report = $checkForUnion || $acceptedType->isList()->no(); + + if ($report) { + $verbosity = VerbosityLevel::getRecommendedLevelByType($acceptingType, $acceptedType); + return new RuleLevelHelperAcceptsResult(false, [ + sprintf( + '%s %s a list.', + $acceptedType->describe($verbosity), + $acceptedType->isList()->no() ? 'is not' : 'might not be', + ), + ]); + } + } + + return self::acceptsWithReason( $acceptingType->getIterableKeyType(), $acceptedType->getIterableKeyType(), $strictTypes, - ) && self::accepts( + )->and(self::acceptsWithReason( $acceptingType->getIterableValueType(), $acceptedType->getIterableValueType(), $strictTypes, - ); + )); } - return $this->checkUnionTypes ? $accepts->yes() : !$accepts->no(); + return new RuleLevelHelperAcceptsResult( + $checkForUnion ? $accepts->yes() : !$accepts->no(), + $accepts->reasons, + ); } /** @@ -143,64 +315,162 @@ public function findTypeToCheck( return new FoundTypeResult(new ErrorType(), [], [], null); } $type = $scope->getType($var); - if (!$this->checkNullables && !$type instanceof NullType) { + + return $this->findTypeToCheckImplementation($scope, $var, $type, $unknownClassErrorPattern, $unionTypeCriteriaCallback, true); + } + + /** @param callable(Type $type): bool $unionTypeCriteriaCallback */ + private function findTypeToCheckImplementation( + Scope $scope, + Expr $var, + Type $type, + string $unknownClassErrorPattern, + callable $unionTypeCriteriaCallback, + bool $isTopLevel = false, + ): FoundTypeResult + { + if (!$this->checkNullables && !$type->isNull()->yes()) { $type = TypeCombinator::removeNull($type); } - if ( - $this->checkExplicitMixed - && $type instanceof MixedType - && !$type instanceof TemplateMixedType - && $type->isExplicitMixed() - ) { - return new FoundTypeResult(new StrictMixedType(), [], [], null); - } + if ($this->newRuleLevelHelper) { + if ( + ($this->checkExplicitMixed || $this->checkImplicitMixed) + && $type instanceof MixedType + && ($type->isExplicitMixed() ? $this->checkExplicitMixed : $this->checkImplicitMixed) + ) { + return new FoundTypeResult( + $type instanceof TemplateMixedType + ? $type->toStrictMixedType() + : new StrictMixedType(), + [], + [], + null, + ); + } + } else { + if ( + $this->checkExplicitMixed + && $type instanceof MixedType + && !$type instanceof TemplateMixedType + && $type->isExplicitMixed() + ) { + return new FoundTypeResult(new StrictMixedType(), [], [], null); + } - if ( - $this->checkImplicitMixed - && $type instanceof MixedType - && !$type instanceof TemplateMixedType - && !$type->isExplicitMixed() - ) { - return new FoundTypeResult(new StrictMixedType(), [], [], null); + if ( + $this->checkImplicitMixed + && $type instanceof MixedType + && !$type instanceof TemplateMixedType + && !$type->isExplicitMixed() + ) { + return new FoundTypeResult(new StrictMixedType(), [], [], null); + } } if ($type instanceof MixedType || $type instanceof NeverType) { return new FoundTypeResult(new ErrorType(), [], [], null); } - if ($type instanceof StaticType) { - $type = $type->getStaticObjectType(); + if (!$this->newRuleLevelHelper) { + if ($isTopLevel && $type instanceof StaticType) { + $type = $type->getStaticObjectType(); + } } $errors = []; - $directClassNames = TypeUtils::getDirectClassNames($type); $hasClassExistsClass = false; - foreach ($directClassNames as $referencedClass) { - if ($this->reflectionProvider->hasClass($referencedClass)) { - $classReflection = $this->reflectionProvider->getClass($referencedClass); - if (!$classReflection->isTrait()) { + $directClassNames = []; + + if ($isTopLevel) { + $directClassNames = $type->getObjectClassNames(); + foreach ($directClassNames as $referencedClass) { + if ($this->reflectionProvider->hasClass($referencedClass)) { + $classReflection = $this->reflectionProvider->getClass($referencedClass); + if (!$classReflection->isTrait()) { + continue; + } + } + + if ($scope->isInClassExists($referencedClass)) { + $hasClassExistsClass = true; continue; } - } - if ($scope->isInClassExists($referencedClass)) { - $hasClassExistsClass = true; - continue; + $errors[] = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass)) + ->line($var->getStartLine()) + ->identifier('class.notFound') + ->discoveringSymbolsTip() + ->build(); } - - $errors[] = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass))->line($var->getLine())->discoveringSymbolsTip()->build(); } if (count($errors) > 0 || $hasClassExistsClass) { return new FoundTypeResult(new ErrorType(), [], $errors, null); } - if (!$this->checkUnionTypes) { - if ($type instanceof ObjectWithoutClassType) { - return new FoundTypeResult(new ErrorType(), [], [], null); - } + if (!$this->checkUnionTypes && $type instanceof ObjectWithoutClassType) { + return new FoundTypeResult(new ErrorType(), [], [], null); + } + + if ($this->newRuleLevelHelper) { if ($type instanceof UnionType) { + $shouldFilterUnion = ( + !$this->checkUnionTypes + && !$type instanceof BenevolentUnionType + ) || ( + !$this->checkBenevolentUnionTypes + && $type instanceof BenevolentUnionType + ); + $newTypes = []; + + foreach ($type->getTypes() as $innerType) { + if ($shouldFilterUnion && !$unionTypeCriteriaCallback($innerType)) { + continue; + } + + $newTypes[] = $this->findTypeToCheckImplementation( + $scope, + $var, + $innerType, + $unknownClassErrorPattern, + $unionTypeCriteriaCallback, + )->getType(); + } + + if (count($newTypes) > 0) { + return new FoundTypeResult(TypeCombinator::union(...$newTypes), $directClassNames, [], null); + } + } + + if ($type instanceof IntersectionType) { + $newTypes = []; + + foreach ($type->getTypes() as $innerType) { + $newTypes[] = $this->findTypeToCheckImplementation( + $scope, + $var, + $innerType, + $unknownClassErrorPattern, + $unionTypeCriteriaCallback, + )->getType(); + } + + return new FoundTypeResult(TypeCombinator::intersect(...$newTypes), $directClassNames, [], null); + } + } else { + if ( + ( + !$this->checkUnionTypes + && $type instanceof UnionType + && !$type instanceof BenevolentUnionType + ) || ( + !$this->checkBenevolentUnionTypes + && $type instanceof BenevolentUnionType + ) + ) { + $newTypes = []; + foreach ($type->getTypes() as $innerType) { if (!$unionTypeCriteriaCallback($innerType)) { continue; @@ -216,7 +486,7 @@ public function findTypeToCheck( } $tip = null; - if (strpos($type->describe(VerbosityLevel::typeOnly()), 'PhpParser\\Node\\Arg|PhpParser\\Node\\VariadicPlaceholder') !== false && !$unionTypeCriteriaCallback($type)) { + if (str_contains($type->describe(VerbosityLevel::typeOnly()), 'PhpParser\\Node\\Arg|PhpParser\\Node\\VariadicPlaceholder') && !$unionTypeCriteriaCallback($type)) { $tip = 'Use ->getArgs() instead of ->args.'; } diff --git a/src/Rules/RuleLevelHelperAcceptsResult.php b/src/Rules/RuleLevelHelperAcceptsResult.php new file mode 100644 index 0000000000..201408f8f5 --- /dev/null +++ b/src/Rules/RuleLevelHelperAcceptsResult.php @@ -0,0 +1,41 @@ + $reasons + */ + public function __construct( + public readonly bool $result, + public readonly array $reasons, + ) + { + } + + public function and(self $other): self + { + return new self( + $this->result && $other->result, + array_merge($this->reasons, $other->reasons), + ); + } + + /** + * @param callable(string): string $cb + */ + public function decorateReasons(callable $cb): self + { + $reasons = []; + foreach ($this->reasons as $reason) { + $reasons[] = $cb($reason); + } + + return new self($this->result, $reasons); + } + +} diff --git a/src/Rules/TipRuleError.php b/src/Rules/TipRuleError.php index fa9f8f9885..ac518ddce7 100644 --- a/src/Rules/TipRuleError.php +++ b/src/Rules/TipRuleError.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules; +/** @api */ interface TipRuleError extends RuleError { diff --git a/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php index 01b4e33160..2b5c809341 100644 --- a/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php @@ -7,7 +7,6 @@ use PHPStan\Node\InArrowFunctionNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\NullType; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -25,22 +24,23 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $functionReturnType = $scope->getAnonymousFunctionReturnType(); - if ($functionReturnType === null || !$functionReturnType instanceof UnionType) { - return []; - } - $arrowFunction = $node->getOriginalNode(); if ($arrowFunction->returnType === null) { return []; } + $expr = $arrowFunction->expr; if ($expr instanceof Node\Expr\YieldFrom || $expr instanceof Node\Expr\Yield_) { return []; } + $functionReturnType = $scope->getFunctionType($arrowFunction->returnType, false, false); + if (!$functionReturnType instanceof UnionType) { + return []; + } + $returnType = $scope->getType($expr); - if ($returnType instanceof NullType) { + if ($returnType->isNull()->yes()) { return []; } $messages = []; @@ -52,7 +52,7 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( 'Anonymous function never returns %s so it can be removed from the return type.', $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), - ))->build(); + ))->identifier('return.unusedType')->build(); } return $messages; diff --git a/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php index 60cd322ce6..77879f99a3 100644 --- a/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php @@ -7,7 +7,6 @@ use PHPStan\Node\ClosureReturnStatementsNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\NullType; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -27,11 +26,6 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $closureReturnType = $scope->getAnonymousFunctionReturnType(); - if ($closureReturnType === null || !$closureReturnType instanceof UnionType) { - return []; - } - $closureExpr = $node->getClosureExpr(); if ($closureExpr->returnType === null) { return []; @@ -47,6 +41,11 @@ public function processNode(Node $node, Scope $scope): array return []; } + $closureReturnType = $scope->getFunctionType($closureExpr->returnType, false, false); + if (!$closureReturnType instanceof UnionType) { + return []; + } + $returnTypes = []; foreach ($returnStatements as $returnStatement) { $returnNode = $returnStatement->getReturnNode(); @@ -62,7 +61,7 @@ public function processNode(Node $node, Scope $scope): array } $returnType = TypeCombinator::union(...$returnTypes); - if ($returnType instanceof NullType) { + if ($returnType->isNull()->yes()) { return []; } @@ -75,7 +74,7 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( 'Anonymous function never returns %s so it can be removed from the return type.', $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), - ))->build(); + ))->identifier('return.unusedType')->build(); } return $messages; diff --git a/src/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRule.php b/src/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRule.php new file mode 100644 index 0000000000..8056f39420 --- /dev/null +++ b/src/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRule.php @@ -0,0 +1,41 @@ + + */ +class TooWideFunctionParameterOutTypeRule implements Rule +{ + + public function __construct( + private TooWideParameterOutTypeCheck $check, + ) + { + } + + public function getNodeType(): string + { + return FunctionReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $inFunction = $node->getFunctionReflection(); + + return $this->check->check( + $node->getExecutionEnds(), + $node->getReturnStatements(), + ParametersAcceptorSelector::selectSingle($inFunction->getVariants())->getParameters(), + sprintf('Function %s()', $inFunction->getName()), + ); + } + +} diff --git a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php index 35f4080685..9451787f4d 100644 --- a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php @@ -5,13 +5,11 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\FunctionReturnStatementsNode; -use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; -use PHPStan\Type\NullType; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function count; @@ -30,12 +28,10 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $function = $scope->getFunction(); - if (!$function instanceof FunctionReflection) { - throw new ShouldNotHappenException(); - } + $function = $node->getFunctionReflection(); $functionReturnType = ParametersAcceptorSelector::selectSingle($function->getVariants())->getReturnType(); + $functionReturnType = TypeUtils::resolveLateResolvableTypes($functionReturnType); if (!$functionReturnType instanceof UnionType) { return []; } @@ -71,7 +67,7 @@ public function processNode(Node $node, Scope $scope): array continue; } - if ($type instanceof NullType && !$node->hasNativeReturnTypehint()) { + if ($type->isNull()->yes() && !$node->hasNativeReturnTypehint()) { foreach ($node->getExecutionEnds() as $executionEnd) { if ($executionEnd->getStatementResult()->isAlwaysTerminating()) { continue; @@ -85,7 +81,7 @@ public function processNode(Node $node, Scope $scope): array 'Function %s() never returns %s so it can be removed from the return type.', $function->getName(), $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), - ))->build(); + ))->identifier('return.unusedType')->build(); } return $messages; diff --git a/src/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRule.php b/src/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRule.php new file mode 100644 index 0000000000..e715ce7d91 --- /dev/null +++ b/src/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRule.php @@ -0,0 +1,41 @@ + + */ +class TooWideMethodParameterOutTypeRule implements Rule +{ + + public function __construct( + private TooWideParameterOutTypeCheck $check, + ) + { + } + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $inMethod = $node->getMethodReflection(); + + return $this->check->check( + $node->getExecutionEnds(), + $node->getReturnStatements(), + ParametersAcceptorSelector::selectSingle($inMethod->getVariants())->getParameters(), + sprintf('Method %s::%s()', $inMethod->getDeclaringClass()->getDisplayName(), $inMethod->getName()), + ); + } + +} diff --git a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php index 259487e3bf..5e62501f72 100644 --- a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php @@ -5,14 +5,12 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\MethodReturnStatementsNode; -use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\NullType; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function count; @@ -24,7 +22,7 @@ class TooWideMethodReturnTypehintRule implements Rule { - public function __construct(private bool $checkProtectedAndPublicMethods) + public function __construct(private bool $checkProtectedAndPublicMethods, private bool $alwaysCheckFinal) { } @@ -35,21 +33,31 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $method = $scope->getFunction(); - if (!$method instanceof MethodReflection) { - throw new ShouldNotHappenException(); + if ($scope->isInTrait()) { + return []; } + $method = $node->getMethodReflection(); $isFirstDeclaration = $method->getPrototype()->getDeclaringClass() === $method->getDeclaringClass(); if (!$method->isPrivate()) { - if (!$this->checkProtectedAndPublicMethods) { + if ($this->alwaysCheckFinal) { + if (!$method->getDeclaringClass()->isFinal() && !$method->isFinal()->yes()) { + if (!$this->checkProtectedAndPublicMethods) { + return []; + } + + if ($isFirstDeclaration) { + return []; + } + } + } elseif (!$this->checkProtectedAndPublicMethods) { return []; - } - if ($isFirstDeclaration && !$method->getDeclaringClass()->isFinal() && !$method->isFinal()->yes()) { + } elseif ($isFirstDeclaration && !$method->getDeclaringClass()->isFinal() && !$method->isFinal()->yes()) { return []; } } $methodReturnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType(); + $methodReturnType = TypeUtils::resolveLateResolvableTypes($methodReturnType); if (!$methodReturnType instanceof UnionType) { return []; } @@ -80,7 +88,7 @@ public function processNode(Node $node, Scope $scope): array $returnType = TypeCombinator::union(...$returnTypes); if ( !$method->isPrivate() - && ($returnType instanceof NullType || $returnType instanceof ConstantBooleanType) + && ($returnType->isNull()->yes() || $returnType instanceof ConstantBooleanType) && !$isFirstDeclaration ) { return []; @@ -92,7 +100,7 @@ public function processNode(Node $node, Scope $scope): array continue; } - if ($type instanceof NullType && !$node->hasNativeReturnTypehint()) { + if ($type->isNull()->yes() && !$node->hasNativeReturnTypehint()) { foreach ($node->getExecutionEnds() as $executionEnd) { if ($executionEnd->getStatementResult()->isAlwaysTerminating()) { continue; @@ -107,7 +115,7 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), - ))->build(); + ))->identifier('return.unusedType')->build(); } return $messages; diff --git a/src/Rules/TooWideTypehints/TooWideParameterOutTypeCheck.php b/src/Rules/TooWideTypehints/TooWideParameterOutTypeCheck.php new file mode 100644 index 0000000000..097812f918 --- /dev/null +++ b/src/Rules/TooWideTypehints/TooWideParameterOutTypeCheck.php @@ -0,0 +1,118 @@ + $executionEnds + * @param list $returnStatements + * @param ParameterReflectionWithPhpDocs[] $parameters + * @return list + */ + public function check( + array $executionEnds, + array $returnStatements, + array $parameters, + string $functionDescription, + ): array + { + $finalScope = null; + foreach ($executionEnds as $executionEnd) { + $endScope = $executionEnd->getStatementResult()->getScope(); + if ($finalScope === null) { + $finalScope = $endScope; + continue; + } + + $finalScope = $finalScope->mergeWith($endScope); + } + + foreach ($returnStatements as $statement) { + if ($finalScope === null) { + $finalScope = $statement->getScope(); + continue; + } + + $finalScope = $finalScope->mergeWith($statement->getScope()); + } + + if ($finalScope === null) { + return []; + } + + $errors = []; + foreach ($parameters as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + foreach ($this->processSingleParameter($finalScope, $functionDescription, $parameter) as $error) { + $errors[] = $error; + } + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleParameter( + Scope $scope, + string $functionDescription, + ParameterReflectionWithPhpDocs $parameter, + ): array + { + $isParamOutType = true; + $outType = $parameter->getOutType(); + if ($outType === null) { + $isParamOutType = false; + $outType = $parameter->getType(); + } + + $outType = TypeUtils::resolveLateResolvableTypes($outType); + if (!$outType instanceof UnionType) { + return []; + } + + $variableExpr = new Variable($parameter->getName()); + $variableType = $scope->getType($variableExpr); + + $messages = []; + foreach ($outType->getTypes() as $type) { + if (!$type->isSuperTypeOf($variableType)->no()) { + continue; + } + + $errorBuilder = RuleErrorBuilder::message(sprintf( + '%s never assigns %s to &$%s so it can be removed from the %s.', + $functionDescription, + $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), + $parameter->getName(), + $isParamOutType ? '@param-out type' : 'by-ref type', + ))->identifier(sprintf('%s.unusedType', $isParamOutType ? 'paramOut' : 'parameterByRef')); + if (!$isParamOutType) { + $errorBuilder->tip('You can narrow the parameter out type with @param-out PHPDoc tag.'); + } + + $messages[] = $errorBuilder->build(); + } + + return $messages; + } + +} diff --git a/src/Rules/Traits/ConflictingTraitConstantsRule.php b/src/Rules/Traits/ConflictingTraitConstantsRule.php new file mode 100644 index 0000000000..73612f3074 --- /dev/null +++ b/src/Rules/Traits/ConflictingTraitConstantsRule.php @@ -0,0 +1,248 @@ + + */ +class ConflictingTraitConstantsRule implements Rule +{ + + public function __construct(private InitializerExprTypeResolver $initializerExprTypeResolver) + { + } + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$scope->isInClass()) { + return []; + } + + $classReflection = $scope->getClassReflection(); + $traitConstants = []; + foreach ($classReflection->getTraits(true) as $trait) { + foreach ($trait->getNativeReflection()->getReflectionConstants() as $constant) { + $traitConstants[] = $constant; + } + } + + $errors = []; + foreach ($node->consts as $const) { + foreach ($traitConstants as $traitConstant) { + if ($traitConstant->getName() !== $const->name->toString()) { + continue; + } + + foreach ($this->processSingleConstant($classReflection, $traitConstant, $node, $const->value) as $error) { + $errors[] = $error; + } + } + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleConstant(ClassReflection $classReflection, ReflectionClassConstant $traitConstant, Node\Stmt\ClassConst $classConst, Node\Expr $valueExpr): array + { + $errors = []; + if ($traitConstant->isPublic()) { + if ($classConst->isProtected()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Protected constant %s::%s overriding public constant %s::%s should also be public.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + )) + ->nonIgnorable() + ->identifier('classConstant.visibility') + ->build(); + } elseif ($classConst->isPrivate()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Private constant %s::%s overriding public constant %s::%s should also be public.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + )) + ->nonIgnorable() + ->identifier('classConstant.visibility') + ->build(); + } + } elseif ($traitConstant->isProtected()) { + if ($classConst->isPublic()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Public constant %s::%s overriding protected constant %s::%s should also be protected.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + )) + ->nonIgnorable() + ->identifier('classConstant.visibility') + ->build(); + } elseif ($classConst->isPrivate()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Private constant %s::%s overriding protected constant %s::%s should also be protected.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + )) + ->nonIgnorable() + ->identifier('classConstant.visibility') + ->build(); + } + } elseif ($traitConstant->isPrivate()) { + if ($classConst->isPublic()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Public constant %s::%s overriding private constant %s::%s should also be private.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + )) + ->nonIgnorable() + ->identifier('classConstant.visibility') + ->build(); + } elseif ($classConst->isProtected()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Protected constant %s::%s overriding private constant %s::%s should also be private.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + )) + ->nonIgnorable() + ->identifier('classConstant.visibility') + ->build(); + } + } + + if ($traitConstant->isFinal()) { + if (!$classConst->isFinal()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Non-final constant %s::%s overriding final constant %s::%s should also be final.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + )) + ->nonIgnorable() + ->identifier('classConstant.nonFinal') + ->build(); + } + } elseif ($classConst->isFinal()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Final constant %s::%s overriding non-final constant %s::%s should also be non-final.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + )) + ->nonIgnorable() + ->identifier('classConstant.final') + ->build(); + } + + $traitNativeType = $traitConstant->getType(); + $constantNativeType = $classConst->type; + $traitDeclaringClass = $traitConstant->getDeclaringClass(); + if ($traitNativeType === null) { + if ($constantNativeType !== null) { + $constantNativeTypeType = ParserNodeTypeToPHPStanType::resolve($constantNativeType, $classReflection); + $errors[] = RuleErrorBuilder::message(sprintf( + 'Constant %s::%s (%s) overriding constant %s::%s should not have a native type.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $constantNativeTypeType->describe(VerbosityLevel::typeOnly()), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + )) + ->nonIgnorable() + ->identifier('classConstant.nativeType') + ->build(); + } + } elseif ($constantNativeType === null) { + $traitNativeTypeType = TypehintHelper::decideTypeFromReflection($traitNativeType, null, $traitDeclaringClass->getName()); + $errors[] = RuleErrorBuilder::message(sprintf( + 'Constant %s::%s overriding constant %s::%s (%s) should also have native type %s.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + $traitNativeTypeType->describe(VerbosityLevel::typeOnly()), + $traitNativeTypeType->describe(VerbosityLevel::typeOnly()), + )) + ->nonIgnorable() + ->identifier('classConstant.missingNativeType') + ->build(); + } else { + $traitNativeTypeType = TypehintHelper::decideTypeFromReflection($traitNativeType, null, $traitDeclaringClass->getName()); + $constantNativeTypeType = ParserNodeTypeToPHPStanType::resolve($constantNativeType, $classReflection); + if (!$traitNativeTypeType->equals($constantNativeTypeType)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Constant %s::%s (%s) overriding constant %s::%s (%s) should have the same native type %s.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $constantNativeTypeType->describe(VerbosityLevel::typeOnly()), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + $traitNativeTypeType->describe(VerbosityLevel::typeOnly()), + $traitNativeTypeType->describe(VerbosityLevel::typeOnly()), + )) + ->nonIgnorable() + ->identifier('classConstant.nativeType') + ->build(); + } + } + + $classConstantValueType = $this->initializerExprTypeResolver->getType($valueExpr, InitializerExprContext::fromClassReflection($classReflection)); + $traitConstantValueType = $this->initializerExprTypeResolver->getType( + $traitConstant->getValueExpression(), + InitializerExprContext::fromClass( + $traitDeclaringClass->getName(), + $traitDeclaringClass->getFileName() !== false ? $traitDeclaringClass->getFileName() : null, + ), + ); + if (!$classConstantValueType->equals($traitConstantValueType)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Constant %s::%s with value %s overriding constant %s::%s with different value %s should have the same value.', + $classReflection->getDisplayName(), + $traitConstant->getName(), + $classConstantValueType->describe(VerbosityLevel::value()), + $traitConstant->getDeclaringClass()->getName(), + $traitConstant->getName(), + $traitConstantValueType->describe(VerbosityLevel::value()), + )) + ->nonIgnorable() + ->identifier('classConstant.value') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Traits/ConstantsInTraitsRule.php b/src/Rules/Traits/ConstantsInTraitsRule.php new file mode 100644 index 0000000000..6948568540 --- /dev/null +++ b/src/Rules/Traits/ConstantsInTraitsRule.php @@ -0,0 +1,46 @@ + + */ +class ConstantsInTraitsRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + /** + * @param Node\Stmt\ClassConst $node + */ + public function processNode(Node $node, Scope $scope): array + { + if ($this->phpVersion->supportsConstantsInTraits()) { + return []; + } + + if (!$scope->isInTrait()) { + return []; + } + + return [ + RuleErrorBuilder::message( + 'Constant is declared inside a trait but is only supported on PHP 8.2 and later.', + )->identifier('classConstant.inTrait')->nonIgnorable()->build(), + ]; + } + +} diff --git a/src/Rules/Traits/NotAnalysedTraitRule.php b/src/Rules/Traits/NotAnalysedTraitRule.php index 2c8d9c00ce..3ae9b74c46 100644 --- a/src/Rules/Traits/NotAnalysedTraitRule.php +++ b/src/Rules/Traits/NotAnalysedTraitRule.php @@ -23,6 +23,10 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { + if ($node->isOnlyFilesAnalysis()) { + return []; + } + $traitDeclarationData = $node->get(TraitDeclarationCollector::class); $traitUseData = $node->get(TraitUseCollector::class); @@ -49,6 +53,7 @@ public function processNode(Node $node, Scope $scope): array )) ->file($file) ->line($line) + ->identifier('trait.unused') ->tip('See: https://phpstan.org/blog/how-phpstan-analyses-traits') ->build(); } diff --git a/src/Rules/Traits/TraitDeclarationCollector.php b/src/Rules/Traits/TraitDeclarationCollector.php index 53277f4eda..7ce2cee84d 100644 --- a/src/Rules/Traits/TraitDeclarationCollector.php +++ b/src/Rules/Traits/TraitDeclarationCollector.php @@ -23,7 +23,7 @@ public function processNode(Node $node, Scope $scope) return null; } - return [$node->namespacedName->toString(), $node->getLine()]; + return [$node->namespacedName->toString(), $node->getStartLine()]; } } diff --git a/src/Rules/Traits/TraitUseCollector.php b/src/Rules/Traits/TraitUseCollector.php index 3a9b316e88..1bd3f82cba 100644 --- a/src/Rules/Traits/TraitUseCollector.php +++ b/src/Rules/Traits/TraitUseCollector.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Collectors\Collector; use function array_map; +use function array_values; /** * @implements Collector> @@ -20,7 +21,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope) { - return array_map(static fn (Node\Name $traitName) => $traitName->toString(), $node->traits); + return array_values(array_map(static fn (Node\Name $traitName) => $traitName->toString(), $node->traits)); } } diff --git a/src/Rules/Types/InvalidTypesInUnionRule.php b/src/Rules/Types/InvalidTypesInUnionRule.php new file mode 100644 index 0000000000..88442c2bde --- /dev/null +++ b/src/Rules/Types/InvalidTypesInUnionRule.php @@ -0,0 +1,125 @@ + + */ +class InvalidTypesInUnionRule implements Rule +{ + + private const ONLY_STANDALONE_TYPES = [ + 'mixed', + 'never', + 'void', + ]; + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node instanceof Node\FunctionLike && !$node instanceof ClassPropertyNode) { + return []; + } + + if ($node instanceof Node\FunctionLike) { + return $this->processFunctionLikeNode($node); + } + + return $this->processClassPropertyNode($node); + } + + /** + * @return list + */ + private function processFunctionLikeNode(Node\FunctionLike $functionLike): array + { + $errors = []; + + foreach ($functionLike->getParams() as $param) { + if (!$param->type instanceof Node\ComplexType) { + continue; + } + + $errors = array_merge($errors, $this->processComplexType($param->type)); + } + + if ($functionLike->getReturnType() instanceof Node\ComplexType) { + $errors = array_merge($errors, $this->processComplexType($functionLike->getReturnType())); + } + + return $errors; + } + + /** + * @return list + */ + private function processClassPropertyNode(ClassPropertyNode $classPropertyNode): array + { + if (!$classPropertyNode->getNativeType() instanceof Node\ComplexType) { + return []; + } + + return $this->processComplexType($classPropertyNode->getNativeType()); + } + + /** + * @return list + */ + private function processComplexType(Node\ComplexType $complexType): array + { + if (!$complexType instanceof Node\UnionType && !$complexType instanceof Node\NullableType) { + return []; + } + + if ($complexType instanceof Node\UnionType) { + foreach ($complexType->types as $type) { + if (!$type instanceof Node\Identifier) { + continue; + } + + $typeString = $type->toLowerString(); + if (in_array($typeString, self::ONLY_STANDALONE_TYPES, true)) { + return [ + RuleErrorBuilder::message(sprintf('Type %s cannot be part of a union type declaration.', $type->toString())) + ->line($complexType->getStartLine()) + ->identifier(sprintf('unionType.%s', $typeString)) + ->nonIgnorable() + ->build(), + ]; + } + } + + return []; + } + + if ($complexType->type instanceof Node\Identifier) { + $complexTypeString = $complexType->type->toLowerString(); + if (in_array($complexTypeString, self::ONLY_STANDALONE_TYPES, true)) { + return [ + RuleErrorBuilder::message(sprintf('Type %s cannot be part of a nullable type declaration.', $complexType->type->toString())) + ->line($complexType->getStartLine()) + ->identifier(sprintf('nullableType.%s', $complexTypeString)) + ->nonIgnorable() + ->build(), + ]; + } + } + + return []; + } + +} diff --git a/src/Rules/UnusedFunctionParametersCheck.php b/src/Rules/UnusedFunctionParametersCheck.php index 890fc5d3f6..8da2427d31 100644 --- a/src/Rules/UnusedFunctionParametersCheck.php +++ b/src/Rules/UnusedFunctionParametersCheck.php @@ -23,8 +23,8 @@ public function __construct(private ReflectionProvider $reflectionProvider) /** * @param string[] $parameterNames * @param Node[] $statements - * @param mixed[] $additionalMetadata - * @return RuleError[] + * @param 'constructor.unusedParameter'|'closure.unusedUse' $identifier + * @return list */ public function getUnusedParameters( Scope $scope, @@ -32,7 +32,6 @@ public function getUnusedParameters( array $statements, string $unusedParameterMessage, string $identifier, - array $additionalMetadata, ): array { $unusedParameters = array_fill_keys($parameterNames, true); @@ -47,14 +46,14 @@ public function getUnusedParameters( foreach (array_keys($unusedParameters) as $name) { $errors[] = RuleErrorBuilder::message( sprintf($unusedParameterMessage, $name), - )->identifier($identifier)->metadata($additionalMetadata + ['variableName' => $name])->build(); + )->identifier($identifier)->build(); } return $errors; } /** - * @param Node[]|Node|scalar $node + * @param Node[]|Node|scalar|null $node * @return string[] */ private function getUsedVariables(Scope $scope, $node): array @@ -63,7 +62,7 @@ private function getUsedVariables(Scope $scope, $node): array if ($node instanceof Node) { if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) { $functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope); - if ($functionName === 'func_get_args') { + if ($functionName === 'func_get_args' || $functionName === 'get_defined_vars') { return $scope->getDefinedVariables(); } } diff --git a/src/Rules/Variables/CompactVariablesRule.php b/src/Rules/Variables/CompactVariablesRule.php index 454082e900..c6324f7678 100644 --- a/src/Rules/Variables/CompactVariablesRule.php +++ b/src/Rules/Variables/CompactVariablesRule.php @@ -53,11 +53,11 @@ public function processNode(Node $node, Scope $scope): array if ($scopeHasVariable->no()) { $messages[] = RuleErrorBuilder::message( sprintf('Call to function compact() contains undefined variable $%s.', $variableName), - )->line($argument->getLine())->build(); + )->identifier('variable.undefined')->line($argument->getStartLine())->build(); } elseif ($this->checkMaybeUndefinedVariables && $scopeHasVariable->maybe()) { $messages[] = RuleErrorBuilder::message( sprintf('Call to function compact() contains possibly undefined variable $%s.', $variableName), - )->line($argument->getLine())->build(); + )->identifier('variable.undefined')->line($argument->getStartLine())->build(); } } } diff --git a/src/Rules/Variables/DefinedVariableRule.php b/src/Rules/Variables/DefinedVariableRule.php index 3b756df99c..fbe15dcfbc 100644 --- a/src/Rules/Variables/DefinedVariableRule.php +++ b/src/Rules/Variables/DefinedVariableRule.php @@ -53,15 +53,6 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf('Undefined variable: $%s', $node->name)) ->identifier('variable.undefined') - ->metadata([ - 'variableName' => $node->name, - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - 'depth' => $node->getAttribute('expressionDepth'), - 'order' => $node->getAttribute('expressionOrder'), - 'variables' => $scope->getDefinedVariables(), - 'parentVariables' => $this->getParentVariables($scope), - ]) ->build(), ]; } elseif ( @@ -70,16 +61,7 @@ public function processNode(Node $node, Scope $scope): array ) { return [ RuleErrorBuilder::message(sprintf('Variable $%s might not be defined.', $node->name)) - ->identifier('variable.maybeUndefined') - ->metadata([ - 'variableName' => $node->name, - 'statementDepth' => $node->getAttribute('statementDepth'), - 'statementOrder' => $node->getAttribute('statementOrder'), - 'depth' => $node->getAttribute('expressionDepth'), - 'order' => $node->getAttribute('expressionOrder'), - 'variables' => $scope->getDefinedVariables(), - 'parentVariables' => $this->getParentVariables($scope), - ]) + ->identifier('variable.undefined') ->build(), ]; } @@ -87,19 +69,4 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** - * @return array> - */ - private function getParentVariables(Scope $scope): array - { - $variables = []; - $parent = $scope->getParentScope(); - while ($parent !== null) { - $variables[] = $parent->getDefinedVariables(); - $parent = $parent->getParentScope(); - } - - return $variables; - } - } diff --git a/src/Rules/Variables/EmptyRule.php b/src/Rules/Variables/EmptyRule.php index 9a48592cb9..ca588bd711 100644 --- a/src/Rules/Variables/EmptyRule.php +++ b/src/Rules/Variables/EmptyRule.php @@ -6,8 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\IssetCheck; use PHPStan\Rules\Rule; -use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\NullType; use PHPStan\Type\Type; /** @@ -27,12 +25,12 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $error = $this->issetCheck->check($node->expr, $scope, 'in empty()', static function (Type $type): ?string { - $isNull = (new NullType())->isSuperTypeOf($type); - $isFalsey = (new ConstantBooleanType(false))->isSuperTypeOf($type->toBoolean()); + $error = $this->issetCheck->check($node->expr, $scope, 'in empty()', 'empty', static function (Type $type): ?string { + $isNull = $type->isNull(); if ($isNull->maybe()) { return null; } + $isFalsey = $type->toBoolean()->isFalse(); if ($isFalsey->maybe()) { return null; } diff --git a/src/Rules/Variables/IssetRule.php b/src/Rules/Variables/IssetRule.php index 55cd5dc05f..f5c59363ff 100644 --- a/src/Rules/Variables/IssetRule.php +++ b/src/Rules/Variables/IssetRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\IssetCheck; use PHPStan\Rules\Rule; -use PHPStan\Type\NullType; use PHPStan\Type\Type; /** @@ -28,8 +27,8 @@ public function processNode(Node $node, Scope $scope): array { $messages = []; foreach ($node->vars as $var) { - $error = $this->issetCheck->check($var, $scope, 'in isset()', static function (Type $type): ?string { - $isNull = (new NullType())->isSuperTypeOf($type); + $error = $this->issetCheck->check($var, $scope, 'in isset()', 'isset', static function (Type $type): ?string { + $isNull = $type->isNull(); if ($isNull->maybe()) { return null; } diff --git a/src/Rules/Variables/NullCoalesceRule.php b/src/Rules/Variables/NullCoalesceRule.php index 8666ab4f7e..ef289640e3 100644 --- a/src/Rules/Variables/NullCoalesceRule.php +++ b/src/Rules/Variables/NullCoalesceRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\IssetCheck; use PHPStan\Rules\Rule; -use PHPStan\Type\NullType; use PHPStan\Type\Type; /** @@ -27,7 +26,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $typeMessageCallback = static function (Type $type): ?string { - $isNull = (new NullType())->isSuperTypeOf($type); + $isNull = $type->isNull(); if ($isNull->maybe()) { return null; } @@ -40,9 +39,9 @@ public function processNode(Node $node, Scope $scope): array }; if ($node instanceof Node\Expr\BinaryOp\Coalesce) { - $error = $this->issetCheck->check($node->left, $scope, 'on left side of ??', $typeMessageCallback); + $error = $this->issetCheck->check($node->left, $scope, 'on left side of ??', 'nullCoalesce', $typeMessageCallback); } elseif ($node instanceof Node\Expr\AssignOp\Coalesce) { - $error = $this->issetCheck->check($node->var, $scope, 'on left side of ??=', $typeMessageCallback); + $error = $this->issetCheck->check($node->var, $scope, 'on left side of ??=', 'nullCoalesce', $typeMessageCallback); } else { return []; } diff --git a/src/Rules/Variables/ParameterOutAssignedTypeRule.php b/src/Rules/Variables/ParameterOutAssignedTypeRule.php new file mode 100644 index 0000000000..9c24597525 --- /dev/null +++ b/src/Rules/Variables/ParameterOutAssignedTypeRule.php @@ -0,0 +1,122 @@ + + */ +class ParameterOutAssignedTypeRule implements Rule +{ + + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + ) + { + } + + public function getNodeType(): string + { + return VariableAssignNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $inFunction = $scope->getFunction(); + if ($inFunction === null) { + return []; + } + + if ($scope->isInAnonymousFunction()) { + return []; + } + + $variable = $node->getVariable(); + if (!is_string($variable->name)) { + return []; + } + + $variant = ParametersAcceptorSelector::selectSingle($inFunction->getVariants()); + $parameters = $variant->getParameters(); + $foundParameter = null; + foreach ($parameters as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + if ($parameter->getName() !== $variable->name) { + continue; + } + + $foundParameter = $parameter; + break; + } + + if ($foundParameter === null) { + return []; + } + + $isParamOutType = true; + $outType = $foundParameter->getOutType(); + if ($outType === null) { + $isParamOutType = false; + $outType = $foundParameter->getType(); + } + + $outType = TypeUtils::resolveLateResolvableTypes($outType); + + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->getAssignedExpr(), + '', + static fn (Type $type): bool => $outType->isSuperTypeOf($type)->yes(), + ); + $type = $typeResult->getType(); + if ($type instanceof ErrorType) { + return $typeResult->getUnknownClassErrors(); + } + + $assignedExprType = $scope->getType($node->getAssignedExpr()); + if ($outType->isSuperTypeOf($assignedExprType)->yes()) { + return []; + } + + if ($inFunction instanceof ExtendedMethodReflection) { + $functionDescription = sprintf('method %s::%s()', $inFunction->getDeclaringClass()->getDisplayName(), $inFunction->getName()); + } else { + $functionDescription = sprintf('function %s()', $inFunction->getName()); + } + + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($outType, $assignedExprType); + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'Parameter &$%s %s of %s expects %s, %s given.', + $foundParameter->getName(), + $isParamOutType ? '@param-out type' : 'by-ref type', + $functionDescription, + $outType->describe($verbosityLevel), + $assignedExprType->describe($verbosityLevel), + ))->identifier(sprintf('%s.type', $isParamOutType ? 'paramOut' : 'parameterByRef')); + + if (!$isParamOutType) { + $errorBuilder->tip('You can change the parameter out type with @param-out PHPDoc tag.'); + } + + return [ + $errorBuilder->build(), + ]; + } + +} diff --git a/src/Rules/Variables/ParameterOutExecutionEndTypeRule.php b/src/Rules/Variables/ParameterOutExecutionEndTypeRule.php new file mode 100644 index 0000000000..fee386168b --- /dev/null +++ b/src/Rules/Variables/ParameterOutExecutionEndTypeRule.php @@ -0,0 +1,124 @@ + + */ +class ParameterOutExecutionEndTypeRule implements Rule +{ + + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + ) + { + } + + public function getNodeType(): string + { + return ExecutionEndNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $inFunction = $scope->getFunction(); + if ($inFunction === null) { + return []; + } + + if ($scope->isInAnonymousFunction()) { + return []; + } + + $variant = ParametersAcceptorSelector::selectSingle($inFunction->getVariants()); + $parameters = $variant->getParameters(); + $errors = []; + foreach ($parameters as $parameter) { + if (!$parameter->passedByReference()->createsNewVariable()) { + continue; + } + + foreach ($this->processSingleParameter($scope, $inFunction, $parameter) as $error) { + $errors[] = $error; + } + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleParameter( + Scope $scope, + FunctionReflection|ExtendedMethodReflection $inFunction, + ParameterReflectionWithPhpDocs $parameter, + ): array + { + $outType = $parameter->getOutType(); + if ($outType === null) { + return []; + } + + if ($scope->hasExpressionType(new ParameterVariableOriginalValueExpr($parameter->getName()))->no()) { + return []; + } + + $outType = TypeUtils::resolveLateResolvableTypes($outType); + + $variableExpr = new Node\Expr\Variable($parameter->getName()); + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $variableExpr, + '', + static fn (Type $type): bool => $outType->isSuperTypeOf($type)->yes(), + ); + $type = $typeResult->getType(); + if ($type instanceof ErrorType) { + return $typeResult->getUnknownClassErrors(); + } + + $assignedExprType = $scope->getType($variableExpr); + if ($outType->isSuperTypeOf($assignedExprType)->yes()) { + return []; + } + + if ($inFunction instanceof ExtendedMethodReflection) { + $functionDescription = sprintf('method %s::%s()', $inFunction->getDeclaringClass()->getDisplayName(), $inFunction->getName()); + } else { + $functionDescription = sprintf('function %s()', $inFunction->getName()); + } + + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($outType, $assignedExprType); + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'Parameter &$%s @param-out type of %s expects %s, %s given.', + $parameter->getName(), + $functionDescription, + $outType->describe($verbosityLevel), + $assignedExprType->describe($verbosityLevel), + ))->identifier(sprintf('paramOut.type')); + + return [ + $errorBuilder->build(), + ]; + } + +} diff --git a/src/Rules/Variables/ThrowTypeRule.php b/src/Rules/Variables/ThrowTypeRule.php index deb7a22856..7678406a9e 100644 --- a/src/Rules/Variables/ThrowTypeRule.php +++ b/src/Rules/Variables/ThrowTypeRule.php @@ -55,7 +55,7 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Invalid type %s to throw.', $foundType->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('throw.notThrowable')->build(), ]; } diff --git a/src/Rules/Variables/UnsetRule.php b/src/Rules/Variables/UnsetRule.php index 2484802dc1..43efa6646c 100644 --- a/src/Rules/Variables/UnsetRule.php +++ b/src/Rules/Variables/UnsetRule.php @@ -4,8 +4,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; use function is_string; @@ -39,14 +39,17 @@ public function processNode(Node $node, Scope $scope): array return $errors; } - private function canBeUnset(Node $node, Scope $scope): ?RuleError + private function canBeUnset(Node $node, Scope $scope): ?IdentifierRuleError { if ($node instanceof Node\Expr\Variable && is_string($node->name)) { $hasVariable = $scope->hasVariableType($node->name); if ($hasVariable->no()) { return RuleErrorBuilder::message( sprintf('Call to function unset() contains undefined variable $%s.', $node->name), - )->line($node->getLine())->build(); + ) + ->line($node->getStartLine()) + ->identifier('unset.variable') + ->build(); } } elseif ($node instanceof Node\Expr\ArrayDimFetch && $node->dim !== null) { $type = $scope->getType($node->var); @@ -59,7 +62,10 @@ private function canBeUnset(Node $node, Scope $scope): ?RuleError $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value()), ), - )->line($node->getLine())->build(); + ) + ->line($node->getStartLine()) + ->identifier('unset.offset') + ->build(); } return $this->canBeUnset($node->var, $scope); diff --git a/src/Rules/Variables/VariableCloningRule.php b/src/Rules/Variables/VariableCloningRule.php index 81bfbcb253..89aca44aa9 100644 --- a/src/Rules/Variables/VariableCloningRule.php +++ b/src/Rules/Variables/VariableCloningRule.php @@ -52,7 +52,7 @@ public function processNode(Node $node, Scope $scope): array 'Cannot clone non-object variable $%s of type %s.', $node->expr->name, $type->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('clone.nonObject')->build(), ]; } @@ -60,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Cannot clone %s.', $type->describe(VerbosityLevel::typeOnly()), - ))->build(), + ))->identifier('clone.nonObject')->build(), ]; } diff --git a/src/Rules/Whitespace/FileWhitespaceRule.php b/src/Rules/Whitespace/FileWhitespaceRule.php index 9a48d9293a..7f3bcbdcfe 100644 --- a/src/Rules/Whitespace/FileWhitespaceRule.php +++ b/src/Rules/Whitespace/FileWhitespaceRule.php @@ -33,7 +33,9 @@ public function processNode(Node $node, Scope $scope): array $firstNode = $nodes[0]; $messages = []; if ($firstNode instanceof Node\Stmt\InlineHTML && $firstNode->value === "\xef\xbb\xbf") { - $messages[] = RuleErrorBuilder::message('File begins with UTF-8 BOM character. This may cause problems when running the code in the web browser.')->build(); + $messages[] = RuleErrorBuilder::message('File begins with UTF-8 BOM character. This may cause problems when running the code in the web browser.') + ->identifier('whitespace.bom') + ->build(); } $nodeTraverser = new NodeTraverser(); @@ -43,7 +45,7 @@ public function processNode(Node $node, Scope $scope): array private array $lastNodes = []; /** - * @return int|Node|null + * @return int|null */ public function enterNode(Node $node) { @@ -81,7 +83,9 @@ public function getLastNodes(): array continue; } - $messages[] = RuleErrorBuilder::message('File ends with a trailing whitespace. This may cause problems when running the code in the web browser. Remove the closing ?> mark or remove the whitespace.')->line($lastNode->getStartLine())->build(); + $messages[] = RuleErrorBuilder::message('File ends with a trailing whitespace. This may cause problems when running the code in the web browser. Remove the closing ?> mark or remove the whitespace.')->line($lastNode->getStartLine()) + ->identifier('whitespace.fileEnd') + ->build(); } return $messages; diff --git a/src/Testing/ErrorFormatterTestCase.php b/src/Testing/ErrorFormatterTestCase.php index 4813452b71..f4b90e6778 100644 --- a/src/Testing/ErrorFormatterTestCase.php +++ b/src/Testing/ErrorFormatterTestCase.php @@ -100,6 +100,9 @@ protected function getAnalysisResult(int $numFileErrors, int $numGenericErrors): false, null, true, + 0, + false, + [], ); } diff --git a/src/Testing/LevelsTestCase.php b/src/Testing/LevelsTestCase.php index 277c9db9f4..7d0b1998a9 100644 --- a/src/Testing/LevelsTestCase.php +++ b/src/Testing/LevelsTestCase.php @@ -16,6 +16,7 @@ use function exec; use function implode; use function method_exists; +use function putenv; use function range; use function sprintf; use function unlink; @@ -68,6 +69,8 @@ public function testLevels( throw new ShouldNotHappenException('Could not clear result cache: ' . implode("\n", $clearResultCacheOutputLines)); } + putenv('__PHPSTAN_FORCE_VALIDATE_STUB_FILES=1'); + foreach (range(0, 9) as $level) { unset($outputLines); exec(sprintf('%s %s analyse --no-progress --error-format=prettyJson --level=%d %s %s %s', escapeshellarg(PHP_BINARY), $command, $level, $configPath !== null ? '--configuration ' . escapeshellarg($configPath) : '', $this->shouldAutoloadAnalysedFile() ? sprintf('--autoload-file %s', escapeshellarg($file)) : '', escapeshellarg($file)), $outputLines); @@ -111,6 +114,7 @@ public function testLevels( } unset($message['tip']); + unset($message['identifier']); $messages[] = $message; } @@ -168,7 +172,7 @@ private function compareFiles(string $expectedJsonFile, array $expectedMessages) { if (count($expectedMessages) === 0) { try { - self::assertFileDoesNotExist($expectedJsonFile); + self::ourCustomAssertFileDoesNotExist($expectedJsonFile); return null; } catch (AssertionFailedError $e) { unlink($expectedJsonFile); @@ -191,8 +195,9 @@ private function compareFiles(string $expectedJsonFile, array $expectedMessages) return null; } - public static function assertFileDoesNotExist(string $filename, string $message = ''): void + public static function ourCustomAssertFileDoesNotExist(string $filename, string $message = ''): void { + // this method is no longer called assertFileDoesNotExist because this method is final in PHPUnit 10 if (!method_exists(parent::class, 'assertFileDoesNotExist')) { parent::assertFileNotExists($filename, $message); return; diff --git a/src/Testing/PHPStanTestCase.php b/src/Testing/PHPStanTestCase.php index 17ce442df0..14efcc7480 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -3,7 +3,7 @@ namespace PHPStan\Testing; use PHPStan\Analyser\ConstantResolver; -use PHPStan\Analyser\DirectScopeFactory; +use PHPStan\Analyser\DirectInternalScopeFactory; use PHPStan\Analyser\Error; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; @@ -18,8 +18,11 @@ use PHPStan\DependencyInjection\ContainerFactory; use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider; use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider; +use PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider; use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider; use PHPStan\File\FileHelper; +use PHPStan\Internal\DirectoryCreator; +use PHPStan\Internal\DirectoryCreatorException; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Parser\Parser; use PHPStan\Php\PhpVersion; @@ -29,6 +32,7 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Type\Constant\OversizedArrayBuilder; use PHPStan\Type\TypeAliasResolver; use PHPStan\Type\UsefulTypeAliasResolver; use PHPUnit\Framework\ExpectationFailedException; @@ -36,8 +40,6 @@ use function array_merge; use function count; use function implode; -use function is_dir; -use function mkdir; use function rtrim; use function sha1; use function sprintf; @@ -64,8 +66,10 @@ public static function getContainer(): Container if (!isset(self::$containers[$cacheKey])) { $tmpDir = sys_get_temp_dir() . '/phpstan-tests'; - if (!@mkdir($tmpDir, 0777) && !is_dir($tmpDir)) { - self::fail(sprintf('Cannot create temp directory %s', $tmpDir)); + try { + DirectoryCreator::ensureDirectoryExists($tmpDir, 0777); + } catch (DirectoryCreatorException $e) { + self::fail($e->getMessage()); } $rootDir = __DIR__ . '/../..'; @@ -90,6 +94,8 @@ public static function getContainer(): Container require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnumUnitCase.php'; require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnumBackedCase.php'; } + } else { + ContainerFactory::postInitializeContainer(self::$containers[$cacheKey]); } return self::$containers[$cacheKey]; @@ -103,7 +109,7 @@ public static function getAdditionalConfigFiles(): array return []; } - public function getParser(): Parser + public static function getParser(): Parser { /** @var Parser $parser */ $parser = self::getContainer()->getService('defaultAnalysisParser'); @@ -120,7 +126,7 @@ public function createBroker(): Broker } /** @api */ - public function createReflectionProvider(): ReflectionProvider + public static function createReflectionProvider(): ReflectionProvider { return self::getContainer()->getByType(ReflectionProvider::class); } @@ -143,7 +149,7 @@ public static function getReflectors(): array ]; } - public function getClassReflectionExtensionRegistryProvider(): ClassReflectionExtensionRegistryProvider + public static function getClassReflectionExtensionRegistryProvider(): ClassReflectionExtensionRegistryProvider { return self::getContainer()->getByType(ClassReflectionExtensionRegistryProvider::class); } @@ -151,7 +157,7 @@ public function getClassReflectionExtensionRegistryProvider(): ClassReflectionEx /** * @param string[] $dynamicConstantNames */ - public function createScopeFactory(ReflectionProvider $reflectionProvider, TypeSpecifier $typeSpecifier, array $dynamicConstantNames = []): ScopeFactory + public static function createScopeFactory(ReflectionProvider $reflectionProvider, TypeSpecifier $typeSpecifier, array $dynamicConstantNames = []): ScopeFactory { $container = self::getContainer(); @@ -162,28 +168,37 @@ public function createScopeFactory(ReflectionProvider $reflectionProvider, TypeS $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider); $constantResolver = new ConstantResolver($reflectionProviderProvider, $dynamicConstantNames); - return new DirectScopeFactory( - MutatingScope::class, - $reflectionProvider, - new InitializerExprTypeResolver($constantResolver, $reflectionProviderProvider, new PhpVersion(PHP_VERSION_ID), $container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class)), - $container->getByType(DynamicReturnTypeExtensionRegistryProvider::class), - $container->getByType(ExprPrinter::class), - $typeSpecifier, - new PropertyReflectionFinder(), - $this->getParser(), - $container->getByType(NodeScopeResolver::class), - $this->shouldTreatPhpDocTypesAsCertain(), - $container->getByType(PhpVersion::class), - $container->getParameter('featureToggles')['explicitMixedInUnknownGenericNew'], - $container->getParameter('featureToggles')['explicitMixedForGlobalVariables'], - $constantResolver, + return new ScopeFactory( + new DirectInternalScopeFactory( + MutatingScope::class, + $reflectionProvider, + new InitializerExprTypeResolver( + $constantResolver, + $reflectionProviderProvider, + $container->getByType(PhpVersion::class), + $container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class), + new OversizedArrayBuilder(), + $container->getParameter('usePathConstantsAsConstantString'), + ), + $container->getByType(DynamicReturnTypeExtensionRegistryProvider::class), + $container->getByType(ExpressionTypeResolverExtensionRegistryProvider::class), + $container->getByType(ExprPrinter::class), + $typeSpecifier, + new PropertyReflectionFinder(), + self::getParser(), + $container->getByType(NodeScopeResolver::class), + $container->getByType(PhpVersion::class), + $container->getParameter('featureToggles')['explicitMixedInUnknownGenericNew'], + $container->getParameter('featureToggles')['explicitMixedForGlobalVariables'], + $constantResolver, + ), ); } /** * @param array $globalTypeAliases */ - public function createTypeAliasResolver(array $globalTypeAliases, ReflectionProvider $reflectionProvider): TypeAliasResolver + public static function createTypeAliasResolver(array $globalTypeAliases, ReflectionProvider $reflectionProvider): TypeAliasResolver { $container = self::getContainer(); @@ -200,7 +215,7 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return true; } - public function getFileHelper(): FileHelper + public static function getFileHelper(): FileHelper { return self::getContainer()->getByType(FileHelper::class); } diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index 6c260beca9..67cae7e669 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -4,22 +4,23 @@ use PhpParser\Node; use PHPStan\Analyser\Analyser; +use PHPStan\Analyser\AnalyserResultFinalizer; use PHPStan\Analyser\Error; use PHPStan\Analyser\FileAnalyser; +use PHPStan\Analyser\LocalIgnoresProcessor; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\RuleErrorTransformer; -use PHPStan\Analyser\ScopeContext; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Collectors\Collector; use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; use PHPStan\File\FileHelper; -use PHPStan\Node\CollectedDataNode; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\StubPhpDocProvider; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\SignatureMap\SignatureMapProvider; use PHPStan\Rules\DirectRegistry as DirectRuleRegistry; use PHPStan\Rules\Properties\DirectReadWritePropertiesExtensionProvider; use PHPStan\Rules\Properties\ReadWritePropertiesExtension; @@ -66,12 +67,9 @@ protected function getTypeSpecifier(): TypeSpecifier return self::getContainer()->getService('typeSpecifier'); } - private function getAnalyser(): Analyser + private function getAnalyser(DirectRuleRegistry $ruleRegistry): Analyser { if ($this->analyser === null) { - $ruleRegistry = new DirectRuleRegistry([ - $this->getRule(), - ]); $collectorRegistry = new CollectorRegistry($this->getCollectors()); $reflectionProvider = $this->createReflectionProvider(); @@ -82,21 +80,27 @@ private function getAnalyser(): Analyser $reflectionProvider, self::getContainer()->getByType(InitializerExprTypeResolver::class), self::getReflector(), - $this->getClassReflectionExtensionRegistryProvider(), + self::getClassReflectionExtensionRegistryProvider(), $this->getParser(), self::getContainer()->getByType(FileTypeMapper::class), self::getContainer()->getByType(StubPhpDocProvider::class), self::getContainer()->getByType(PhpVersion::class), + self::getContainer()->getByType(SignatureMapProvider::class), self::getContainer()->getByType(PhpDocInheritanceResolver::class), self::getContainer()->getByType(FileHelper::class), $typeSpecifier, self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), $readWritePropertiesExtensions !== [] ? new DirectReadWritePropertiesExtensionProvider($readWritePropertiesExtensions) : self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class), + self::createScopeFactory($reflectionProvider, $typeSpecifier), $this->shouldPolluteScopeWithLoopInitialAssignments(), $this->shouldPolluteScopeWithAlwaysIterableForeach(), [], [], - true, + self::getContainer()->getParameter('universalObjectCratesClasses'), + self::getContainer()->getParameter('exceptions')['implicitThrows'], + $this->shouldTreatPhpDocTypesAsCertain(), + self::getContainer()->getParameter('featureToggles')['detectDeadTypeInMultiCatch'], + self::getContainer()->getParameter('featureToggles')['paramOutType'], ); $fileAnalyser = new FileAnalyser( $this->createScopeFactory($reflectionProvider, $typeSpecifier), @@ -104,7 +108,7 @@ private function getAnalyser(): Analyser $this->getParser(), self::getContainer()->getByType(DependencyResolver::class), new RuleErrorTransformer(), - true, + new LocalIgnoresProcessor(), ); $this->analyser = new Analyser( $fileAnalyser, @@ -120,7 +124,7 @@ private function getAnalyser(): Analyser /** * @param string[] $files - * @param list $expectedErrors + * @param list $expectedErrors */ public function analyse(array $files, array $expectedErrors): void { @@ -159,8 +163,11 @@ static function (Error $error) use ($strictlyTypedSprintf): string { */ public function gatherAnalyserErrors(array $files): array { + $ruleRegistry = new DirectRuleRegistry([ + $this->getRule(), + ]); $files = array_map([$this->getFileHelper(), 'normalizePath'], $files); - $analyserResult = $this->getAnalyser()->analyse( + $analyserResult = $this->getAnalyser($ruleRegistry)->analyse( $files, null, null, @@ -170,26 +177,22 @@ public function gatherAnalyserErrors(array $files): array $this->fail(implode("\n", $analyserResult->getInternalErrors())); } - $actualErrors = $analyserResult->getUnorderedErrors(); - $ruleErrorTransformer = new RuleErrorTransformer(); - if (count($analyserResult->getCollectedData()) > 0) { - $ruleRegistry = new DirectRuleRegistry([ - $this->getRule(), - ]); - - $nodeType = CollectedDataNode::class; - $node = new CollectedDataNode($analyserResult->getCollectedData()); - $scopeFactory = $this->createScopeFactory($this->createReflectionProvider(), $this->getTypeSpecifier()); - $scope = $scopeFactory->create(ScopeContext::create('irrelevant')); - foreach ($ruleRegistry->getRules($nodeType) as $rule) { - $ruleErrors = $rule->processNode($node, $scope); - foreach ($ruleErrors as $ruleError) { - $actualErrors[] = $ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getLine()); - } - } + if ($this->shouldFailOnPhpErrors() && count($analyserResult->getAllPhpErrors()) > 0) { + $this->fail(implode("\n", array_map( + static fn (Error $error): string => sprintf('%s on %s:%d', $error->getMessage(), $error->getFile(), $error->getLine()), + $analyserResult->getAllPhpErrors(), + ))); } - return $actualErrors; + $finalizer = new AnalyserResultFinalizer( + $ruleRegistry, + new RuleErrorTransformer(), + $this->createScopeFactory($this->createReflectionProvider(), $this->getTypeSpecifier()), + new LocalIgnoresProcessor(), + true, + ); + + return $finalizer->finalize($analyserResult, false)->getAnalyserResult()->getUnorderedErrors(); } protected function shouldPolluteScopeWithLoopInitialAssignments(): bool @@ -202,6 +205,11 @@ protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool return true; } + protected function shouldFailOnPhpErrors(): bool + { + return true; + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/src/Testing/TestCase.neon b/src/Testing/TestCase.neon index 6f03c38e63..cfa21959b9 100644 --- a/src/Testing/TestCase.neon +++ b/src/Testing/TestCase.neon @@ -9,6 +9,9 @@ services: arguments: phpParser: @phpParserDecorator php8Parser: @php8PhpParser + fileExtensions: %fileExtensions% + obsoleteExcludesAnalyse: %excludes_analyse% + excludePaths: %excludePaths% cacheStorage: class: PHPStan\Cache\MemoryCacheStorage diff --git a/src/Testing/TestCaseSourceLocatorFactory.php b/src/Testing/TestCaseSourceLocatorFactory.php index faeaaea61b..fccedd9ec1 100644 --- a/src/Testing/TestCaseSourceLocatorFactory.php +++ b/src/Testing/TestCaseSourceLocatorFactory.php @@ -12,6 +12,7 @@ use PHPStan\BetterReflection\SourceLocator\Type\MemoizingSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\PhpInternalSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator; use PHPStan\Reflection\BetterReflection\SourceLocator\ComposerJsonAndInstalledJsonSourceLocatorMaker; use PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher; @@ -19,10 +20,20 @@ use ReflectionClass; use function dirname; use function is_file; +use function serialize; +use function sha1; class TestCaseSourceLocatorFactory { + /** @var array> */ + private static array $composerSourceLocatorsCache = []; + + /** + * @param string[] $fileExtensions + * @param string[] $obsoleteExcludesAnalyse + * @param array{analyse?: array, analyseAndScan?: array}|null $excludePaths + */ public function __construct( private ComposerJsonAndInstalledJsonSourceLocatorMaker $composerJsonAndInstalledJsonSourceLocatorMaker, private Parser $phpParser, @@ -30,6 +41,10 @@ public function __construct( private FileNodesFetcher $fileNodesFetcher, private PhpStormStubsSourceStubber $phpstormStubsSourceStubber, private ReflectionSourceStubber $reflectionSourceStubber, + private PhpVersion $phpVersion, + private array $fileExtensions, + private array $obsoleteExcludesAnalyse, + private ?array $excludePaths, ) { } @@ -38,8 +53,14 @@ public function create(): SourceLocator { $classLoaders = ClassLoader::getRegisteredLoaders(); $classLoaderReflection = new ReflectionClass(ClassLoader::class); - $locators = []; - if ($classLoaderReflection->hasProperty('vendorDir')) { + $cacheKey = sha1(serialize([ + $this->phpVersion->getVersionId(), + $this->fileExtensions, + $this->obsoleteExcludesAnalyse, + $this->excludePaths, + ])); + if ($classLoaderReflection->hasProperty('vendorDir') && ! isset(self::$composerSourceLocatorsCache[$cacheKey])) { + $composerLocators = []; $vendorDirProperty = $classLoaderReflection->getProperty('vendorDir'); $vendorDirProperty->setAccessible(true); foreach ($classLoaders as $classLoader) { @@ -52,10 +73,13 @@ public function create(): SourceLocator if ($composerSourceLocator === null) { continue; } - $locators[] = $composerSourceLocator; + $composerLocators[] = $composerSourceLocator; } + + self::$composerSourceLocatorsCache[$cacheKey] = $composerLocators; } + $locators = self::$composerSourceLocatorsCache[$cacheKey] ?? []; $astLocator = new Locator($this->phpParser); $astPhp8Locator = new Locator($this->php8Parser); diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index 7ed0ef7eec..ac41cb5620 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -14,15 +14,21 @@ use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\StubPhpDocProvider; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\SignatureMap\SignatureMapProvider; use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; use PHPStan\TrinaryLogic; +use PHPStan\Type\ConstantScalarType; use PHPStan\Type\FileTypeMapper; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function array_map; use function array_merge; use function count; +use function in_array; use function is_string; use function sprintf; +use function stripos; +use function strtolower; /** @api */ abstract class TypeInferenceTestCase extends PHPStanTestCase @@ -32,42 +38,48 @@ abstract class TypeInferenceTestCase extends PHPStanTestCase * @param callable(Node , Scope ): void $callback * @param string[] $dynamicConstantNames */ - public function processFile( + public static function processFile( string $file, callable $callback, array $dynamicConstantNames = [], ): void { - $reflectionProvider = $this->createReflectionProvider(); + $reflectionProvider = self::createReflectionProvider(); $typeSpecifier = self::getContainer()->getService('typeSpecifier'); $fileHelper = self::getContainer()->getByType(FileHelper::class); $resolver = new NodeScopeResolver( $reflectionProvider, self::getContainer()->getByType(InitializerExprTypeResolver::class), self::getReflector(), - $this->getClassReflectionExtensionRegistryProvider(), - $this->getParser(), + self::getClassReflectionExtensionRegistryProvider(), + self::getParser(), self::getContainer()->getByType(FileTypeMapper::class), self::getContainer()->getByType(StubPhpDocProvider::class), self::getContainer()->getByType(PhpVersion::class), + self::getContainer()->getByType(SignatureMapProvider::class), self::getContainer()->getByType(PhpDocInheritanceResolver::class), self::getContainer()->getByType(FileHelper::class), $typeSpecifier, self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class), - true, - true, - $this->getEarlyTerminatingMethodCalls(), - $this->getEarlyTerminatingFunctionCalls(), - true, + self::createScopeFactory($reflectionProvider, $typeSpecifier), + self::getContainer()->getParameter('polluteScopeWithLoopInitialAssignments'), + self::getContainer()->getParameter('polluteScopeWithAlwaysIterableForeach'), + static::getEarlyTerminatingMethodCalls(), + static::getEarlyTerminatingFunctionCalls(), + self::getContainer()->getParameter('universalObjectCratesClasses'), + self::getContainer()->getParameter('exceptions')['implicitThrows'], + self::getContainer()->getParameter('treatPhpDocTypesAsCertain'), + self::getContainer()->getParameter('featureToggles')['detectDeadTypeInMultiCatch'], + self::getContainer()->getParameter('featureToggles')['paramOutType'], ); - $resolver->setAnalysedFiles(array_map(static fn (string $file): string => $fileHelper->normalizePath($file), array_merge([$file], $this->getAdditionalAnalysedFiles()))); + $resolver->setAnalysedFiles(array_map(static fn (string $file): string => $fileHelper->normalizePath($file), array_merge([$file], static::getAdditionalAnalysedFiles()))); - $scopeFactory = $this->createScopeFactory($reflectionProvider, $typeSpecifier, $dynamicConstantNames); + $scopeFactory = self::createScopeFactory($reflectionProvider, $typeSpecifier, $dynamicConstantNames); $scope = $scopeFactory->create(ScopeContext::create($file)); $resolver->processNodes( - $this->getParser()->parseFile($file), + self::getParser()->parseFile($file), $scope, $callback, ); @@ -84,10 +96,18 @@ public function assertFileAsserts( ): void { if ($assertType === 'type') { - $expectedType = $args[0]; - $expected = $expectedType->getValue(); - $actualType = $args[1]; - $actual = $actualType->describe(VerbosityLevel::precise()); + if ($args[0] instanceof Type) { + // backward compatibility + $expectedType = $args[0]; + $this->assertInstanceOf(ConstantScalarType::class, $expectedType); + $expected = $expectedType->getValue(); + $actualType = $args[1]; + $actual = $actualType->describe(VerbosityLevel::precise()); + } else { + $expected = $args[0]; + $actual = $args[1]; + } + $this->assertSame( $expected, $actual, @@ -99,7 +119,7 @@ public function assertFileAsserts( $variableName = $args[2]; $this->assertTrue( $expectedCertainty->equals($actualCertainty), - sprintf('Expected %s, actual certainty of variable $%s is %s', $expectedCertainty->describe(), $variableName, $actualCertainty->describe()), + sprintf('Expected %s, actual certainty of variable $%s is %s in %s on line %d.', $expectedCertainty->describe(), $variableName, $actualCertainty->describe(), $file, $args[3]), ); } } @@ -108,10 +128,10 @@ public function assertFileAsserts( * @api * @return array */ - public function gatherAssertTypes(string $file): array + public static function gatherAssertTypes(string $file): array { $asserts = []; - $this->processFile($file, function (Node $node, Scope $scope) use (&$asserts, $file): void { + self::processFile($file, static function (Node $node, Scope $scope) use (&$asserts, $file): void { if (!$node instanceof Node\Expr\FuncCall) { return; } @@ -122,76 +142,116 @@ public function gatherAssertTypes(string $file): array } $functionName = $nameNode->toString(); - if ($functionName === 'PHPStan\\Testing\\assertType') { + if (in_array(strtolower($functionName), ['asserttype', 'assertnativetype', 'assertvariablecertainty'], true)) { + self::fail(sprintf( + 'Missing use statement for %s() on line %d.', + $functionName, + $node->getStartLine(), + )); + } elseif ($functionName === 'PHPStan\\Testing\\assertType') { $expectedType = $scope->getType($node->getArgs()[0]->value); + if (!$expectedType instanceof ConstantScalarType) { + self::fail(sprintf('Expected type must be a literal string, %s given on line %d.', $expectedType->describe(VerbosityLevel::precise()), $node->getLine())); + } $actualType = $scope->getType($node->getArgs()[1]->value); - $assert = ['type', $file, $expectedType, $actualType, $node->getLine()]; + $assert = ['type', $file, $expectedType->getValue(), $actualType->describe(VerbosityLevel::precise()), $node->getStartLine()]; } elseif ($functionName === 'PHPStan\\Testing\\assertNativeType') { - $nativeScope = $scope->doNotTreatPhpDocTypesAsCertain(); - $expectedType = $nativeScope->getNativeType($node->getArgs()[0]->value); - $actualType = $nativeScope->getNativeType($node->getArgs()[1]->value); - $assert = ['type', $file, $expectedType, $actualType, $node->getLine()]; + $expectedType = $scope->getType($node->getArgs()[0]->value); + if (!$expectedType instanceof ConstantScalarType) { + self::fail(sprintf('Expected type must be a literal string, %s given on line %d.', $expectedType->describe(VerbosityLevel::precise()), $node->getLine())); + } + + $actualType = $scope->getNativeType($node->getArgs()[1]->value); + $assert = ['type', $file, $expectedType->getValue(), $actualType->describe(VerbosityLevel::precise()), $node->getStartLine()]; } elseif ($functionName === 'PHPStan\\Testing\\assertVariableCertainty') { $certainty = $node->getArgs()[0]->value; if (!$certainty instanceof StaticCall) { - $this->fail(sprintf('First argument of %s() must be TrinaryLogic call', $functionName)); + self::fail(sprintf('First argument of %s() must be TrinaryLogic call', $functionName)); } if (!$certainty->class instanceof Node\Name) { - $this->fail(sprintf('ERROR: Invalid TrinaryLogic call.')); + self::fail(sprintf('ERROR: Invalid TrinaryLogic call.')); } if ($certainty->class->toString() !== 'PHPStan\\TrinaryLogic') { - $this->fail(sprintf('ERROR: Invalid TrinaryLogic call.')); + self::fail(sprintf('ERROR: Invalid TrinaryLogic call.')); } if (!$certainty->name instanceof Node\Identifier) { - $this->fail(sprintf('ERROR: Invalid TrinaryLogic call.')); + self::fail(sprintf('ERROR: Invalid TrinaryLogic call.')); } - // @phpstan-ignore-next-line + // @phpstan-ignore staticMethod.dynamicName $expectedertaintyValue = TrinaryLogic::{$certainty->name->toString()}(); $variable = $node->getArgs()[1]->value; if (!$variable instanceof Node\Expr\Variable) { - $this->fail(sprintf('ERROR: Invalid assertVariableCertainty call.')); + self::fail(sprintf('ERROR: Invalid assertVariableCertainty call.')); } if (!is_string($variable->name)) { - $this->fail(sprintf('ERROR: Invalid assertVariableCertainty call.')); + self::fail(sprintf('ERROR: Invalid assertVariableCertainty call.')); } $actualCertaintyValue = $scope->hasVariableType($variable->name); - $assert = ['variableCertainty', $file, $expectedertaintyValue, $actualCertaintyValue, $variable->name]; + $assert = ['variableCertainty', $file, $expectedertaintyValue, $actualCertaintyValue, $variable->name, $node->getStartLine()]; } else { - return; + $correctFunction = null; + + $assertFunctions = [ + 'assertType' => 'PHPStan\\Testing\\assertType', + 'assertNativeType' => 'PHPStan\\Testing\\assertNativeType', + 'assertVariableCertainty' => 'PHPStan\\Testing\\assertVariableCertainty', + ]; + foreach ($assertFunctions as $assertFn => $fqFunctionName) { + if (stripos($functionName, $assertFn) === false) { + continue; + } + + $correctFunction = $fqFunctionName; + } + + if ($correctFunction === null) { + return; + } + + self::fail(sprintf( + 'Function %s imported with wrong namespace %s called on line %d.', + $correctFunction, + $functionName, + $node->getStartLine(), + )); } if (count($node->getArgs()) !== 2) { - $this->fail(sprintf( + self::fail(sprintf( 'ERROR: Wrong %s() call on line %d.', $functionName, - $node->getLine(), + $node->getStartLine(), )); } - $asserts[$file . ':' . $node->getLine()] = $assert; + $asserts[$file . ':' . $node->getStartLine()] = $assert; }); + if (count($asserts) === 0) { + self::fail(sprintf('File %s does not contain any asserts', $file)); + } + return $asserts; } /** @return string[] */ - protected function getAdditionalAnalysedFiles(): array + protected static function getAdditionalAnalysedFiles(): array { return []; } /** @return string[][] */ - protected function getEarlyTerminatingMethodCalls(): array + protected static function getEarlyTerminatingMethodCalls(): array { return []; } /** @return string[] */ - protected function getEarlyTerminatingFunctionCalls(): array + protected static function getEarlyTerminatingFunctionCalls(): array { return []; } diff --git a/src/TrinaryLogic.php b/src/TrinaryLogic.php index a752540ba6..df4e9f5218 100644 --- a/src/TrinaryLogic.php +++ b/src/TrinaryLogic.php @@ -10,7 +10,7 @@ /** * @api - * @see https://en.wikipedia.org/wiki/Three-valued_logic + * @see https://phpstan.org/developing-extensions/trinary-logic */ class TrinaryLogic { @@ -94,6 +94,10 @@ public function lazyAnd( callable $callback, ): self { + if ($this->no()) { + return $this; + } + $results = []; foreach ($objects as $object) { $result = $callback($object); @@ -124,6 +128,10 @@ public function lazyOr( callable $callback, ): self { + if ($this->yes()) { + return $this; + } + $results = []; foreach ($objects as $object) { $result = $callback($object); @@ -158,24 +166,25 @@ public static function lazyExtremeIdentity( callable $callback, ): self { + if ($objects === []) { + throw new ShouldNotHappenException(); + } + $lastResult = null; - $results = []; foreach ($objects as $object) { $result = $callback($object); if ($lastResult === null) { $lastResult = $result; - $results[] = $result; continue; } if ($lastResult->equals($result)) { - $results[] = $result; continue; } return self::createMaybe(); } - return self::extremeIdentity(...$results); + return $lastResult; } public static function maxMin(self ...$operands): self diff --git a/src/Type/AcceptsResult.php b/src/Type/AcceptsResult.php new file mode 100644 index 0000000000..fd30d8badd --- /dev/null +++ b/src/Type/AcceptsResult.php @@ -0,0 +1,124 @@ + $reasons + */ + public function __construct( + public readonly TrinaryLogic $result, + public readonly array $reasons, + ) + { + } + + public function yes(): bool + { + return $this->result->yes(); + } + + public function maybe(): bool + { + return $this->result->maybe(); + } + + public function no(): bool + { + return $this->result->no(); + } + + public static function createYes(): self + { + return new self(TrinaryLogic::createYes(), []); + } + + public static function createNo(): self + { + return new self(TrinaryLogic::createNo(), []); + } + + public static function createMaybe(): self + { + return new self(TrinaryLogic::createMaybe(), []); + } + + public static function createFromBoolean(bool $value): self + { + return new self(TrinaryLogic::createFromBoolean($value), []); + } + + public function and(self $other): self + { + return new self( + $this->result->and($other->result), + array_values(array_unique(array_merge($this->reasons, $other->reasons))), + ); + } + + public function or(self $other): self + { + return new self( + $this->result->or($other->result), + array_values(array_unique(array_merge($this->reasons, $other->reasons))), + ); + } + + /** + * @param callable(string): string $cb + */ + public function decorateReasons(callable $cb): self + { + $reasons = []; + foreach ($this->reasons as $reason) { + $reasons[] = $cb($reason); + } + + return new self($this->result, $reasons); + } + + public static function extremeIdentity(self ...$operands): self + { + if ($operands === []) { + throw new ShouldNotHappenException(); + } + + $result = TrinaryLogic::extremeIdentity(...array_map(static fn (self $result) => $result->result, $operands)); + $reasons = []; + foreach ($operands as $operand) { + foreach ($operand->reasons as $reason) { + $reasons[] = $reason; + } + } + + return new self($result, $reasons); + } + + public static function maxMin(self ...$operands): self + { + if ($operands === []) { + throw new ShouldNotHappenException(); + } + + $result = TrinaryLogic::maxMin(...array_map(static fn (self $result) => $result->result, $operands)); + $reasons = []; + foreach ($operands as $operand) { + foreach ($operand->reasons as $reason) { + $reasons[] = $reason; + } + } + + return new self($result, $reasons); + } + +} diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php new file mode 100644 index 0000000000..43ebf895c2 --- /dev/null +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -0,0 +1,486 @@ +acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + { + if ($type instanceof CompoundType) { + return $type->isAcceptedWithReasonBy($this, $strictTypes); + } + + $isArray = $type->isArray(); + $isList = $type->isList(); + + return new AcceptsResult($isArray->and($isList), []); + } + + public function isSuperTypeOf(Type $type): TrinaryLogic + { + if ($this->equals($type)) { + return TrinaryLogic::createYes(); + } + + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + return $type->isArray() + ->and($type->isList()); + } + + public function isSubTypeOf(Type $otherType): TrinaryLogic + { + if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOf($this); + } + + return $otherType->isArray() + ->and($otherType->isList()) + ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + { + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return new AcceptsResult($this->isSubTypeOf($acceptingType), []); + } + + public function equals(Type $type): bool + { + return $type instanceof self; + } + + public function describe(VerbosityLevel $level): string + { + return 'list'; + } + + public function isOffsetAccessible(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function hasOffsetValueType(Type $offsetType): TrinaryLogic + { + return $this->getIterableKeyType()->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); + } + + public function getOffsetValueType(Type $offsetType): Type + { + return new MixedType(); + } + + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + if ($offsetType === null || (new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()) { + return $this; + } + + return new ErrorType(); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()) { + return $this; + } + + return new ErrorType(); + } + + public function unsetOffset(Type $offsetType): Type + { + if ($this->hasOffsetValueType($offsetType)->no()) { + return $this; + } + + return new ErrorType(); + } + + public function getKeysArray(): Type + { + return $this; + } + + public function getValuesArray(): Type + { + return $this; + } + + public function fillKeysArray(Type $valueType): Type + { + return new MixedType(); + } + + public function flipArray(): Type + { + return new MixedType(); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + if ($otherArraysType->isList()->yes()) { + return $this; + } + + return new MixedType(); + } + + public function popArray(): Type + { + return $this; + } + + public function searchArray(Type $needleType): Type + { + return new MixedType(); + } + + public function shiftArray(): Type + { + return $this; + } + + public function shuffleArray(): Type + { + return $this; + } + + public function isIterable(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isIterableAtLeastOnce(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getArraySize(): Type + { + return IntegerRangeType::fromInterval(0, null); + } + + public function getIterableKeyType(): Type + { + return IntegerRangeType::fromInterval(0, null); + } + + public function getFirstIterableKeyType(): Type + { + return new ConstantIntegerType(0); + } + + public function getLastIterableKeyType(): Type + { + return $this->getIterableKeyType(); + } + + public function getIterableValueType(): Type + { + return new MixedType(); + } + + public function getFirstIterableValueType(): Type + { + return new MixedType(); + } + + public function getLastIterableValueType(): Type + { + return new MixedType(); + } + + public function isArray(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isConstantArray(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isList(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function toNumber(): Type + { + return new ErrorType(); + } + + public function toInteger(): Type + { + return TypeCombinator::union( + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ); + } + + public function toFloat(): Type + { + return TypeCombinator::union( + new ConstantFloatType(0.0), + new ConstantFloatType(1.0), + ); + } + + public function toString(): Type + { + return new ErrorType(); + } + + public function toArray(): Type + { + return $this; + } + + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function traverse(callable $cb): Type + { + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public static function __set_state(array $properties): Type + { + return new self(); + } + + public static function setListTypeEnabled(bool $enabled): void + { + self::$enabled = $enabled; + } + + public static function isListTypeEnabled(): bool + { + return self::$enabled; + } + + public static function intersectWith(Type $type): Type + { + if (self::$enabled) { + return TypeCombinator::intersect($type, new self()); + } + + return $type; + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('list'); + } + +} diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index 3da6c5cbfb..3fd1b8a356 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -2,7 +2,12 @@ namespace PHPStan\Type\Accessory; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantArrayType; @@ -13,8 +18,10 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; @@ -28,6 +35,7 @@ class AccessoryLiteralStringType implements CompoundType, AccessoryType { use MaybeCallableTypeTrait; + use NonArrayTypeTrait; use NonObjectTypeTrait; use NonIterableTypeTrait; use UndecidedComparisonCompoundTypeTrait; @@ -44,16 +52,36 @@ public function getReferencedClasses(): array return []; } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof MixedType) { - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return $type->isLiteralString(); + return new AcceptsResult($type->isLiteralString(), []); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -81,7 +109,12 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - return $this->isSubTypeOf($acceptingType); + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return new AcceptsResult($this->isSubTypeOf($acceptingType), []); } public function equals(Type $type): bool @@ -101,7 +134,7 @@ public function isOffsetAccessible(): TrinaryLogic public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - return (new IntegerType())->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); + return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); } public function getOffsetValueType(Type $offsetType): Type @@ -118,19 +151,14 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } - public function unsetOffset(Type $offsetType): Type - { - return new ErrorType(); - } - - public function isArray(): TrinaryLogic + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - return TrinaryLogic::createNo(); + return $this; } - public function isOversizedArray(): TrinaryLogic + public function unsetOffset(Type $offsetType): Type { - return TrinaryLogic::createNo(); + return new ErrorType(); } public function toNumber(): Type @@ -164,9 +192,66 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], + [], + TrinaryLogic::createYes(), ); } + public function toArrayKey(): Type + { + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -192,11 +277,51 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function hasMethod(string $methodName): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function traverse(callable $cb): Type { return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + public function generalize(GeneralizePrecision $precision): Type { return new StringType(); @@ -207,4 +332,22 @@ public static function __set_state(array $properties): Type return new self(); } + public function exponentiate(Type $exponent): Type + { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('literal-string'); + } + } diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 50aad6085a..771dec0e22 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -2,7 +2,13 @@ namespace PHPStan\Type\Accessory; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -12,8 +18,10 @@ use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; @@ -28,6 +36,7 @@ class AccessoryNonEmptyStringType implements CompoundType, AccessoryType { use MaybeCallableTypeTrait; + use NonArrayTypeTrait; use NonObjectTypeTrait; use NonIterableTypeTrait; use UndecidedComparisonCompoundTypeTrait; @@ -44,13 +53,33 @@ public function getReferencedClasses(): array return []; } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return $type->isNonEmptyString(); + return new AcceptsResult($type->isNonEmptyString(), []); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -82,7 +111,12 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - return $this->isSubTypeOf($acceptingType); + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return new AcceptsResult($this->isSubTypeOf($acceptingType), []); } public function equals(Type $type): bool @@ -102,7 +136,7 @@ public function isOffsetAccessible(): TrinaryLogic public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - return (new IntegerType())->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); + return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); } public function getOffsetValueType(Type $offsetType): Type @@ -123,19 +157,14 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } - public function unsetOffset(Type $offsetType): Type + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - return new ErrorType(); - } - - public function isArray(): TrinaryLogic - { - return TrinaryLogic::createNo(); + return $this; } - public function isOversizedArray(): TrinaryLogic + public function unsetOffset(Type $offsetType): Type { - return TrinaryLogic::createNo(); + return new ErrorType(); } public function toNumber(): Type @@ -164,9 +193,66 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], + [], + TrinaryLogic::createYes(), ); } + public function toArrayKey(): Type + { + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -192,11 +278,46 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function traverse(callable $cb): Type { return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + public function generalize(GeneralizePrecision $precision): Type { return new StringType(); @@ -216,4 +337,22 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } + public function exponentiate(Type $exponent): Type + { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('non-empty-string'); + } + } diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php index 0eadc169ee..cf4402f059 100644 --- a/src/Type/Accessory/AccessoryNonFalsyStringType.php +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -2,18 +2,25 @@ namespace PHPStan\Type\Accessory; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; use PHPStan\Type\GeneralizePrecision; -use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; @@ -28,6 +35,7 @@ class AccessoryNonFalsyStringType implements CompoundType, AccessoryType { use MaybeCallableTypeTrait; + use NonArrayTypeTrait; use NonObjectTypeTrait; use NonIterableTypeTrait; use TruthyBooleanTypeTrait; @@ -45,13 +53,33 @@ public function getReferencedClasses(): array return []; } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return $type->isNonFalsyString(); + return new AcceptsResult($type->isNonFalsyString(), []); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -83,7 +111,12 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - return $this->isSubTypeOf($acceptingType); + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return new AcceptsResult($this->isSubTypeOf($acceptingType), []); } public function equals(Type $type): bool @@ -103,7 +136,7 @@ public function isOffsetAccessible(): TrinaryLogic public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - return (new IntegerType())->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); + return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); } public function getOffsetValueType(Type $offsetType): Type @@ -120,19 +153,14 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } - public function unsetOffset(Type $offsetType): Type - { - return new ErrorType(); - } - - public function isArray(): TrinaryLogic + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - return TrinaryLogic::createNo(); + return $this; } - public function isOversizedArray(): TrinaryLogic + public function unsetOffset(Type $offsetType): Type { - return TrinaryLogic::createNo(); + return new ErrorType(); } public function toNumber(): Type @@ -142,10 +170,7 @@ public function toNumber(): Type public function toInteger(): Type { - return new UnionType([ - IntegerRangeType::fromInterval(null, -1), - IntegerRangeType::fromInterval(1, null), - ]); + return new IntegerType(); } public function toFloat(): Type @@ -164,9 +189,66 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], + [], + TrinaryLogic::createYes(), ); } + public function toArrayKey(): Type + { + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -192,11 +274,46 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function traverse(callable $cb): Type { return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + public function generalize(GeneralizePrecision $precision): Type { return new StringType(); @@ -207,4 +324,22 @@ public static function __set_state(array $properties): Type return new self(); } + public function exponentiate(Type $exponent): Type + { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('non-falsy-string'); + } + } diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 67fb6472f1..a15078542b 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -2,7 +2,13 @@ namespace PHPStan\Type\Accessory; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -13,6 +19,7 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonCallableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; @@ -27,6 +34,7 @@ class AccessoryNumericStringType implements CompoundType, AccessoryType { + use NonArrayTypeTrait; use NonCallableTypeTrait; use NonObjectTypeTrait; use NonIterableTypeTrait; @@ -44,13 +52,33 @@ public function getReferencedClasses(): array return []; } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return $type->isNumericString(); + return new AcceptsResult($type->isNumericString(), []); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -77,16 +105,21 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic } public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + { + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult { if ($acceptingType->isNonFalsyString()->yes()) { - return TrinaryLogic::createMaybe(); + return AcceptsResult::createMaybe(); } if ($acceptingType->isNonEmptyString()->yes()) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } - return $this->isSubTypeOf($acceptingType); + return new AcceptsResult($this->isSubTypeOf($acceptingType), []); } public function equals(Type $type): bool @@ -106,7 +139,7 @@ public function isOffsetAccessible(): TrinaryLogic public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - return (new IntegerType())->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); + return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); } public function getOffsetValueType(Type $offsetType): Type @@ -123,19 +156,14 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } - public function unsetOffset(Type $offsetType): Type - { - return new ErrorType(); - } - - public function isArray(): TrinaryLogic + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - return TrinaryLogic::createNo(); + return $this; } - public function isOversizedArray(): TrinaryLogic + public function unsetOffset(Type $offsetType): Type { - return TrinaryLogic::createNo(); + return new ErrorType(); } public function toNumber(): Type @@ -167,9 +195,66 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], + [], + TrinaryLogic::createYes(), ); } + public function toArrayKey(): Type + { + return new IntegerType(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -195,11 +280,46 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function traverse(callable $cb): Type { return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + public function generalize(GeneralizePrecision $precision): Type { return new StringType(); @@ -219,4 +339,22 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } + public function exponentiate(Type $exponent): Type + { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('numeric-string'); + } + } diff --git a/src/Type/Accessory/HasMethodType.php b/src/Type/Accessory/HasMethodType.php index 217c16ff5b..070f853329 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -2,15 +2,20 @@ namespace PHPStan\Type\Accessory; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\Dummy\DummyMethodReflection; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; +use PHPStan\Type\ErrorType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\StringType; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonRemoveableTypeTrait; @@ -41,6 +46,16 @@ public function getReferencedClasses(): array return []; } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + private function getCanonicalMethodName(): string { return strtolower($this->methodName); @@ -48,7 +63,16 @@ private function getCanonicalMethodName(): string public function accepts(Type $type, bool $strictTypes): TrinaryLogic { - return TrinaryLogic::createFromBoolean($this->equals($type)); + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + { + if ($type instanceof CompoundType) { + return $type->isAcceptedWithReasonBy($this, $strictTypes); + } + + return AcceptsResult::createFromBoolean($this->equals($type)); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -77,7 +101,12 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - return $this->isSubTypeOf($acceptingType); + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return new AcceptsResult($this->isSubTypeOf($acceptingType), []); } public function equals(Type $type): bool @@ -100,7 +129,7 @@ public function hasMethod(string $methodName): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -125,6 +154,15 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function toString(): Type + { + if ($this->getCanonicalMethodName() === '__tostring') { + return new StringType(); + } + + return new ErrorType(); + } + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return [ @@ -132,14 +170,39 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) ]; } + public function getEnumCases(): array + { + return []; + } + public function traverse(callable $cb): Type { return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + public static function __set_state(array $properties): Type { return new self($properties['methodName']); } + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode(''); // no PHPDoc representation + } + } diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index 8760f1ffd7..2681b9ebb5 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -2,14 +2,20 @@ namespace PHPStan\Type\Accessory; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ErrorType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\Traits\MaybeArrayTypeTrait; use PHPStan\Type\Traits\MaybeCallableTypeTrait; use PHPStan\Type\Traits\MaybeIterableTypeTrait; use PHPStan\Type\Traits\MaybeObjectTypeTrait; @@ -26,6 +32,7 @@ class HasOffsetType implements CompoundType, AccessoryType { + use MaybeArrayTypeTrait; use MaybeCallableTypeTrait; use MaybeIterableTypeTrait; use MaybeObjectTypeTrait; @@ -56,14 +63,33 @@ public function getReferencedClasses(): array return []; } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return $type->isOffsetAccessible() - ->and($type->hasOffsetValueType($this->offsetType)); + return new AcceptsResult($type->isOffsetAccessible()->and($type->hasOffsetValueType($this->offsetType)), []); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -88,7 +114,12 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - return $this->isSubTypeOf($acceptingType); + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return new AcceptsResult($this->isSubTypeOf($acceptingType), []); } public function equals(Type $type): bool @@ -109,7 +140,7 @@ public function isOffsetAccessible(): TrinaryLogic public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - if ($offsetType instanceof ConstantScalarType && $offsetType->equals($this->offsetType)) { + if ($offsetType->isConstantScalarValue()->yes() && $offsetType->equals($this->offsetType)) { return TrinaryLogic::createYes(); } @@ -126,6 +157,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { if ($this->offsetType->isSuperTypeOf($offsetType)->yes()) { @@ -134,19 +170,87 @@ public function unsetOffset(Type $offsetType): Type return $this; } + public function fillKeysArray(Type $valueType): Type + { + return new NonEmptyArrayType(); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + if ($otherArraysType->hasOffsetValueType($this->offsetType)->yes()) { + return $this; + } + + return new MixedType(); + } + + public function shuffleArray(): Type + { + return new NonEmptyArrayType(); + } + public function isIterableAtLeastOnce(): TrinaryLogic { return TrinaryLogic::createYes(); } - public function isArray(): TrinaryLogic + public function isList(): TrinaryLogic { + if ($this->offsetType->isString()->yes()) { + return TrinaryLogic::createNo(); + } + return TrinaryLogic::createMaybe(); } - public function isOversizedArray(): TrinaryLogic + public function isNull(): TrinaryLogic { - return TrinaryLogic::createMaybe(); + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); } public function isString(): TrinaryLogic @@ -174,6 +278,46 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function getKeysArray(): Type + { + return new NonEmptyArrayType(); + } + + public function getValuesArray(): Type + { + return new NonEmptyArrayType(); + } + public function toNumber(): Type { return new ErrorType(); @@ -199,14 +343,44 @@ public function toArray(): Type return new MixedType(); } + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function getEnumCases(): array + { + return []; + } + public function traverse(callable $cb): Type { return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + public static function __set_state(array $properties): Type { return new self($properties['offsetType']); } + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode(''); // no PHPDoc representation + } + } diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index bf0dfbe054..d230bbff48 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -2,8 +2,13 @@ namespace PHPStan\Type\Accessory; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; @@ -11,6 +16,8 @@ use PHPStan\Type\ErrorType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\Traits\MaybeArrayTypeTrait; use PHPStan\Type\Traits\MaybeCallableTypeTrait; use PHPStan\Type\Traits\MaybeIterableTypeTrait; use PHPStan\Type\Traits\MaybeObjectTypeTrait; @@ -27,6 +34,7 @@ class HasOffsetValueType implements CompoundType, AccessoryType { + use MaybeArrayTypeTrait; use MaybeCallableTypeTrait; use MaybeIterableTypeTrait; use MaybeObjectTypeTrait; @@ -55,15 +63,38 @@ public function getReferencedClasses(): array return []; } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return $type->isOffsetAccessible() - ->and($type->hasOffsetValueType($this->offsetType)) - ->and($this->valueType->accepts($type->getOffsetValueType($this->offsetType), $strictTypes)); + return new AcceptsResult( + $type->isOffsetAccessible() + ->and($type->hasOffsetValueType($this->offsetType)) + ->and($this->valueType->accepts($type->getOffsetValueType($this->offsetType), $strictTypes)), + [], + ); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -90,7 +121,12 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - return $this->isSubTypeOf($acceptingType); + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return new AcceptsResult($this->isSubTypeOf($acceptingType), []); } public function equals(Type $type): bool @@ -112,7 +148,7 @@ public function isOffsetAccessible(): TrinaryLogic public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - if ($offsetType instanceof ConstantScalarType && $offsetType->equals($this->offsetType)) { + if ($offsetType->isConstantScalarValue()->yes() && $offsetType->equals($this->offsetType)) { return TrinaryLogic::createYes(); } @@ -121,7 +157,7 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic public function getOffsetValueType(Type $offsetType): Type { - if ($offsetType instanceof ConstantScalarType && $offsetType->equals($this->offsetType)) { + if ($offsetType->isConstantScalarValue()->yes() && $offsetType->equals($this->offsetType)) { return $this->valueType; } @@ -145,6 +181,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new self($offsetType, $valueType); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return new self($this->offsetType, $valueType); + } + public function unsetOffset(Type $offsetType): Type { if ($this->offsetType->isSuperTypeOf($offsetType)->yes()) { @@ -153,19 +194,119 @@ public function unsetOffset(Type $offsetType): Type return $this; } + public function getKeysArray(): Type + { + return new NonEmptyArrayType(); + } + + public function getValuesArray(): Type + { + return new NonEmptyArrayType(); + } + + public function fillKeysArray(Type $valueType): Type + { + return new NonEmptyArrayType(); + } + + public function flipArray(): Type + { + $valueType = $this->valueType->toArrayKey(); + if ($valueType instanceof ConstantIntegerType || $valueType instanceof ConstantStringType) { + return new self($valueType, $this->offsetType); + } + + return new MixedType(); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + if ($otherArraysType->hasOffsetValueType($this->offsetType)->yes()) { + return $this; + } + + return new MixedType(); + } + + public function searchArray(Type $needleType): Type + { + if ( + $needleType instanceof ConstantScalarType && $this->valueType instanceof ConstantScalarType + && $needleType->getValue() === $this->valueType->getValue() + ) { + return $this->offsetType; + } + + return new MixedType(); + } + + public function shuffleArray(): Type + { + return new NonEmptyArrayType(); + } + public function isIterableAtLeastOnce(): TrinaryLogic { return TrinaryLogic::createYes(); } - public function isArray(): TrinaryLogic + public function isList(): TrinaryLogic { + if ($this->offsetType->isString()->yes()) { + return TrinaryLogic::createNo(); + } + return TrinaryLogic::createMaybe(); } - public function isOversizedArray(): TrinaryLogic + public function isNull(): TrinaryLogic { - return TrinaryLogic::createMaybe(); + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); } public function isString(): TrinaryLogic @@ -193,6 +334,36 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function toNumber(): Type { return new ErrorType(); @@ -218,6 +389,16 @@ public function toArray(): Type return new MixedType(); } + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function getEnumCases(): array + { + return []; + } + public function traverse(callable $cb): Type { $newValueType = $cb($this->valueType); @@ -228,9 +409,34 @@ public function traverse(callable $cb): Type return new self($this->offsetType, $newValueType); } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + $newValueType = $cb($this->valueType, $right->getOffsetValueType($this->offsetType)); + if ($newValueType === $this->valueType) { + return $this; + } + + return new self($this->offsetType, $newValueType); + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + public static function __set_state(array $properties): Type { return new self($properties['offsetType'], $properties['valueType']); } + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode(''); // no PHPDoc representation + } + } diff --git a/src/Type/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index 1d65057878..53170faac3 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -2,10 +2,14 @@ namespace PHPStan\Type\Accessory; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; +use PHPStan\Type\ErrorType; use PHPStan\Type\IntersectionType; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; @@ -39,6 +43,21 @@ public function getReferencedClasses(): array return []; } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + public function getPropertyName(): string { return $this->propertyName; @@ -46,7 +65,16 @@ public function getPropertyName(): string public function accepts(Type $type, bool $strictTypes): TrinaryLogic { - return TrinaryLogic::createFromBoolean($this->equals($type)); + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + { + if ($type instanceof CompoundType) { + return $type->isAcceptedWithReasonBy($this, $strictTypes); + } + + return AcceptsResult::createFromBoolean($this->equals($type)); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -71,7 +99,12 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - return $this->isSubTypeOf($acceptingType); + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return new AcceptsResult($this->isSubTypeOf($acceptingType), []); } public function equals(Type $type): bool @@ -99,14 +132,39 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) return [new TrivialParametersAcceptor()]; } + public function getEnumCases(): array + { + return []; + } + public function traverse(callable $cb): Type { return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + public static function __set_state(array $properties): Type { return new self($properties['propertyName']); } + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode(''); // no PHPDoc representation + } + } diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index 430d14c231..cdd7d0b803 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -2,11 +2,17 @@ namespace PHPStan\Type\Accessory; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ErrorType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; @@ -41,14 +47,46 @@ public function getReferencedClasses(): array return []; } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getArrays(): array + { + return []; + } + + public function getConstantArrays(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return $type->isArray() - ->and($type->isIterableAtLeastOnce()); + $isArray = $type->isArray(); + $isIterableAtLeastOnce = $type->isIterableAtLeastOnce(); + + return new AcceptsResult($isArray->and($isIterableAtLeastOnce), []); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -78,7 +116,12 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - return $this->isSubTypeOf($acceptingType); + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return new AcceptsResult($this->isSubTypeOf($acceptingType), []); } public function equals(Type $type): bool @@ -111,11 +154,61 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { return new ErrorType(); } + public function getKeysArray(): Type + { + return $this; + } + + public function getValuesArray(): Type + { + return $this; + } + + public function fillKeysArray(Type $valueType): Type + { + return $this; + } + + public function flipArray(): Type + { + return $this; + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + return new MixedType(); + } + + public function popArray(): Type + { + return new MixedType(); + } + + public function searchArray(Type $needleType): Type + { + return new MixedType(); + } + + public function shiftArray(): Type + { + return new MixedType(); + } + + public function shuffleArray(): Type + { + return $this; + } + public function isIterable(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -126,26 +219,111 @@ public function isIterableAtLeastOnce(): TrinaryLogic return TrinaryLogic::createYes(); } + public function getArraySize(): Type + { + return IntegerRangeType::fromInterval(1, null); + } + public function getIterableKeyType(): Type { return new MixedType(); } + public function getFirstIterableKeyType(): Type + { + return new MixedType(); + } + + public function getLastIterableKeyType(): Type + { + return new MixedType(); + } + public function getIterableValueType(): Type { return new MixedType(); } + public function getFirstIterableValueType(): Type + { + return new MixedType(); + } + + public function getLastIterableValueType(): Type + { + return new MixedType(); + } + public function isArray(): TrinaryLogic { return TrinaryLogic::createYes(); } + public function isConstantArray(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isOversizedArray(): TrinaryLogic { return TrinaryLogic::createMaybe(); } + public function isList(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -171,6 +349,36 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function toNumber(): Type { return new ErrorType(); @@ -193,7 +401,12 @@ public function toString(): Type public function toArray(): Type { - return new MixedType(); + return $this; + } + + public function toArrayKey(): Type + { + return new ErrorType(); } public function traverse(callable $cb): Type @@ -201,9 +414,29 @@ public function traverse(callable $cb): Type return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + public static function __set_state(array $properties): Type { return new self(); } + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('non-empty-array'); + } + } diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php index ee820e3c5f..dbab26f8d9 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -2,11 +2,17 @@ namespace PHPStan\Type\Accessory; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ErrorType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; @@ -40,14 +46,43 @@ public function getReferencedClasses(): array return []; } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getArrays(): array + { + return []; + } + + public function getConstantArrays(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return $type->isArray() - ->and($type->isIterableAtLeastOnce()); + return new AcceptsResult($type->isArray()->and($type->isIterableAtLeastOnce()), []); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -77,7 +112,12 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - return $this->isSubTypeOf($acceptingType); + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return new AcceptsResult($this->isSubTypeOf($acceptingType), []); } public function equals(Type $type): bool @@ -110,11 +150,61 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { return new ErrorType(); } + public function getKeysArray(): Type + { + return $this; + } + + public function getValuesArray(): Type + { + return $this; + } + + public function fillKeysArray(Type $valueType): Type + { + return $this; + } + + public function flipArray(): Type + { + return $this; + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + return $this; + } + + public function popArray(): Type + { + return $this; + } + + public function searchArray(Type $needleType): Type + { + return new MixedType(); + } + + public function shiftArray(): Type + { + return $this; + } + + public function shuffleArray(): Type + { + return $this; + } + public function isIterable(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -125,26 +215,111 @@ public function isIterableAtLeastOnce(): TrinaryLogic return TrinaryLogic::createYes(); } + public function getArraySize(): Type + { + return IntegerRangeType::fromInterval(1, null); + } + public function getIterableKeyType(): Type { return new MixedType(); } + public function getFirstIterableKeyType(): Type + { + return new MixedType(); + } + + public function getLastIterableKeyType(): Type + { + return new MixedType(); + } + public function getIterableValueType(): Type { return new MixedType(); } + public function getFirstIterableValueType(): Type + { + return new MixedType(); + } + + public function getLastIterableValueType(): Type + { + return new MixedType(); + } + public function isArray(): TrinaryLogic { return TrinaryLogic::createYes(); } + public function isConstantArray(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isOversizedArray(): TrinaryLogic { return TrinaryLogic::createYes(); } + public function isList(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -170,6 +345,36 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function toNumber(): Type { return new ErrorType(); @@ -195,14 +400,39 @@ public function toArray(): Type return new MixedType(); } + public function toArrayKey(): Type + { + return new ErrorType(); + } + public function traverse(callable $cb): Type { return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + public static function __set_state(array $properties): Type { return new self(); } + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode(''); // no PHPDoc representation + } + } diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index c5815f7726..9d37ad7ee5 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -2,21 +2,25 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; -use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\TemplateMixedType; -use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Traits\MaybeCallableTypeTrait; @@ -25,9 +29,7 @@ use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; use function array_merge; -use function is_float; -use function is_int; -use function key; +use function count; use function sprintf; /** @api */ @@ -72,37 +74,64 @@ public function getReferencedClasses(): array ); } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getArrays(): array + { + return [$this]; + } + + public function getConstantArrays(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } if ($type instanceof ConstantArrayType) { - $result = TrinaryLogic::createYes(); + $result = AcceptsResult::createYes(); $thisKeyType = $this->keyType; $itemType = $this->getItemType(); foreach ($type->getKeyTypes() as $i => $keyType) { $valueType = $type->getValueTypes()[$i]; - $result = $result->and($thisKeyType->accepts($keyType, $strictTypes))->and($itemType->accepts($valueType, $strictTypes)); + $acceptsKey = $thisKeyType->acceptsWithReason($keyType, $strictTypes); + $acceptsValue = $itemType->acceptsWithReason($valueType, $strictTypes); + $result = $result->and($acceptsKey)->and($acceptsValue); } return $result; } if ($type instanceof ArrayType) { - return $this->getItemType()->accepts($type->getItemType(), $strictTypes) - ->and($this->keyType->accepts($type->keyType, $strictTypes)); + return $this->getItemType()->acceptsWithReason($type->getItemType(), $strictTypes) + ->and($this->keyType->acceptsWithReason($type->keyType, $strictTypes)); } - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } public function isSuperTypeOf(Type $type): TrinaryLogic { if ($type instanceof self) { return $this->getItemType()->isSuperTypeOf($type->getItemType()) - ->and($this->keyType->isSuperTypeOf($type->keyType)); + ->and($this->getIterableKeyType()->isSuperTypeOf($type->getIterableKeyType())); } if ($type instanceof CompoundType) { @@ -115,8 +144,8 @@ public function isSuperTypeOf(Type $type): TrinaryLogic public function equals(Type $type): bool { return $type instanceof self - && !$type instanceof ConstantArrayType - && $this->getItemType()->equals($type->getItemType()) + && $type->isConstantArray()->no() + && $this->getItemType()->equals($type->getIterableValueType()) && $this->keyType->equals($type->keyType); } @@ -154,6 +183,9 @@ function () use ($level, $isMixedKeyType, $isMixedItemType): string { ); } + /** + * @deprecated + */ public function generalizeKeys(): self { return new self($this->keyType->generalize(GeneralizePrecision::lessSpecific()), $this->itemType); @@ -164,14 +196,14 @@ public function generalizeValues(): self return new self($this->keyType, $this->itemType->generalize(GeneralizePrecision::lessSpecific())); } - public function getKeysArray(): self + public function getKeysArray(): Type { - return new self(new IntegerType(), $this->keyType); + return AccessoryArrayListType::intersectWith(new self(new IntegerType(), $this->getIterableKeyType())); } - public function getValuesArray(): self + public function getValuesArray(): Type { - return new self(new IntegerType(), $this->itemType); + return AccessoryArrayListType::intersectWith(new self(new IntegerType(), $this->itemType)); } public function isIterable(): TrinaryLogic @@ -184,6 +216,11 @@ public function isIterableAtLeastOnce(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getArraySize(): Type + { + return IntegerRangeType::fromInterval(0, null); + } + public function getIterableKeyType(): Type { $keyType = $this->keyType; @@ -197,21 +234,105 @@ public function getIterableKeyType(): Type return $keyType; } + public function getFirstIterableKeyType(): Type + { + return $this->getIterableKeyType(); + } + + public function getLastIterableKeyType(): Type + { + return $this->getIterableKeyType(); + } + public function getIterableValueType(): Type { return $this->getItemType(); } + public function getFirstIterableValueType(): Type + { + return $this->getItemType(); + } + + public function getLastIterableValueType(): Type + { + return $this->getItemType(); + } + public function isArray(): TrinaryLogic { return TrinaryLogic::createYes(); } + public function isConstantArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isOversizedArray(): TrinaryLogic { return TrinaryLogic::createMaybe(); } + public function isList(): TrinaryLogic + { + if (IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($this->getKeyType())->no()) { + return TrinaryLogic::createNo(); + } + + return TrinaryLogic::createMaybe(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -237,6 +358,36 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function isOffsetAccessible(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -244,8 +395,11 @@ public function isOffsetAccessible(): TrinaryLogic public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - $offsetType = self::castToArrayKeyType($offsetType); - if ($this->getKeyType()->isSuperTypeOf($offsetType)->no()) { + $offsetType = $offsetType->toArrayKey(); + + if ($this->getKeyType()->isSuperTypeOf($offsetType)->no() + && ($offsetType->isString()->no() || !$offsetType->isConstantScalarValue()->no()) + ) { return TrinaryLogic::createNo(); } @@ -254,8 +408,10 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic public function getOffsetValueType(Type $offsetType): Type { - $offsetType = self::castToArrayKeyType($offsetType); - if ($this->getKeyType()->isSuperTypeOf($offsetType)->no()) { + $offsetType = $offsetType->toArrayKey(); + if ($this->getKeyType()->isSuperTypeOf($offsetType)->no() + && ($offsetType->isString()->no() || !$offsetType->isConstantScalarValue()->no()) + ) { return new ErrorType(); } @@ -270,9 +426,33 @@ public function getOffsetValueType(Type $offsetType): Type public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { if ($offsetType === null) { - $offsetType = new IntegerType(); + $isKeyTypeInteger = $this->keyType->isInteger(); + if ($isKeyTypeInteger->no()) { + $offsetType = new IntegerType(); + } elseif ($isKeyTypeInteger->yes()) { + $offsetType = $this->keyType; + } else { + $integerTypes = []; + TypeTraverser::map($this->keyType, static function (Type $type, callable $traverse) use (&$integerTypes): Type { + if ($type instanceof UnionType) { + return $traverse($type); + } + + $isInteger = $type->isInteger(); + if ($isInteger->yes()) { + $integerTypes[] = $type; + } + + return $type; + }); + if (count($integerTypes) === 0) { + $offsetType = $this->keyType; + } else { + $offsetType = TypeCombinator::union(...$integerTypes); + } + } } else { - $offsetType = self::castToArrayKeyType($offsetType); + $offsetType = $offsetType->toArrayKey(); } if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) { @@ -301,9 +481,17 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni ); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return new self( + $this->keyType, + TypeCombinator::union($this->itemType, $valueType), + ); + } + public function unsetOffset(Type $offsetType): Type { - $offsetType = self::castToArrayKeyType($offsetType); + $offsetType = $offsetType->toArrayKey(); if ( ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) @@ -320,14 +508,67 @@ public function unsetOffset(Type $offsetType): Type return $this; } + public function fillKeysArray(Type $valueType): Type + { + $itemType = $this->getItemType(); + if ($itemType->isInteger()->no()) { + $stringKeyType = $itemType->toString(); + if ($stringKeyType instanceof ErrorType) { + return $stringKeyType; + } + + return new ArrayType($stringKeyType, $valueType); + } + + return new ArrayType($itemType, $valueType); + } + + public function flipArray(): Type + { + return new self($this->getIterableValueType()->toArrayKey(), $this->getIterableKeyType()); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + $isKeySuperType = $otherArraysType->getIterableKeyType()->isSuperTypeOf($this->getIterableKeyType()); + if ($isKeySuperType->no()) { + return ConstantArrayTypeBuilder::createEmpty()->getArray(); + } + + if ($isKeySuperType->yes()) { + return $otherArraysType->isIterableAtLeastOnce()->yes() + ? TypeCombinator::intersect($this, new NonEmptyArrayType()) + : $this; + } + + return new self($otherArraysType->getIterableKeyType(), $this->getIterableValueType()); + } + + public function popArray(): Type + { + return $this; + } + + public function searchArray(Type $needleType): Type + { + return TypeCombinator::union($this->getIterableKeyType(), new ConstantBooleanType(false)); + } + + public function shiftArray(): Type + { + return $this; + } + + public function shuffleArray(): Type + { + return AccessoryArrayListType::intersectWith(new self(new IntegerType(), $this->itemType)); + } + public function isCallable(): TrinaryLogic { return TrinaryLogic::createMaybe()->and($this->itemType->isString()); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { if ($this->isCallable()->no()) { @@ -368,54 +609,21 @@ public function toArray(): Type return $this; } + public function toArrayKey(): Type + { + return new ErrorType(); + } + + /** @deprecated Use getArraySize() instead */ public function count(): Type { - return IntegerRangeType::fromInterval(0, null); + return $this->getArraySize(); } + /** @deprecated Use $offsetType->toArrayKey() instead */ public static function castToArrayKeyType(Type $offsetType): Type { - return TypeTraverser::map($offsetType, static function (Type $offsetType, callable $traverse): Type { - if ($offsetType instanceof TemplateType) { - return $offsetType; - } - - if ($offsetType instanceof ConstantScalarType) { - $keyValue = $offsetType->getValue(); - if (is_float($keyValue)) { - $keyValue = (int) $keyValue; - } - /** @var int|string $offsetValue */ - $offsetValue = key([$keyValue => null]); - return is_int($offsetValue) ? new ConstantIntegerType($offsetValue) : new ConstantStringType($offsetValue); - } - - if ($offsetType instanceof IntegerType) { - return $offsetType; - } - - if ($offsetType instanceof BooleanType) { - return new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]); - } - - if ($offsetType instanceof UnionType) { - return $traverse($offsetType); - } - - if ($offsetType instanceof FloatType || $offsetType->isNumericString()->yes()) { - return new IntegerType(); - } - - if ($offsetType->isString()->yes()) { - return $offsetType; - } - - if ($offsetType instanceof IntersectionType) { - return $traverse($offsetType); - } - - return new UnionType([new IntegerType(), new StringType()]); - }); + return $offsetType->toArrayKey(); } public function inferTemplateTypes(Type $receivedType): TemplateTypeMap @@ -425,7 +633,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap } if ($receivedType->isArray()->yes()) { - $keyTypeMap = $this->getKeyType()->inferTemplateTypes($receivedType->getIterableKeyType()); + $keyTypeMap = $this->getIterableKeyType()->inferTemplateTypes($receivedType->getIterableKeyType()); $itemTypeMap = $this->getItemType()->inferTemplateTypes($receivedType->getIterableValueType()); return $keyTypeMap->union($itemTypeMap); @@ -436,31 +644,61 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array { - $keyVariance = $positionVariance; - $itemVariance = $positionVariance; + $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant()); + + return array_merge( + $this->getIterableKeyType()->getReferencedTemplateTypes($variance), + $this->getItemType()->getReferencedTemplateTypes($variance), + ); + } - if (!$positionVariance->contravariant()) { - $keyType = $this->getKeyType(); - if ($keyType instanceof TemplateType) { - $keyVariance = $keyType->getVariance(); + public function traverse(callable $cb): Type + { + $keyType = $cb($this->keyType); + $itemType = $cb($this->itemType); + + if ($keyType !== $this->keyType || $itemType !== $this->itemType) { + if ($keyType instanceof NeverType && $itemType instanceof NeverType) { + return new ConstantArrayType([], []); } - $itemType = $this->getItemType(); - if ($itemType instanceof TemplateType) { - $itemVariance = $itemType->getVariance(); + return new self($keyType, $itemType); + } + + return $this; + } + + public function toPhpDocNode(): TypeNode + { + $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed'; + $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed'; + + if ($isMixedKeyType) { + if ($isMixedItemType) { + return new IdentifierTypeNode('array'); } + + return new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + $this->itemType->toPhpDocNode(), + ], + ); } - return array_merge( - $this->getKeyType()->getReferencedTemplateTypes($keyVariance), - $this->getItemType()->getReferencedTemplateTypes($itemVariance), + return new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + $this->keyType->toPhpDocNode(), + $this->itemType->toPhpDocNode(), + ], ); } - public function traverse(callable $cb): Type + public function traverseSimultaneously(Type $right, callable $cb): Type { - $keyType = $cb($this->keyType); - $itemType = $cb($this->itemType); + $keyType = $cb($this->keyType, $right->getIterableKeyType()); + $itemType = $cb($this->itemType, $right->getIterableValueType()); if ($keyType !== $this->keyType || $itemType !== $this->itemType) { if ($keyType instanceof NeverType && $itemType instanceof NeverType) { @@ -475,7 +713,7 @@ public function traverse(callable $cb): Type public function tryRemove(Type $typeToRemove): ?Type { - if ($typeToRemove instanceof ConstantArrayType && $typeToRemove->isIterableAtLeastOnce()->no()) { + if ($typeToRemove->isConstantArray()->yes() && $typeToRemove->isIterableAtLeastOnce()->no()) { return TypeCombinator::intersect($this, new NonEmptyArrayType()); } @@ -483,17 +721,27 @@ public function tryRemove(Type $typeToRemove): ?Type return new ConstantArrayType([], []); } - if ($this instanceof ConstantArrayType && $typeToRemove instanceof HasOffsetType) { + if ($this->isConstantArray()->yes() && $typeToRemove instanceof HasOffsetType) { return $this->unsetOffset($typeToRemove->getOffsetType()); } - if ($this instanceof ConstantArrayType && $typeToRemove instanceof HasOffsetValueType) { + if ($this->isConstantArray()->yes() && $typeToRemove instanceof HasOffsetValueType) { return $this->unsetOffset($typeToRemove->getOffsetType()); } return null; } + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + /** * @param mixed[] $properties */ diff --git a/src/Type/BenevolentUnionType.php b/src/Type/BenevolentUnionType.php index c4afd4bc08..dc1245ad45 100644 --- a/src/Type/BenevolentUnionType.php +++ b/src/Type/BenevolentUnionType.php @@ -43,6 +43,45 @@ protected function unionTypes(callable $getType): Type return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$resultTypes)); } + protected function pickFromTypes( + callable $getValues, + callable $criteria, + ): array + { + $values = []; + foreach ($this->getTypes() as $type) { + $innerValues = $getValues($type); + if ($innerValues === [] && $criteria($type)) { + return []; + } + + foreach ($innerValues as $innerType) { + $values[] = $innerType; + } + } + + return $values; + } + + public function getOffsetValueType(Type $offsetType): Type + { + $types = []; + foreach ($this->getTypes() as $innerType) { + $valueType = $innerType->getOffsetValueType($offsetType); + if ($valueType instanceof ErrorType) { + continue; + } + + $types[] = $valueType; + } + + if (count($types) === 0) { + return new ErrorType(); + } + + return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); + } + protected function unionResults(callable $getResult): TrinaryLogic { return TrinaryLogic::createNo()->lazyOr($this->getTypes(), $getResult); @@ -50,7 +89,17 @@ protected function unionResults(callable $getResult): TrinaryLogic public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - return TrinaryLogic::createNo()->lazyOr($this->getTypes(), static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes)); + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + $result = AcceptsResult::createNo(); + foreach ($this->getTypes() as $innerType) { + $result = $result->or($acceptingType->acceptsWithReason($innerType, $strictTypes)); + } + + return $result; } public function inferTemplateTypes(Type $receivedType): TemplateTypeMap @@ -95,6 +144,35 @@ public function traverse(callable $cb): Type return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + $types = []; + $changed = false; + + if (!$right instanceof UnionType) { + return $this; + } + + if (count($this->getTypes()) !== count($right->getTypes())) { + return $this; + } + + foreach ($this->getSortedTypes() as $i => $leftType) { + $rightType = $right->getSortedTypes()[$i]; + $newType = $cb($leftType, $rightType); + if ($leftType !== $newType) { + $changed = true; + } + $types[] = $newType; + } + + if ($changed) { + return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types)); + } + + return $this; + } + /** * @param mixed[] $properties */ diff --git a/src/Type/BitwiseFlagHelper.php b/src/Type/BitwiseFlagHelper.php index d00d60189b..9b7175cd3f 100644 --- a/src/Type/BitwiseFlagHelper.php +++ b/src/Type/BitwiseFlagHelper.php @@ -94,8 +94,7 @@ private function typeContainsIntFlag(Type $type, int $flag): TrinaryLogic return TrinaryLogic::createNo(); } - $integerType = new IntegerType(); - if ($integerType->isSuperTypeOf($type)->yes() || $type instanceof MixedType) { + if ($type->isInteger()->yes() || $type instanceof MixedType) { return TrinaryLogic::createMaybe(); } diff --git a/src/Type/BooleanType.php b/src/Type/BooleanType.php index 9162932f64..c0ac5b5920 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -2,11 +2,16 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonCallableTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; @@ -21,6 +26,7 @@ class BooleanType implements Type { use JustNullableTypeTrait; + use NonArrayTypeTrait; use NonCallableTypeTrait; use NonIterableTypeTrait; use NonObjectTypeTrait; @@ -35,6 +41,11 @@ public function __construct() { } + public function getConstantStrings(): array + { + return []; + } + public function describe(VerbosityLevel $level): string { return 'bool'; @@ -75,9 +86,46 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], + [], + TrinaryLogic::createYes(), ); } + public function toArrayKey(): Type + { + return new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function tryRemove(Type $typeToRemove): ?Type { if ($typeToRemove instanceof ConstantBooleanType) { @@ -87,6 +135,24 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } + public function getFiniteTypes(): array + { + return [ + new ConstantBooleanType(true), + new ConstantBooleanType(false), + ]; + } + + public function exponentiate(Type $exponent): Type + { + return ExponentiateHelper::exponentiate($this, $exponent); + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('bool'); + } + /** * @param mixed[] $properties */ diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index 62ea5e002f..c1cfe1a717 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -3,16 +3,31 @@ namespace PHPStan\Type; use PHPStan\Analyser\OutOfClassScope; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDoc\Tag\TemplateTag; +use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; +use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; +use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\PhpDocParser\Printer\Printer; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\Reflection\Callables\SimpleThrowPoint; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\PassedByReference; +use PHPStan\Reflection\Php\DummyParameter; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; +use PHPStan\Type\Traits\MaybeArrayTypeTrait; use PHPStan\Type\Traits\MaybeIterableTypeTrait; use PHPStan\Type\Traits\MaybeObjectTypeTrait; use PHPStan\Type\Traits\MaybeOffsetAccessibleTypeTrait; @@ -22,13 +37,13 @@ use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use function array_map; use function array_merge; -use function implode; -use function sprintf; +use function count; /** @api */ -class CallableType implements CompoundType, ParametersAcceptor +class CallableType implements CompoundType, CallableParametersAcceptor { + use MaybeArrayTypeTrait; use MaybeIterableTypeTrait; use MaybeObjectTypeTrait; use MaybeOffsetAccessibleTypeTrait; @@ -44,19 +59,46 @@ class CallableType implements CompoundType, ParametersAcceptor private bool $isCommonCallable; + private TemplateTypeMap $templateTypeMap; + + private TemplateTypeMap $resolvedTemplateTypeMap; + + private TrinaryLogic $isPure; + /** * @api - * @param array $parameters + * @param array|null $parameters + * @param array $templateTags */ public function __construct( ?array $parameters = null, ?Type $returnType = null, private bool $variadic = true, + ?TemplateTypeMap $templateTypeMap = null, + ?TemplateTypeMap $resolvedTemplateTypeMap = null, + private array $templateTags = [], + ?TrinaryLogic $isPure = null, ) { $this->parameters = $parameters ?? []; $this->returnType = $returnType ?? new MixedType(); $this->isCommonCallable = $parameters === null && $returnType === null; + $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); + $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty(); + $this->isPure = $isPure ?? TrinaryLogic::createMaybe(); + } + + /** + * @return array + */ + public function getTemplateTags(): array + { + return $this->templateTags; + } + + public function isPure(): TrinaryLogic + { + return $this->isPure; } /** @@ -72,10 +114,30 @@ public function getReferencedClasses(): array return array_merge($classes, $this->returnType->getReferencedClasses()); } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType && !$type instanceof self) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } return $this->isSuperTypeOfInternal($type, true); @@ -87,13 +149,13 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return $type->isSubTypeOf($this); } - return $this->isSuperTypeOfInternal($type, false); + return $this->isSuperTypeOfInternal($type, false)->result; } - private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): TrinaryLogic + private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): AcceptsResult { - $isCallable = $type->isCallable(); - if ($isCallable->no() || $this->isCommonCallable) { + $isCallable = new AcceptsResult($type->isCallable(), []); + if ($isCallable->no()) { return $isCallable; } @@ -102,6 +164,19 @@ private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): Trina $scope = new OutOfClassScope(); } + if ($this->isCommonCallable) { + if ($this->isPure()->yes()) { + $typePure = TrinaryLogic::createYes(); + foreach ($type->getCallableParametersAcceptors($scope) as $variant) { + $typePure = $typePure->and($variant->isPure()); + } + + return $isCallable->and(new AcceptsResult($typePure, [])); + } + + return $isCallable; + } + $variantsResult = null; foreach ($type->getCallableParametersAcceptors($scope) as $variant) { $isSuperType = CallableTypeHelper::isParametersAcceptorSuperTypeOf($this, $variant, $treatMixedAsAny); @@ -131,31 +206,48 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - return $this->isSubTypeOf($acceptingType); + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return new AcceptsResult($this->isSubTypeOf($acceptingType), []); } public function equals(Type $type): bool { - return $type instanceof self; + if (!$type instanceof self) { + return false; + } + + return $this->describe(VerbosityLevel::precise()) === $type->describe(VerbosityLevel::precise()); } public function describe(VerbosityLevel $level): string { return $level->handle( static fn (): string => 'callable', - fn (): string => sprintf( - 'callable(%s): %s', - implode(', ', array_map( - static fn (ParameterReflection $param): string => sprintf( - '%s%s%s', - $param->isVariadic() ? '...' : '', - $param->getType()->describe($level), - $param->isOptional() && !$param->isVariadic() ? '=' : '', - ), - $this->getParameters(), - )), - $this->returnType->describe($level), - ), + function (): string { + $printer = new Printer(); + $selfWithoutParameterNames = new self( + array_map(static fn (ParameterReflection $p): ParameterReflection => new DummyParameter( + '', + $p->getType(), + $p->isOptional() && !$p->isVariadic(), + PassedByReference::createNo(), + $p->isVariadic(), + $p->getDefaultValue(), + ), $this->parameters), + $this->returnType, + $this->variadic, + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + $this->templateTags, + $this->isPure, + ); + + return $printer->print($selfWithoutParameterNames->toPhpDocNode()); + }, ); } @@ -164,14 +256,44 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createYes(); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return [$this]; } + public function getThrowPoints(): array + { + return [ + SimpleThrowPoint::createImplicit(), + ]; + } + + public function getImpurePoints(): array + { + $pure = $this->isPure(); + if ($pure->yes()) { + return []; + } + + return [ + new SimpleImpurePoint( + 'functionCall', + 'call to a callable', + $pure->no(), + ), + ]; + } + + public function getInvalidateExpressions(): array + { + return []; + } + + public function getUsedVariables(): array + { + return []; + } + public function toNumber(): Type { return new ErrorType(); @@ -197,14 +319,24 @@ public function toArray(): Type return new ArrayType(new MixedType(), new MixedType()); } + public function toArrayKey(): Type + { + return new ErrorType(); + } + public function getTemplateTypeMap(): TemplateTypeMap { - return TemplateTypeMap::createEmpty(); + return $this->templateTypeMap; } public function getResolvedTemplateTypeMap(): TemplateTypeMap { - return TemplateTypeMap::createEmpty(); + return $this->resolvedTemplateTypeMap; + } + + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return TemplateTypeVarianceMap::createEmpty(); } /** @@ -307,12 +439,61 @@ public function traverse(callable $cb): Type $parameters, $cb($this->getReturnType()), $this->isVariadic(), + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + $this->templateTags, + $this->isPure, ); } - public function isArray(): TrinaryLogic + public function traverseSimultaneously(Type $right, callable $cb): Type { - return TrinaryLogic::createMaybe(); + if ($this->isCommonCallable) { + return $this; + } + + if (!$right->isCallable()->yes()) { + return $this; + } + + $rightAcceptors = $right->getCallableParametersAcceptors(new OutOfClassScope()); + if (count($rightAcceptors) !== 1) { + return $this; + } + + $rightParameters = $rightAcceptors[0]->getParameters(); + if (count($this->getParameters()) !== count($rightParameters)) { + return $this; + } + + $parameters = []; + foreach ($this->getParameters() as $i => $leftParam) { + $rightParam = $rightParameters[$i]; + $leftDefaultValue = $leftParam->getDefaultValue(); + $rightDefaultValue = $rightParam->getDefaultValue(); + $defaultValue = $leftDefaultValue; + if ($leftDefaultValue !== null && $rightDefaultValue !== null) { + $defaultValue = $cb($leftDefaultValue, $rightDefaultValue); + } + $parameters[] = new NativeParameterReflection( + $leftParam->getName(), + $leftParam->isOptional(), + $cb($leftParam->getType(), $rightParam->getType()), + $leftParam->passedByReference(), + $leftParam->isVariadic(), + $defaultValue, + ); + } + + return new self( + $parameters, + $cb($this->getReturnType(), $rightAcceptors[0]->getReturnType()), + $this->isVariadic(), + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + $this->templateTags, + $this->isPure, + ); } public function isOversizedArray(): TrinaryLogic @@ -320,6 +501,56 @@ public function isOversizedArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createMaybe(); @@ -345,11 +576,90 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function getEnumCases(): array + { + return []; + } + public function isCommonCallable(): bool { return $this->isCommonCallable; } + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + if ($this->isCommonCallable) { + return new IdentifierTypeNode($this->isPure()->yes() ? 'pure-callable' : 'callable'); + } + + $parameters = []; + foreach ($this->parameters as $parameter) { + $parameters[] = new CallableTypeParameterNode( + $parameter->getType()->toPhpDocNode(), + !$parameter->passedByReference()->no(), + $parameter->isVariadic(), + $parameter->getName() === '' ? '' : '$' . $parameter->getName(), + $parameter->isOptional(), + ); + } + + $templateTags = []; + foreach ($this->templateTags as $templateName => $templateTag) { + $templateTags[] = new TemplateTagValueNode( + $templateName, + $templateTag->getBound()->toPhpDocNode(), + '', + ); + } + + return new CallableTypeNode( + new IdentifierTypeNode($this->isPure->yes() ? 'pure-callable' : 'callable'), + $parameters, + $this->returnType->toPhpDocNode(), + $templateTags, + ); + } + /** * @param mixed[] $properties */ @@ -359,6 +669,10 @@ public static function __set_state(array $properties): Type (bool) $properties['isCommonCallable'] ? null : $properties['parameters'], (bool) $properties['isCommonCallable'] ? null : $properties['returnType'], $properties['variadic'], + $properties['templateTypeMap'], + $properties['resolvedTemplateTypeMap'], + $properties['templateTags'], + $properties['isPure'], ); } diff --git a/src/Type/CallableTypeHelper.php b/src/Type/CallableTypeHelper.php index f45c72ed7f..eb58de74bb 100644 --- a/src/Type/CallableTypeHelper.php +++ b/src/Type/CallableTypeHelper.php @@ -2,63 +2,115 @@ namespace PHPStan\Type; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\TrinaryLogic; +use function array_key_exists; +use function array_merge; +use function count; +use function sprintf; class CallableTypeHelper { public static function isParametersAcceptorSuperTypeOf( - ParametersAcceptor $ours, - ParametersAcceptor $theirs, + CallableParametersAcceptor $ours, + CallableParametersAcceptor $theirs, bool $treatMixedAsAny, - ): TrinaryLogic + ): AcceptsResult { $theirParameters = $theirs->getParameters(); $ourParameters = $ours->getParameters(); - $result = null; + $lastParameter = null; + foreach ($theirParameters as $theirParameter) { + $lastParameter = $theirParameter; + } + $theirParameterCount = count($theirParameters); + $ourParameterCount = count($ourParameters); + if ( + $lastParameter !== null + && $lastParameter->isVariadic() + && $theirParameterCount < $ourParameterCount + ) { + foreach ($ourParameters as $i => $ourParameter) { + if (array_key_exists($i, $theirParameters)) { + continue; + } + $theirParameters[] = $lastParameter; + } + } + + $result = AcceptsResult::createYes(); foreach ($theirParameters as $i => $theirParameter) { + $parameterDescription = $theirParameter->getName() === '' ? sprintf('#%d', $i + 1) : sprintf('#%d $%s', $i + 1, $theirParameter->getName()); if (!isset($ourParameters[$i])) { if ($theirParameter->isOptional()) { continue; } - return TrinaryLogic::createNo(); + $accepts = new AcceptsResult(TrinaryLogic::createNo(), [ + sprintf( + 'Parameter %s of passed callable is required but accepting callable does not have that parameter. It will be called without it.', + $parameterDescription, + ), + ]); + $result = $result->and($accepts); + continue; } $ourParameter = $ourParameters[$i]; $ourParameterType = $ourParameter->getType(); if ($ourParameter->isOptional() && !$theirParameter->isOptional()) { - return TrinaryLogic::createNo(); + $accepts = new AcceptsResult(TrinaryLogic::createNo(), [ + sprintf( + 'Parameter %s of passed callable is required but the parameter of accepting callable is optional. It might be called without it.', + $parameterDescription, + ), + ]); + $result = $result->and($accepts); } if ($treatMixedAsAny) { - $isSuperType = $theirParameter->getType()->accepts($ourParameterType, true); + $isSuperType = $theirParameter->getType()->acceptsWithReason($ourParameterType, true); } else { - $isSuperType = $theirParameter->getType()->isSuperTypeOf($ourParameterType); + $isSuperType = new AcceptsResult($theirParameter->getType()->isSuperTypeOf($ourParameterType), []); } - if ($result === null) { - $result = $isSuperType; - } else { - $result = $result->and($isSuperType); + + if ($isSuperType->maybe()) { + $verbosity = VerbosityLevel::getRecommendedLevelByType($theirParameter->getType(), $ourParameterType); + $isSuperType = new AcceptsResult($isSuperType->result, array_merge($isSuperType->reasons, [ + sprintf( + 'Type %s of parameter %s of passed callable needs to be same or wider than parameter type %s of accepting callable.', + $theirParameter->getType()->describe($verbosity), + $parameterDescription, + $ourParameterType->describe($verbosity), + ), + ])); } + + $result = $result->and($isSuperType); + } + + if (!$treatMixedAsAny && $theirParameterCount < $ourParameterCount) { + $result = $result->and(AcceptsResult::createMaybe()); } $theirReturnType = $theirs->getReturnType(); if ($treatMixedAsAny) { - $isReturnTypeSuperType = $ours->getReturnType()->accepts($theirReturnType, true); + $isReturnTypeSuperType = $ours->getReturnType()->acceptsWithReason($theirReturnType, true); } else { - $isReturnTypeSuperType = $ours->getReturnType()->isSuperTypeOf($theirReturnType); + $isReturnTypeSuperType = new AcceptsResult($ours->getReturnType()->isSuperTypeOf($theirReturnType), []); } - if ($result === null) { - $result = $isReturnTypeSuperType; - } else { - $result = $result->and($isReturnTypeSuperType); + + $pure = $ours->isPure(); + if ($pure->yes()) { + $result = $result->and(new AcceptsResult($theirs->isPure(), [])); + } elseif ($pure->no()) { + $result = $result->and(new AcceptsResult($theirs->isPure()->negate(), [])); } - return $result; + return $result->and($isReturnTypeSuperType); } } diff --git a/src/Type/ClassStringType.php b/src/Type/ClassStringType.php index 863b438c27..802d17e162 100644 --- a/src/Type/ClassStringType.php +++ b/src/Type/ClassStringType.php @@ -2,8 +2,9 @@ namespace PHPStan\Type; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; -use PHPStan\Type\Constant\ConstantStringType; /** @api */ class ClassStringType extends StringType @@ -22,44 +23,25 @@ public function describe(VerbosityLevel $level): string public function accepts(Type $type, bool $strictTypes): TrinaryLogic { - if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); - } - - if ($type instanceof ConstantStringType) { - return TrinaryLogic::createFromBoolean($type->isClassString()); - } - - if ($type instanceof self) { - return TrinaryLogic::createYes(); - } + return $this->acceptsWithReason($type, $strictTypes)->result; + } - if ($type instanceof StringType) { - return TrinaryLogic::createMaybe(); + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + { + if ($type instanceof CompoundType) { + return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return TrinaryLogic::createNo(); + return new AcceptsResult($type->isClassStringType(), []); } public function isSuperTypeOf(Type $type): TrinaryLogic { - if ($type instanceof ConstantStringType) { - return TrinaryLogic::createFromBoolean($type->isClassString()); - } - - if ($type instanceof self) { - return TrinaryLogic::createYes(); - } - - if ($type instanceof parent) { - return TrinaryLogic::createMaybe(); - } - if ($type instanceof CompoundType) { return $type->isSubTypeOf($this); } - return TrinaryLogic::createNo(); + return $type->isClassStringType(); } public function isString(): TrinaryLogic @@ -87,6 +69,26 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('class-string'); + } + /** * @param mixed[] $properties */ diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index 3b98321c22..d3498921e1 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -4,14 +4,28 @@ use Closure; use PHPStan\Analyser\OutOfClassScope; +use PHPStan\Node\InvalidateExprNode; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDoc\Tag\TemplateTag; +use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; +use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; +use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\PhpDocParser\Printer\Printer; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\Reflection\Callables\SimpleThrowPoint; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\PassedByReference; use PHPStan\Reflection\Php\ClosureCallUnresolvedMethodPrototypeReflection; +use PHPStan\Reflection\Php\DummyParameter; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; @@ -22,47 +36,102 @@ use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; +use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; use function array_map; use function array_merge; -use function implode; -use function sprintf; +use function count; /** @api */ -class ClosureType implements TypeWithClassName, ParametersAcceptor +class ClosureType implements TypeWithClassName, CallableParametersAcceptor { + use NonArrayTypeTrait; use NonGenericTypeTrait; + use NonIterableTypeTrait; use UndecidedComparisonTypeTrait; use NonOffsetAccessibleTypeTrait; use NonRemoveableTypeTrait; use NonGeneralizableTypeTrait; + /** @var array */ + private array $parameters; + + private Type $returnType; + + private bool $isCommonCallable; + private ObjectType $objectType; private TemplateTypeMap $templateTypeMap; private TemplateTypeMap $resolvedTemplateTypeMap; + private TemplateTypeVarianceMap $callSiteVarianceMap; + /** * @api - * @param array $parameters + * @param array|null $parameters + * @param array $templateTags + * @param SimpleThrowPoint[] $throwPoints + * @param SimpleImpurePoint[] $impurePoints + * @param InvalidateExprNode[] $invalidateExpressions + * @param string[] $usedVariables */ public function __construct( - private array $parameters, - private Type $returnType, - private bool $variadic, + ?array $parameters = null, + ?Type $returnType = null, + private bool $variadic = true, ?TemplateTypeMap $templateTypeMap = null, ?TemplateTypeMap $resolvedTemplateTypeMap = null, + ?TemplateTypeVarianceMap $callSiteVarianceMap = null, + private array $templateTags = [], + private array $throwPoints = [], + private array $impurePoints = [], + private array $invalidateExpressions = [], + private array $usedVariables = [], ) { + $this->parameters = $parameters ?? []; + $this->returnType = $returnType ?? new MixedType(); + $this->isCommonCallable = $parameters === null && $returnType === null; $this->objectType = new ObjectType(Closure::class); $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty(); + $this->callSiteVarianceMap = $callSiteVarianceMap ?? TemplateTypeVarianceMap::createEmpty(); + } + + /** + * @return array + */ + public function getTemplateTags(): array + { + return $this->templateTags; + } + + public function isPure(): TrinaryLogic + { + $impurePoints = $this->getImpurePoints(); + if (count($impurePoints) === 0) { + return TrinaryLogic::createYes(); + } + + $certainCount = 0; + foreach ($impurePoints as $impurePoint) { + if (!$impurePoint->isCertain()) { + continue; + } + + $certainCount++; + } + + return $certainCount > 0 ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe(); } public function getClassName(): string @@ -93,14 +162,29 @@ public function getReferencedClasses(): array return array_merge($classes, $this->returnType->getReferencedClasses()); } + public function getObjectClassNames(): array + { + return $this->objectType->getObjectClassNames(); + } + + public function getObjectClassReflections(): array + { + return $this->objectType->getObjectClassReflections(); + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } if (!$type instanceof ClosureType) { - return $this->objectType->accepts($type, $strictTypes); + return $this->objectType->acceptsWithReason($type, $strictTypes); } return $this->isSuperTypeOfInternal($type, true); @@ -112,10 +196,10 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return $type->isSubTypeOf($this); } - return $this->isSuperTypeOfInternal($type, false); + return $this->isSuperTypeOfInternal($type, false)->result; } - private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): TrinaryLogic + private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): AcceptsResult { if ($type instanceof self) { return CallableTypeHelper::isParametersAcceptorSuperTypeOf( @@ -125,14 +209,11 @@ private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): Trina ); } - if ( - $type instanceof TypeWithClassName - && $type->getClassName() === Closure::class - ) { - return TrinaryLogic::createMaybe(); + if ($type->getObjectClassNames() === [Closure::class]) { + return AcceptsResult::createMaybe(); } - return $this->objectType->isSuperTypeOf($type); + return new AcceptsResult($this->objectType->isSuperTypeOf($type), []); } public function equals(Type $type): bool @@ -141,29 +222,60 @@ public function equals(Type $type): bool return false; } - return $this->returnType->equals($type->returnType); + return $this->describe(VerbosityLevel::precise()) === $type->describe(VerbosityLevel::precise()); } public function describe(VerbosityLevel $level): string { return $level->handle( static fn (): string => 'Closure', - fn (): string => sprintf( - 'Closure(%s): %s', - implode(', ', array_map( - static fn (ParameterReflection $param): string => sprintf( - '%s%s%s', - $param->isVariadic() ? '...' : '', - $param->getType()->describe($level), - $param->isOptional() && !$param->isVariadic() ? '=' : '', - ), - $this->parameters, - )), - $this->returnType->describe($level), - ), + function (): string { + if ($this->isCommonCallable) { + return $this->isPure()->yes() ? 'pure-Closure' : 'Closure'; + } + + $printer = new Printer(); + $selfWithoutParameterNames = new self( + array_map(static fn (ParameterReflection $p): ParameterReflection => new DummyParameter( + '', + $p->getType(), + $p->isOptional() && !$p->isVariadic(), + PassedByReference::createNo(), + $p->isVariadic(), + $p->getDefaultValue(), + ), $this->parameters), + $this->returnType, + $this->variadic, + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + $this->templateTags, + $this->throwPoints, + $this->impurePoints, + $this->invalidateExpressions, + $this->usedVariables, + ); + + return $printer->print($selfWithoutParameterNames->toPhpDocNode()); + }, ); } + public function isObject(): TrinaryLogic + { + return $this->objectType->isObject(); + } + + public function isEnum(): TrinaryLogic + { + return $this->objectType->isEnum(); + } + + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return $this->objectType->getTemplateType($ancestorClassName, $templateTypeName); + } + public function canAccessProperties(): TrinaryLogic { return $this->objectType->canAccessProperties(); @@ -194,7 +306,7 @@ public function hasMethod(string $methodName): TrinaryLogic return $this->objectType->hasMethod($methodName); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -226,6 +338,11 @@ public function getConstant(string $constantName): ConstantReflection return $this->objectType->getConstant($constantName); } + public function getConstantStrings(): array + { + return []; + } + public function isIterable(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -236,29 +353,46 @@ public function isIterableAtLeastOnce(): TrinaryLogic return TrinaryLogic::createNo(); } - public function getIterableKeyType(): Type + public function isCallable(): TrinaryLogic { - return new ErrorType(); + return TrinaryLogic::createYes(); } - public function getIterableValueType(): Type + public function getEnumCases(): array { - return new ErrorType(); + return []; } - public function isCallable(): TrinaryLogic + public function isCommonCallable(): bool { - return TrinaryLogic::createYes(); + return $this->isCommonCallable; } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return [$this]; } + public function getThrowPoints(): array + { + return $this->throwPoints; + } + + public function getImpurePoints(): array + { + return $this->impurePoints; + } + + public function getInvalidateExpressions(): array + { + return $this->invalidateExpressions; + } + + public function getUsedVariables(): array + { + return $this->usedVariables; + } + public function isCloneable(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -295,9 +429,16 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], + [], + TrinaryLogic::createYes(), ); } + public function toArrayKey(): Type + { + return new ErrorType(); + } + public function getTemplateTypeMap(): TemplateTypeMap { return $this->templateTypeMap; @@ -308,6 +449,11 @@ public function getResolvedTemplateTypeMap(): TemplateTypeMap return $this->resolvedTemplateTypeMap; } + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->callSiteVarianceMap; + } + /** * @return array */ @@ -371,6 +517,10 @@ private function inferTemplateTypesOnParametersAcceptor(ParametersAcceptor $para public function traverse(callable $cb): Type { + if ($this->isCommonCallable) { + return $this; + } + return new self( array_map(static function (ParameterReflection $param) use ($cb): NativeParameterReflection { $defaultValue = $param->getDefaultValue(); @@ -387,15 +537,110 @@ public function traverse(callable $cb): Type $this->isVariadic(), $this->templateTypeMap, $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + $this->templateTags, + $this->throwPoints, + $this->impurePoints, + $this->invalidateExpressions, + $this->usedVariables, + ); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if ($this->isCommonCallable) { + return $this; + } + + if (!$right instanceof self) { + return $this; + } + + $rightParameters = $right->getParameters(); + if (count($this->getParameters()) !== count($rightParameters)) { + return $this; + } + + $parameters = []; + foreach ($this->getParameters() as $i => $leftParam) { + $rightParam = $rightParameters[$i]; + $leftDefaultValue = $leftParam->getDefaultValue(); + $rightDefaultValue = $rightParam->getDefaultValue(); + $defaultValue = $leftDefaultValue; + if ($leftDefaultValue !== null && $rightDefaultValue !== null) { + $defaultValue = $cb($leftDefaultValue, $rightDefaultValue); + } + $parameters[] = new NativeParameterReflection( + $leftParam->getName(), + $leftParam->isOptional(), + $cb($leftParam->getType(), $rightParam->getType()), + $leftParam->passedByReference(), + $leftParam->isVariadic(), + $defaultValue, + ); + } + + return new self( + $parameters, + $cb($this->getReturnType(), $right->getReturnType()), + $this->isVariadic(), + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + $this->templateTags, + $this->throwPoints, + $this->impurePoints, + $this->invalidateExpressions, + $this->usedVariables, ); } - public function isArray(): TrinaryLogic + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function isOversizedArray(): TrinaryLogic + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -425,6 +670,80 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this; + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + if ($this->isCommonCallable) { + return new IdentifierTypeNode($this->isPure()->yes() ? 'pure-Closure' : 'Closure'); + } + + $parameters = []; + foreach ($this->parameters as $parameter) { + $parameters[] = new CallableTypeParameterNode( + $parameter->getType()->toPhpDocNode(), + !$parameter->passedByReference()->no(), + $parameter->isVariadic(), + $parameter->getName() === '' ? '' : '$' . $parameter->getName(), + $parameter->isOptional(), + ); + } + + $templateTags = []; + foreach ($this->templateTags as $templateName => $templateTag) { + $templateTags[] = new TemplateTagValueNode( + $templateName, + $templateTag->getBound()->toPhpDocNode(), + '', + ); + } + + return new CallableTypeNode( + new IdentifierTypeNode('Closure'), + $parameters, + $this->returnType->toPhpDocNode(), + $templateTags, + ); + } + /** * @param mixed[] $properties */ @@ -436,6 +755,12 @@ public static function __set_state(array $properties): Type $properties['variadic'], $properties['templateTypeMap'], $properties['resolvedTemplateTypeMap'], + $properties['callSiteVarianceMap'], + $properties['templateTags'], + $properties['throwPoints'], + $properties['impurePoints'], + $properties['invalidateExpressions'], + $properties['usedVariables'], ); } diff --git a/src/Type/ClosureTypeFactory.php b/src/Type/ClosureTypeFactory.php index 52d43410de..07a395a5fe 100644 --- a/src/Type/ClosureTypeFactory.php +++ b/src/Type/ClosureTypeFactory.php @@ -54,7 +54,7 @@ public function fromClosureObject(Closure $closure): ClosureType throw new ShouldNotHappenException('Closure reflection not found.'); } - /** @var \PHPStan\BetterReflection\Reflection\ReflectionFunction[] $reflections */ + /** @var list<\PHPStan\BetterReflection\Reflection\ReflectionFunction> $reflections */ $reflections = $find($this->reflector, $ast, new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION), $locatedSource); if (count($reflections) !== 1) { throw new ShouldNotHappenException('Closure reflection not found.'); diff --git a/src/Type/CompoundType.php b/src/Type/CompoundType.php index 199c516c4b..ea9b2f4660 100644 --- a/src/Type/CompoundType.php +++ b/src/Type/CompoundType.php @@ -12,6 +12,8 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic; public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic; + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult; + public function isGreaterThan(Type $otherType): TrinaryLogic; public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic; diff --git a/src/Type/ConditionalType.php b/src/Type/ConditionalType.php index c03acf7d73..aa5d8af0a6 100644 --- a/src/Type/ConditionalType.php +++ b/src/Type/ConditionalType.php @@ -2,6 +2,8 @@ namespace PHPStan\Type; +use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Traits\LateResolvableTypeTrait; @@ -16,6 +18,14 @@ final class ConditionalType implements CompoundType, LateResolvableType use LateResolvableTypeTrait; use NonGeneralizableTypeTrait; + private ?Type $normalizedIf = null; + + private ?Type $normalizedElse = null; + + private ?Type $subjectWithTargetIntersectedType = null; + + private ?Type $subjectWithTargetRemovedType = null; + public function __construct( private Type $subject, private Type $target, @@ -112,30 +122,72 @@ protected function getResult(): Type $isSuperType = $this->target->isSuperTypeOf($this->subject); if ($isSuperType->yes()) { - return !$this->negated ? $this->if : $this->else; + return !$this->negated ? $this->getNormalizedIf() : $this->getNormalizedElse(); } if ($isSuperType->no()) { - return !$this->negated ? $this->else : $this->if; + return !$this->negated ? $this->getNormalizedElse() : $this->getNormalizedIf(); } - return TypeCombinator::union($this->if, $this->else); + return TypeCombinator::union( + $this->getNormalizedIf(), + $this->getNormalizedElse(), + ); } public function traverse(callable $cb): Type { $subject = $cb($this->subject); $target = $cb($this->target); - $if = $cb($this->if); - $else = $cb($this->else); + $if = $cb($this->getNormalizedIf()); + $else = $cb($this->getNormalizedElse()); + + if ( + $this->subject === $subject + && $this->target === $target + && $this->getNormalizedIf() === $if + && $this->getNormalizedElse() === $else + ) { + return $this; + } + + return new self($subject, $target, $if, $else, $this->negated); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } - if ($this->subject === $subject && $this->target === $target && $this->if === $if && $this->else === $else) { + $subject = $cb($this->subject, $right->subject); + $target = $cb($this->target, $right->target); + $if = $cb($this->getNormalizedIf(), $right->getNormalizedIf()); + $else = $cb($this->getNormalizedElse(), $right->getNormalizedElse()); + + if ( + $this->subject === $subject + && $this->target === $target + && $this->getNormalizedIf() === $if + && $this->getNormalizedElse() === $else + ) { return $this; } return new self($subject, $target, $if, $else, $this->negated); } + public function toPhpDocNode(): TypeNode + { + return new ConditionalTypeNode( + $this->subject->toPhpDocNode(), + $this->target->toPhpDocNode(), + $this->if->toPhpDocNode(), + $this->else->toPhpDocNode(), + $this->negated, + ); + } + /** * @param mixed[] $properties */ @@ -150,4 +202,34 @@ public static function __set_state(array $properties): Type ); } + private function getNormalizedIf(): Type + { + return $this->normalizedIf ??= TypeTraverser::map( + $this->if, + fn (Type $type, callable $traverse) => $type === $this->subject + ? (!$this->negated ? $this->getSubjectWithTargetIntersectedType() : $this->getSubjectWithTargetRemovedType()) + : $traverse($type), + ); + } + + private function getNormalizedElse(): Type + { + return $this->normalizedElse ??= TypeTraverser::map( + $this->else, + fn (Type $type, callable $traverse) => $type === $this->subject + ? (!$this->negated ? $this->getSubjectWithTargetRemovedType() : $this->getSubjectWithTargetIntersectedType()) + : $traverse($type), + ); + } + + private function getSubjectWithTargetIntersectedType(): Type + { + return $this->subjectWithTargetIntersectedType ??= TypeCombinator::intersect($this->subject, $this->target); + } + + private function getSubjectWithTargetRemovedType(): Type + { + return $this->subjectWithTargetRemovedType ??= TypeCombinator::remove($this->subject, $this->target); + } + } diff --git a/src/Type/ConditionalTypeForParameter.php b/src/Type/ConditionalTypeForParameter.php index 4702d20a00..57c2fe5d8d 100644 --- a/src/Type/ConditionalTypeForParameter.php +++ b/src/Type/ConditionalTypeForParameter.php @@ -2,6 +2,8 @@ namespace PHPStan\Type; +use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Traits\LateResolvableTypeTrait; @@ -132,9 +134,6 @@ protected function getResult(): Type return TypeCombinator::union($this->if, $this->else); } - /** - * @param callable(Type): Type $cb - */ public function traverse(callable $cb): Type { $target = $cb($this->target); @@ -145,7 +144,35 @@ public function traverse(callable $cb): Type return $this; } - return new ConditionalTypeForParameter($this->parameterName, $target, $if, $else, $this->negated); + return new self($this->parameterName, $target, $if, $else, $this->negated); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } + + $target = $cb($this->target, $right->target); + $if = $cb($this->if, $right->if); + $else = $cb($this->else, $right->else); + + if ($this->target === $target && $this->if === $if && $this->else === $else) { + return $this; + } + + return new self($this->parameterName, $target, $if, $else, $this->negated); + } + + public function toPhpDocNode(): TypeNode + { + return new ConditionalTypeForParameterNode( + $this->parameterName, + $this->target->toPhpDocNode(), + $this->if->toPhpDocNode(), + $this->else->toPhpDocNode(), + $this->negated, + ); } /** diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 5b2eb2de7e..db02994f2d 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -2,22 +2,37 @@ namespace PHPStan\Type\Constant; +use Nette\Utils\Strings; +use PHPStan\Analyser\OutOfClassScope; +use PHPStan\DependencyInjection\BleedingEdgeToggle; +use PHPStan\Internal\CombinationsHelper; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; +use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\Callables\FunctionCallableVariant; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\InaccessibleMethod; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\PhpVersionStaticAccessor; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; +use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ConstantType; use PHPStan\Type\ErrorType; use PHPStan\Type\GeneralizePrecision; -use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; @@ -25,12 +40,8 @@ use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; -use PHPStan\Type\ObjectType; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function array_keys; @@ -46,13 +57,15 @@ use function count; use function implode; use function in_array; +use function is_bool; use function is_int; use function is_string; use function min; use function pow; +use function range; use function sort; use function sprintf; -use function strpos; +use function str_contains; /** * @api @@ -62,6 +75,8 @@ class ConstantArrayType extends ArrayType implements ConstantType private const DESCRIBE_LIMIT = 8; + private TrinaryLogic $isList; + /** @var self[]|null */ private ?array $allArrays = null; @@ -80,6 +95,7 @@ public function __construct( private array $valueTypes, int|array $nextAutoIndexes = [0], private array $optionalKeys = [], + bool|TrinaryLogic $isList = false, ) { assert(count($keyTypes) === count($valueTypes)); @@ -93,18 +109,35 @@ public function __construct( $keyTypesCount = count($this->keyTypes); if ($keyTypesCount === 0) { $keyType = new NeverType(true); + $isList = TrinaryLogic::createYes(); } elseif ($keyTypesCount === 1) { $keyType = $this->keyTypes[0]; } else { $keyType = new UnionType($this->keyTypes); } + if (is_bool($isList)) { + $isList = TrinaryLogic::createFromBoolean($isList); + } + $this->isList = $isList; + parent::__construct( $keyType, count($valueTypes) > 0 ? TypeCombinator::union(...$valueTypes) : new NeverType(true), ); } + public function getConstantArrays(): array + { + return [$this]; + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + /** @deprecated Use isIterableAtLeastOnce()->no() instead */ public function isEmpty(): bool { return count($this->keyTypes) === 0; @@ -163,6 +196,12 @@ public function getAllArrays(): array $arrays = []; foreach ($optionalKeysCombinations as $combination) { $keys = array_merge($requiredKeys, $combination); + sort($keys); + + if ($this->isList->yes() && array_keys($keys) !== $keys) { + continue; + } + $builder = ConstantArrayTypeBuilder::createEmpty(); foreach ($keys as $i) { $builder->setOffsetValueType($this->keyTypes[$i], $this->valueTypes[$i]); @@ -213,30 +252,16 @@ public function getKeyTypes(): array return $this->keyTypes; } + /** @deprecated Use getFirstIterableKeyType() instead */ public function getFirstKeyType(): Type { - $keyTypes = []; - foreach ($this->keyTypes as $i => $keyType) { - $keyTypes[] = $keyType; - if (!$this->isOptionalKey($i)) { - break; - } - } - - return TypeCombinator::union(...$keyTypes); + return $this->getFirstIterableKeyType(); } + /** @deprecated Use getLastIterableKeyType() instead */ public function getLastKeyType(): Type { - $keyTypes = []; - for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) { - $keyTypes[] = $this->keyTypes[$i]; - if (!$this->isOptionalKey($i)) { - break; - } - } - - return TypeCombinator::union(...$keyTypes); + return $this->getLastIterableKeyType(); } /** @@ -247,30 +272,16 @@ public function getValueTypes(): array return $this->valueTypes; } + /** @deprecated Use getFirstIterableValueType() instead */ public function getFirstValueType(): Type { - $valueTypes = []; - foreach ($this->valueTypes as $i => $valueType) { - $valueTypes[] = $valueType; - if (!$this->isOptionalKey($i)) { - break; - } - } - - return TypeCombinator::union(...$valueTypes); + return $this->getFirstIterableValueType(); } + /** @deprecated Use getLastIterableValueType() instead */ public function getLastValueType(): Type { - $valueTypes = []; - for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) { - $valueTypes[] = $this->valueTypes[$i]; - if (!$this->isOptionalKey($i)) { - break; - } - } - - return TypeCombinator::union(...$valueTypes); + return $this->getLastIterableValueType(); } public function isOptionalKey(int $i): bool @@ -279,19 +290,28 @@ public function isOptionalKey(int $i): bool } public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof MixedType && !$type instanceof TemplateMixedType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } if ($type instanceof self && count($this->keyTypes) === 0) { - return TrinaryLogic::createFromBoolean(count($type->keyTypes) === 0); + return AcceptsResult::createFromBoolean(count($type->keyTypes) === 0); } - $result = TrinaryLogic::createYes(); + $result = AcceptsResult::createYes(); foreach ($this->keyTypes as $i => $keyType) { $valueType = $this->valueTypes[$i]; - $hasOffset = $type->hasOffsetValueType($keyType); + $hasOffsetValueType = $type->hasOffsetValueType($keyType); + $hasOffset = new AcceptsResult( + $hasOffsetValueType, + $hasOffsetValueType->yes() || !$type->isConstantArray()->yes() ? [] : [sprintf('Array %s have offset %s.', $hasOffsetValueType->no() ? 'does not' : 'might not', $keyType->describe(VerbosityLevel::value()))], + ); if ($hasOffset->no()) { if ($this->isOptionalKey($i)) { continue; @@ -299,19 +319,45 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic return $hasOffset; } if ($hasOffset->maybe() && $this->isOptionalKey($i)) { - $hasOffset = TrinaryLogic::createYes(); + $hasOffset = AcceptsResult::createYes(); } $result = $result->and($hasOffset); $otherValueType = $type->getOffsetValueType($keyType); - $acceptsValue = $valueType->accepts($otherValueType, $strictTypes); + $verbosity = VerbosityLevel::getRecommendedLevelByType($valueType, $otherValueType); + $acceptsValue = $valueType->acceptsWithReason($otherValueType, $strictTypes)->decorateReasons( + static fn (string $reason) => sprintf( + 'Offset %s (%s) does not accept type %s: %s', + $keyType->describe(VerbosityLevel::value()), + $valueType->describe($verbosity), + $otherValueType->describe($verbosity), + $reason, + ), + ); + if (!$acceptsValue->yes() && count($acceptsValue->reasons) === 0 && $type->isConstantArray()->yes()) { + $acceptsValue = new AcceptsResult($acceptsValue->result, [ + sprintf( + 'Offset %s (%s) does not accept type %s.', + $keyType->describe(VerbosityLevel::value()), + $valueType->describe($verbosity), + $otherValueType->describe($verbosity), + ), + ]); + } if ($acceptsValue->no()) { return $acceptsValue; } $result = $result->and($acceptsValue); } - return $result->and($type->isArray()); + $result = $result->and(new AcceptsResult($type->isArray(), [])); + if ($type->isOversizedArray()->yes()) { + if (!$result->no()) { + return AcceptsResult::createYes(); + } + } + + return $result; } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -336,12 +382,17 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return TrinaryLogic::createNo(); } - $results[] = TrinaryLogic::createMaybe(); + $results[] = TrinaryLogic::createYes(); continue; } elseif ($hasOffset->maybe() && !$this->isOptionalKey($i)) { $results[] = TrinaryLogic::createMaybe(); } - $results[] = $this->valueTypes[$i]->isSuperTypeOf($type->getOffsetValueType($keyType)); + + $isValueSuperType = $this->valueTypes[$i]->isSuperTypeOf($type->getOffsetValueType($keyType)); + if ($isValueSuperType->no()) { + return TrinaryLogic::createNo(); + } + $results[] = $isValueSuperType; } return TrinaryLogic::createYes()->and(...$results); @@ -353,10 +404,12 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return $result; } - return $result->and( - $this->getKeyType()->isSuperTypeOf($type->getKeyType()), - $this->getItemType()->isSuperTypeOf($type->getItemType()), - ); + $isKeySuperType = $this->getKeyType()->isSuperTypeOf($type->getKeyType()); + if ($isKeySuperType->no()) { + return TrinaryLogic::createNo(); + } + + return $result->and($isKeySuperType, $this->getItemType()->isSuperTypeOf($type->getItemType())); } if ($type instanceof CompoundType) { @@ -366,6 +419,16 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return TrinaryLogic::createNo(); } + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($this->isIterableAtLeastOnce()->no() && count($type->getConstantScalarValues()) === 1) { + // @phpstan-ignore equal.invalid, equal.notAllowed + return new ConstantBooleanType($type->getConstantScalarValues()[0] == []); // phpcs:ignore + } + + return new BooleanType(); + } + public function equals(Type $type): bool { if (!$type instanceof self) { @@ -408,9 +471,6 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createYes()->and(...$results); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { $typeAndMethodNames = $this->findTypeAndMethodNames(); @@ -433,44 +493,58 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) continue; } - array_push($acceptors, ...$method->getVariants()); + array_push($acceptors, ...FunctionCallableVariant::createFromVariants($method, $method->getVariants())); } return $acceptors; } - /** @deprecated Use findTypeAndMethodNames() instead */ - public function findTypeAndMethodName(): ?ConstantArrayTypeAndMethod + /** + * @return array{Type, Type}|array{} + */ + private function getClassOrObjectAndMethods(): array { if (count($this->keyTypes) !== 2) { - return null; + return []; } - if ($this->keyTypes[0]->isSuperTypeOf(new ConstantIntegerType(0))->no()) { - return null; + $classOrObject = null; + $method = null; + foreach ($this->keyTypes as $i => $keyType) { + if ($keyType->isSuperTypeOf(new ConstantIntegerType(0))->yes()) { + $classOrObject = $this->valueTypes[$i]; + continue; + } + + if (!$keyType->isSuperTypeOf(new ConstantIntegerType(1))->yes()) { + continue; + } + + $method = $this->valueTypes[$i]; } - if ($this->keyTypes[1]->isSuperTypeOf(new ConstantIntegerType(1))->no()) { - return null; + if ($classOrObject === null || $method === null) { + return []; } - [$classOrObject, $method] = $this->valueTypes; + return [$classOrObject, $method]; + } + + /** @deprecated Use findTypeAndMethodNames() instead */ + public function findTypeAndMethodName(): ?ConstantArrayTypeAndMethod + { + $callableArray = $this->getClassOrObjectAndMethods(); + if ($callableArray === []) { + return null; + } + [$classOrObject, $method] = $callableArray; if (!$method instanceof ConstantStringType) { return ConstantArrayTypeAndMethod::createUnknown(); } - if ($classOrObject instanceof ConstantStringType) { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if (!$reflectionProvider->hasClass($classOrObject->getValue())) { - return null; - } - $type = new ObjectType($reflectionProvider->getClass($classOrObject->getValue())->getName()); - } elseif ($classOrObject instanceof GenericClassStringType) { - $type = $classOrObject->getGenericType(); - } elseif ((new ObjectWithoutClassType())->isSuperTypeOf($classOrObject)->yes()) { - $type = $classOrObject; - } else { + $type = $classOrObject->getObjectTypeOrClassStringObjectType(); + if (!$type->isObject()->yes()) { return ConstantArrayTypeAndMethod::createUnknown(); } @@ -489,56 +563,45 @@ public function findTypeAndMethodName(): ?ConstantArrayTypeAndMethod /** @return ConstantArrayTypeAndMethod[] */ public function findTypeAndMethodNames(): array { - if (count($this->keyTypes) !== 2) { + $callableArray = $this->getClassOrObjectAndMethods(); + if ($callableArray === []) { return []; } - if ($this->keyTypes[0]->isSuperTypeOf(new ConstantIntegerType(0))->no()) { - return []; + [$classOrObject, $methods] = $callableArray; + if (count($methods->getConstantStrings()) === 0) { + return [ConstantArrayTypeAndMethod::createUnknown()]; } - if ($this->keyTypes[1]->isSuperTypeOf(new ConstantIntegerType(1))->no()) { - return []; + $type = $classOrObject->getObjectTypeOrClassStringObjectType(); + if (!$type->isObject()->yes()) { + return [ConstantArrayTypeAndMethod::createUnknown()]; } - [$classOrObjects, $methods] = $this->valueTypes; - $classOrObjects = TypeUtils::flattenTypes($classOrObjects); - $methods = TypeUtils::flattenTypes($methods); - $typeAndMethods = []; - foreach ($classOrObjects as $classOrObject) { - if ($classOrObject instanceof ConstantStringType) { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if (!$reflectionProvider->hasClass($classOrObject->getValue())) { - continue; - } - $type = new ObjectType($reflectionProvider->getClass($classOrObject->getValue())->getName()); - } elseif ($classOrObject instanceof GenericClassStringType) { - $type = $classOrObject->getGenericType(); - } elseif ((new ObjectWithoutClassType())->isSuperTypeOf($classOrObject)->yes()) { - $type = $classOrObject; - } else { - $typeAndMethods[] = ConstantArrayTypeAndMethod::createUnknown(); + $phpVersion = PhpVersionStaticAccessor::getInstance(); + foreach ($methods->getConstantStrings() as $method) { + $has = $type->hasMethod($method->getValue()); + if ($has->no()) { continue; } - foreach ($methods as $method) { - if (!$method instanceof ConstantStringType) { - $typeAndMethods[] = ConstantArrayTypeAndMethod::createUnknown(); - continue; - } - - $has = $type->hasMethod($method->getValue()); - if ($has->no()) { + if ( + BleedingEdgeToggle::isBleedingEdge() + && $has->yes() + && !$phpVersion->supportsCallableInstanceMethods() + ) { + $methodReflection = $type->getMethod($method->getValue(), new OutOfClassScope()); + if ($classOrObject->isString()->yes() && !$methodReflection->isStatic()) { continue; } + } - if ($this->isOptionalKey(0) || $this->isOptionalKey(1)) { - $has = $has->and(TrinaryLogic::createMaybe()); - } - - $typeAndMethods[] = ConstantArrayTypeAndMethod::createConcrete($type, $method->getValue(), $has); + if ($this->isOptionalKey(0) || $this->isOptionalKey(1)) { + $has = $has->and(TrinaryLogic::createMaybe()); } + + $typeAndMethods[] = ConstantArrayTypeAndMethod::createConcrete($type, $method->getValue(), $has); } return $typeAndMethods; @@ -546,17 +609,22 @@ public function findTypeAndMethodNames(): array public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - $offsetType = ArrayType::castToArrayKeyType($offsetType); + $offsetType = $offsetType->toArrayKey(); if ($offsetType instanceof UnionType) { - return TrinaryLogic::lazyExtremeIdentity($offsetType->getTypes(), fn (Type $innerType) => $this->hasOffsetValueType($innerType)); + $results = []; + foreach ($offsetType->getTypes() as $innerType) { + $results[] = $this->hasOffsetValueType($innerType); + } + + return TrinaryLogic::extremeIdentity(...$results); } $result = TrinaryLogic::createNo(); foreach ($this->keyTypes as $i => $keyType) { if ( $keyType instanceof ConstantIntegerType - && $offsetType instanceof StringType - && !$offsetType instanceof ConstantStringType + && !$offsetType->isString()->no() + && $offsetType->isConstantScalarValue()->no() ) { return TrinaryLogic::createMaybe(); } @@ -580,7 +648,7 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic public function getOffsetValueType(Type $offsetType): Type { - $offsetType = ArrayType::castToArrayKeyType($offsetType); + $offsetType = $offsetType->toArrayKey(); $matchingValueTypes = []; $all = true; foreach ($this->keyTypes as $i => $keyType) { @@ -620,9 +688,24 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $builder->getArray(); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + $offsetType = $offsetType->toArrayKey(); + $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); + foreach ($this->keyTypes as $keyType) { + if ($offsetType->isSuperTypeOf($keyType)->no()) { + continue; + } + + $builder->setOffsetValueType($keyType, $valueType); + } + + return $builder->getArray(); + } + public function unsetOffset(Type $offsetType): Type { - $offsetType = ArrayType::castToArrayKeyType($offsetType); + $offsetType = $offsetType->toArrayKey(); if ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) { foreach ($this->keyTypes as $i => $keyType) { if ($keyType->getValue() !== $offsetType->getValue()) { @@ -648,18 +731,18 @@ public function unsetOffset(Type $offsetType): Type $k++; } - return new self($newKeyTypes, $newValueTypes, $this->nextAutoIndexes, $newOptionalKeys); + return new self($newKeyTypes, $newValueTypes, $this->nextAutoIndexes, $newOptionalKeys, TrinaryLogic::createNo()); } return $this; } - $constantScalars = TypeUtils::getConstantScalars($offsetType); + $constantScalars = $offsetType->getConstantScalarTypes(); if (count($constantScalars) > 0) { $optionalKeys = $this->optionalKeys; foreach ($constantScalars as $constantScalar) { - $constantScalar = ArrayType::castToArrayKeyType($constantScalar); + $constantScalar = $constantScalar->toArrayKey(); if (!$constantScalar instanceof ConstantIntegerType && !$constantScalar instanceof ConstantStringType) { continue; } @@ -677,10 +760,136 @@ public function unsetOffset(Type $offsetType): Type } } - return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys); + return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, TrinaryLogic::createNo()); + } + + $optionalKeys = $this->optionalKeys; + $isList = $this->isList; + foreach ($this->keyTypes as $i => $keyType) { + if (!$offsetType->isSuperTypeOf($keyType)->yes()) { + continue; + } + $optionalKeys[] = $i; + $isList = TrinaryLogic::createNo(); + } + $optionalKeys = array_values(array_unique($optionalKeys)); + + return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, $isList); + } + + public function fillKeysArray(Type $valueType): Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + foreach ($this->valueTypes as $i => $keyType) { + if ($keyType->isInteger()->no()) { + $stringKeyType = $keyType->toString(); + if ($stringKeyType instanceof ErrorType) { + return $stringKeyType; + } + + $builder->setOffsetValueType($stringKeyType, $valueType, $this->isOptionalKey($i)); + } else { + $builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i)); + } + } + + return $builder->getArray(); + } + + public function flipArray(): Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + foreach ($this->keyTypes as $i => $keyType) { + $valueType = $this->valueTypes[$i]; + $builder->setOffsetValueType( + $valueType->toArrayKey(), + $keyType, + $this->isOptionalKey($i), + ); } - return new ArrayType($this->getKeyType(), $this->getItemType()); + return $builder->getArray(); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + foreach ($this->keyTypes as $i => $keyType) { + $valueType = $this->valueTypes[$i]; + $has = $otherArraysType->hasOffsetValueType($keyType); + if ($has->no()) { + continue; + } + $builder->setOffsetValueType($keyType, $valueType, $this->isOptionalKey($i) || !$has->yes()); + } + + return $builder->getArray(); + } + + public function popArray(): Type + { + return $this->removeLastElements(1); + } + + public function searchArray(Type $needleType): Type + { + $matches = []; + $hasIdenticalValue = false; + + foreach ($this->valueTypes as $index => $valueType) { + $isNeedleSuperType = $valueType->isSuperTypeOf($needleType); + if ($isNeedleSuperType->no()) { + continue; + } + + if ($needleType instanceof ConstantScalarType && $valueType instanceof ConstantScalarType + && $needleType->getValue() === $valueType->getValue() + && !$this->isOptionalKey($index) + ) { + $hasIdenticalValue = true; + } + + $matches[] = $this->keyTypes[$index]; + } + + if (count($matches) > 0) { + if ($hasIdenticalValue) { + return TypeCombinator::union(...$matches); + } + + return TypeCombinator::union(new ConstantBooleanType(false), ...$matches); + } + + return new ConstantBooleanType(false); + } + + public function shiftArray(): Type + { + return $this->removeFirstElements(1); + } + + public function shuffleArray(): Type + { + $valuesArray = $this->getValuesArray(); + + $isIterableAtLeastOnce = $valuesArray->isIterableAtLeastOnce(); + if ($isIterableAtLeastOnce->no()) { + return $valuesArray; + } + + $generalizedArray = new ArrayType($valuesArray->getIterableKeyType(), $valuesArray->getItemType()); + + if ($isIterableAtLeastOnce->yes()) { + $generalizedArray = TypeCombinator::intersect($generalizedArray, new NonEmptyArrayType()); + } + if ($valuesArray->isList->yes()) { + $generalizedArray = AccessoryArrayListType::intersectWith($generalizedArray); + } + + return $generalizedArray; } public function isIterableAtLeastOnce(): TrinaryLogic @@ -698,6 +907,80 @@ public function isIterableAtLeastOnce(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getArraySize(): Type + { + $optionalKeysCount = count($this->optionalKeys); + $totalKeysCount = count($this->getKeyTypes()); + if ($optionalKeysCount === 0) { + return new ConstantIntegerType($totalKeysCount); + } + + return IntegerRangeType::fromInterval($totalKeysCount - $optionalKeysCount, $totalKeysCount); + } + + public function getFirstIterableKeyType(): Type + { + $keyTypes = []; + foreach ($this->keyTypes as $i => $keyType) { + $keyTypes[] = $keyType; + if (!$this->isOptionalKey($i)) { + break; + } + } + + return TypeCombinator::union(...$keyTypes); + } + + public function getLastIterableKeyType(): Type + { + $keyTypes = []; + for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) { + $keyTypes[] = $this->keyTypes[$i]; + if (!$this->isOptionalKey($i)) { + break; + } + } + + return TypeCombinator::union(...$keyTypes); + } + + public function getFirstIterableValueType(): Type + { + $valueTypes = []; + foreach ($this->valueTypes as $i => $valueType) { + $valueTypes[] = $valueType; + if (!$this->isOptionalKey($i)) { + break; + } + } + + return TypeCombinator::union(...$valueTypes); + } + + public function getLastIterableValueType(): Type + { + $valueTypes = []; + for ($i = count($this->keyTypes) - 1; $i >= 0; $i--) { + $valueTypes[] = $this->valueTypes[$i]; + if (!$this->isOptionalKey($i)) { + break; + } + } + + return TypeCombinator::union(...$valueTypes); + } + + public function isConstantArray(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isList(): TrinaryLogic + { + return $this->isList; + } + + /** @deprecated Use popArray() instead */ public function removeLast(): self { return $this->removeLastElements(1); @@ -736,7 +1019,7 @@ private function removeLastElements(int $length): self array_pop($valueTypes); $nextAutoindex = $removedKeyType instanceof ConstantIntegerType ? $removedKeyType->getValue() - : $this->getNextAutoIndex(); // @phpstan-ignore-line + : $this->getNextAutoIndex(); // @phpstan-ignore method.deprecated continue; } @@ -753,9 +1036,11 @@ private function removeLastElements(int $length): self $valueTypes, $nextAutoindex, array_values($optionalKeys), + $this->isList, ); } + /** @deprecated Use shiftArray() instead */ public function removeFirst(): self { return $this->removeFirstElements(1); @@ -881,7 +1166,7 @@ public function reverse(bool $preserveKeys = false): self $keyTypesReversedKeys = array_keys($keyTypesReversed); $optionalKeys = array_map(static fn (int $optionalKey): int => $keyTypesReversedKeys[$optionalKey], $this->optionalKeys); - $reversed = new self($keyTypes, array_reverse($this->valueTypes), $this->nextAutoIndexes, $optionalKeys); + $reversed = new self($keyTypes, array_reverse($this->valueTypes), $this->nextAutoIndexes, $optionalKeys, TrinaryLogic::createNo()); return $preserveKeys ? $reversed : $reversed->reindex(); } @@ -920,12 +1205,12 @@ private function reindex(): self $autoIndex++; } - return new self($keyTypes, $this->valueTypes, [$autoIndex], $this->optionalKeys); + return new self($keyTypes, $this->valueTypes, [$autoIndex], $this->optionalKeys, TrinaryLogic::createYes()); } public function toBoolean(): BooleanType { - return $this->count()->toBoolean(); + return $this->getArraySize()->toBoolean(); } public function toInteger(): Type @@ -949,15 +1234,15 @@ public function generalize(GeneralizePrecision $precision): Type } $arrayType = new ArrayType( - $this->getKeyType()->generalize($precision), + $this->getIterableKeyType()->generalize($precision), $this->getItemType()->generalize($precision), ); $keyTypesCount = count($this->keyTypes); $optionalKeysCount = count($this->optionalKeys); + $accessoryTypes = []; if ($precision->isMoreSpecific() && ($keyTypesCount - $optionalKeysCount) < 32) { - $accessoryTypes = []; foreach ($this->keyTypes as $i => $keyType) { if ($this->isOptionalKey($i)) { continue; @@ -965,12 +1250,16 @@ public function generalize(GeneralizePrecision $precision): Type $accessoryTypes[] = new HasOffsetValueType($keyType, $this->valueTypes[$i]->generalize($precision)); } + } elseif ($keyTypesCount > $optionalKeysCount) { + $accessoryTypes[] = new NonEmptyArrayType(); + } - return TypeCombinator::intersect($arrayType, ...$accessoryTypes); + if ($this->isList()->yes()) { + $arrayType = AccessoryArrayListType::intersectWith($arrayType); } - if ($keyTypesCount > $optionalKeysCount) { - return TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + if (count($accessoryTypes) > 0) { + return TypeCombinator::intersect($arrayType, ...$accessoryTypes); } return $arrayType; @@ -986,18 +1275,24 @@ public function generalizeValues(): ArrayType $valueTypes[] = $valueType->generalize(GeneralizePrecision::lessSpecific()); } - return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys); + return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); } + /** @deprecated */ public function generalizeToArray(): Type { - if ($this->isEmpty()) { + $isIterableAtLeastOnce = $this->isIterableAtLeastOnce(); + if ($isIterableAtLeastOnce->no()) { return $this; } - $arrayType = new ArrayType($this->getKeyType(), $this->getItemType()); - if ($this->isIterableAtLeastOnce()->yes()) { - return TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + $arrayType = new ArrayType($this->getIterableKeyType(), $this->getItemType()); + + if ($isIterableAtLeastOnce->yes()) { + $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + } + if ($this->isList->yes()) { + $arrayType = AccessoryArrayListType::intersectWith($arrayType); } return $arrayType; @@ -1006,62 +1301,70 @@ public function generalizeToArray(): Type /** * @return self */ - public function getKeysArray(): ArrayType + public function getKeysArray(): Type { - $keyTypes = []; - $valueTypes = []; - $optionalKeys = []; - $autoIndex = 0; - - foreach ($this->keyTypes as $i => $keyType) { - $keyTypes[] = new ConstantIntegerType($i); - $valueTypes[] = $keyType; - $autoIndex++; - - if (!$this->isOptionalKey($i)) { - continue; - } - - $optionalKeys[] = $i; - } - - return new self($keyTypes, $valueTypes, $autoIndex, $optionalKeys); + return $this->getKeysOrValuesArray($this->keyTypes); } /** * @return self */ - public function getValuesArray(): ArrayType + public function getValuesArray(): Type + { + return $this->getKeysOrValuesArray($this->valueTypes); + } + + /** + * @param array $types + */ + private function getKeysOrValuesArray(array $types): self { + $count = count($types); + $autoIndexes = range($count - count($this->optionalKeys), $count); + assert($autoIndexes !== []); + + if ($this->isList->yes()) { + // Optimized version for lists: Assume that if a later key exists, then earlier keys also exist. + $keyTypes = array_map( + static fn (int $i): ConstantIntegerType => new ConstantIntegerType($i), + array_keys($types), + ); + return new self($keyTypes, $types, $autoIndexes, $this->optionalKeys, TrinaryLogic::createYes()); + } + $keyTypes = []; $valueTypes = []; $optionalKeys = []; - $autoIndex = 0; + $maxIndex = 0; - foreach ($this->valueTypes as $i => $valueType) { + foreach ($types as $i => $type) { $keyTypes[] = new ConstantIntegerType($i); - $valueTypes[] = $valueType; - $autoIndex++; - if (!$this->isOptionalKey($i)) { - continue; + if ($this->isOptionalKey($maxIndex)) { + // move $maxIndex to next non-optional key + do { + $maxIndex++; + } while ($maxIndex < $count && $this->isOptionalKey($maxIndex)); } - $optionalKeys[] = $i; + if ($i === $maxIndex) { + $valueTypes[] = $type; + } else { + $valueTypes[] = TypeCombinator::union(...array_slice($types, $i, $maxIndex - $i + 1)); + if ($maxIndex >= $count) { + $optionalKeys[] = $i; + } + } + $maxIndex++; } - return new self($keyTypes, $valueTypes, $autoIndex, $optionalKeys); + return new self($keyTypes, $valueTypes, $autoIndexes, $optionalKeys, TrinaryLogic::createYes()); } + /** @deprecated Use getArraySize() instead */ public function count(): Type { - $optionalKeysCount = count($this->optionalKeys); - $totalKeysCount = count($this->getKeyTypes()); - if ($optionalKeysCount === 0) { - return new ConstantIntegerType($totalKeysCount); - } - - return IntegerRangeType::fromInterval($totalKeysCount - $optionalKeysCount, $totalKeysCount); + return $this->getArraySize(); } public function describe(VerbosityLevel $level): string @@ -1083,9 +1386,9 @@ public function describe(VerbosityLevel $level): string $keyDescription = $keyType->getValue(); if (is_string($keyDescription)) { - if (strpos($keyDescription, '"') !== false) { + if (str_contains($keyDescription, '"')) { $keyDescription = sprintf('\'%s\'', $keyDescription); - } elseif (strpos($keyDescription, '\'') !== false) { + } elseif (str_contains($keyDescription, '\'')) { $keyDescription = sprintf('"%s"', $keyDescription); } } @@ -1140,7 +1443,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array { - $variance = $positionVariance->compose(TemplateTypeVariance::createInvariant()); + $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant()); $references = []; foreach ($this->keyTypes as $type) { @@ -1176,7 +1479,33 @@ public function traverse(callable $cb): Type return $this; } - return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys); + return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right->isArray()->yes()) { + return $this; + } + + $valueTypes = []; + + $stillOriginal = true; + foreach ($this->valueTypes as $i => $valueType) { + $keyType = $this->keyTypes[$i]; + $transformedValueType = $cb($valueType, $right->getOffsetValueType($keyType)); + if ($transformedValueType !== $valueType) { + $stillOriginal = false; + } + + $valueTypes[] = $transformedValueType; + } + + if ($stillOriginal) { + return $this; + } + + return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); } public function isKeysSupersetOf(self $otherArray): bool @@ -1233,7 +1562,7 @@ public function isKeysSupersetOf(self $otherArray): bool public function mergeWith(self $otherArray): self { - // only call this after verifying isKeysSupersetOf + // only call this after verifying isKeysSupersetOf, or if losing tagged unions is not an issue $valueTypes = $this->valueTypes; $optionalKeys = $this->optionalKeys; foreach ($this->keyTypes as $i => $keyType) { @@ -1251,10 +1580,10 @@ public function mergeWith(self $otherArray): self $optionalKeys = array_values(array_unique($optionalKeys)); - $nextAutoIndexes = array_unique(array_merge($this->nextAutoIndexes, $otherArray->nextAutoIndexes)); + $nextAutoIndexes = array_values(array_unique(array_merge($this->nextAutoIndexes, $otherArray->nextAutoIndexes))); sort($nextAutoIndexes); - return new self($this->keyTypes, $valueTypes, $nextAutoIndexes, $optionalKeys); + return new self($this->keyTypes, $valueTypes, $nextAutoIndexes, $optionalKeys, $this->isList->and($otherArray->isList)); } /** @@ -1282,7 +1611,7 @@ private static function findKeyIndex($otherKeyType, array $keyTypes): ?int public function makeOffsetRequired(Type $offsetType): self { - $offsetType = ArrayType::castToArrayKeyType($offsetType); + $offsetType = $offsetType->toArrayKey(); $optionalKeys = $this->optionalKeys; foreach ($this->keyTypes as $i => $keyType) { if (!$keyType->equals($offsetType)) { @@ -1292,7 +1621,7 @@ public function makeOffsetRequired(Type $offsetType): self foreach ($optionalKeys as $j => $key) { if ($i === $key) { unset($optionalKeys[$j]); - return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, array_values($optionalKeys)); + return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, array_values($optionalKeys), $this->isList); } } @@ -1302,12 +1631,101 @@ public function makeOffsetRequired(Type $offsetType): self return $this; } + public function toPhpDocNode(): TypeNode + { + $items = []; + $values = []; + $exportValuesOnly = true; + foreach ($this->keyTypes as $i => $keyType) { + if ($keyType->getValue() !== $i) { + $exportValuesOnly = false; + } + $keyPhpDocNode = $keyType->toPhpDocNode(); + if (!$keyPhpDocNode instanceof ConstTypeNode) { + continue; + } + $valueType = $this->valueTypes[$i]; + + /** @var ConstExprStringNode|ConstExprIntegerNode $keyNode */ + $keyNode = $keyPhpDocNode->constExpr; + if ($keyNode instanceof ConstExprStringNode) { + $value = $keyNode->value; + if (self::isValidIdentifier($value)) { + $keyNode = new IdentifierTypeNode($value); + } + } + + $isOptional = $this->isOptionalKey($i); + if ($isOptional) { + $exportValuesOnly = false; + } + $items[] = new ArrayShapeItemNode( + $keyNode, + $isOptional, + $valueType->toPhpDocNode(), + ); + $values[] = new ArrayShapeItemNode( + null, + $isOptional, + $valueType->toPhpDocNode(), + ); + } + + return new ArrayShapeNode($exportValuesOnly ? $values : $items); + } + + public static function isValidIdentifier(string $value): bool + { + $result = Strings::match($value, '~^(?:[\\\\]?+[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF-]*+)++$~si'); + + return $result !== null; + } + + public function getFiniteTypes(): array + { + $arraysArraysForCombinations = []; + $count = 0; + foreach ($this->getAllArrays() as $array) { + $values = $array->getValueTypes(); + $arraysForCombinations = []; + $combinationCount = 1; + foreach ($values as $valueType) { + $finiteTypes = $valueType->getFiniteTypes(); + if ($finiteTypes === []) { + return []; + } + $arraysForCombinations[] = $finiteTypes; + $combinationCount *= count($finiteTypes); + } + $arraysArraysForCombinations[] = $arraysForCombinations; + $count += $combinationCount; + } + + if ($count > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return []; + } + + $finiteTypes = []; + foreach ($arraysArraysForCombinations as $arraysForCombinations) { + $combinations = CombinationsHelper::combinations($arraysForCombinations); + foreach ($combinations as $combination) { + $builder = ConstantArrayTypeBuilder::createEmpty(); + foreach ($combination as $i => $v) { + $builder->setOffsetValueType($this->keyTypes[$i], $v); + } + $finiteTypes[] = $builder->getArray(); + } + } + + return $finiteTypes; + } + /** * @param mixed[] $properties */ public static function __set_state(array $properties): Type { - return new self($properties['keyTypes'], $properties['valueTypes'], $properties['nextAutoIndexes'] ?? $properties['nextAutoIndex'], $properties['optionalKeys'] ?? []); + return new self($properties['keyTypes'], $properties['valueTypes'], $properties['nextAutoIndexes'] ?? $properties['nextAutoIndex'], $properties['optionalKeys'] ?? [], $properties['isList'] ?? TrinaryLogic::createNo()); } } diff --git a/src/Type/Constant/ConstantArrayTypeBuilder.php b/src/Type/Constant/ConstantArrayTypeBuilder.php index 0cded15c97..8547fa39a9 100644 --- a/src/Type/Constant/ConstantArrayTypeBuilder.php +++ b/src/Type/Constant/ConstantArrayTypeBuilder.php @@ -3,7 +3,10 @@ namespace PHPStan\Type\Constant; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; +use PHPStan\Type\Accessory\OversizedArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -16,6 +19,7 @@ use function in_array; use function is_float; use function max; +use function min; use function range; /** @api */ @@ -26,6 +30,8 @@ class ConstantArrayTypeBuilder private bool $degradeToGeneralArray = false; + private bool $oversized = false; + /** * @param array $keyTypes * @param array $valueTypes @@ -37,13 +43,14 @@ private function __construct( private array $valueTypes, private array $nextAutoIndexes, private array $optionalKeys, + private TrinaryLogic $isList, ) { } public static function createEmpty(): self { - return new self([], [], [0], []); + return new self([], [], [0], [], TrinaryLogic::createYes()); } public static function createFromConstantArray(ConstantArrayType $startArrayType): self @@ -53,10 +60,11 @@ public static function createFromConstantArray(ConstantArrayType $startArrayType $startArrayType->getValueTypes(), $startArrayType->getNextAutoIndexes(), $startArrayType->getOptionalKeys(), + $startArrayType->isList(), ); if (count($startArrayType->getKeyTypes()) > self::ARRAY_COUNT_LIMIT) { - $builder->degradeToGeneralArray(); + $builder->degradeToGeneralArray(true); } return $builder; @@ -65,7 +73,7 @@ public static function createFromConstantArray(ConstantArrayType $startArrayType public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $optional = false): void { if ($offsetType !== null) { - $offsetType = ArrayType::castToArrayKeyType($offsetType); + $offsetType = $offsetType->toArrayKey(); } if (!$this->degradeToGeneralArray) { @@ -109,7 +117,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt } $newAutoIndexes[] = $newAutoIndex; - $this->nextAutoIndexes = array_unique($newAutoIndexes); + $this->nextAutoIndexes = array_values(array_unique($newAutoIndexes)); if ($optional || $hasOptional) { $this->optionalKeys[] = count($this->keyTypes) - 1; @@ -152,7 +160,15 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt $this->valueTypes[] = $valueType; if ($offsetType instanceof ConstantIntegerType) { + $min = min($this->nextAutoIndexes); $max = max($this->nextAutoIndexes); + if ($offsetType->getValue() > $min) { + if ($offsetType->getValue() <= $max) { + $this->isList = $this->isList->and(TrinaryLogic::createMaybe()); + } else { + $this->isList = TrinaryLogic::createNo(); + } + } if ($offsetType->getValue() >= $max) { /** @var int|float $newAutoIndex */ $newAutoIndex = $offsetType->getValue() + 1; @@ -165,6 +181,8 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt $this->nextAutoIndexes[] = $newAutoIndex; } } + } else { + $this->isList = TrinaryLogic::createNo(); } if ($optional) { @@ -178,7 +196,9 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt return; } - $scalarTypes = TypeUtils::getConstantScalars($offsetType); + $this->isList = TrinaryLogic::createNo(); + + $scalarTypes = $offsetType->getConstantScalarTypes(); if (count($scalarTypes) === 0) { $integerRanges = TypeUtils::getIntegerRanges($offsetType); if (count($integerRanges) > 0) { @@ -206,7 +226,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt $match = true; $valueTypes = $this->valueTypes; foreach ($scalarTypes as $scalarType) { - $scalarOffsetType = ArrayType::castToArrayKeyType($scalarType); + $scalarOffsetType = $scalarType->toArrayKey(); if (!$scalarOffsetType instanceof ConstantIntegerType && !$scalarOffsetType instanceof ConstantStringType) { throw new ShouldNotHappenException(); } @@ -238,6 +258,8 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt if ($offsetType === null) { $offsetType = TypeCombinator::union(...array_map(static fn (int $index) => new ConstantIntegerType($index), $this->nextAutoIndexes)); + } else { + $this->isList = TrinaryLogic::createNo(); } $this->keyTypes[] = $offsetType; @@ -248,9 +270,10 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt $this->degradeToGeneralArray = true; } - public function degradeToGeneralArray(): void + public function degradeToGeneralArray(bool $oversized = false): void { $this->degradeToGeneralArray = true; + $this->oversized = $this->oversized || $oversized; } public function getArray(): Type @@ -263,7 +286,7 @@ public function getArray(): Type if (!$this->degradeToGeneralArray) { /** @var array $keyTypes */ $keyTypes = $this->keyTypes; - return new ConstantArrayType($keyTypes, $this->valueTypes, $this->nextAutoIndexes, $this->optionalKeys); + return new ConstantArrayType($keyTypes, $this->valueTypes, $this->nextAutoIndexes, $this->optionalKeys, $this->isList); } $array = new ArrayType( @@ -272,10 +295,23 @@ public function getArray(): Type ); if (count($this->optionalKeys) < $keyTypesCount) { - return TypeCombinator::intersect($array, new NonEmptyArrayType()); + $array = TypeCombinator::intersect($array, new NonEmptyArrayType()); + } + + if ($this->oversized) { + $array = TypeCombinator::intersect($array, new OversizedArrayType()); + } + + if ($this->isList->yes()) { + $array = AccessoryArrayListType::intersectWith($array); } return $array; } + public function isList(): bool + { + return $this->isList->yes(); + } + } diff --git a/src/Type/Constant/ConstantBooleanType.php b/src/Type/Constant/ConstantBooleanType.php index 972e2f9b2e..88b7822f6a 100644 --- a/src/Type/Constant/ConstantBooleanType.php +++ b/src/Type/Constant/ConstantBooleanType.php @@ -2,6 +2,10 @@ namespace PHPStan\Type\Constant; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\TrinaryLogic; use PHPStan\Type\BooleanType; use PHPStan\Type\ConstantScalarType; use PHPStan\Type\GeneralizePrecision; @@ -16,7 +20,9 @@ class ConstantBooleanType extends BooleanType implements ConstantScalarType { - use ConstantScalarTypeTrait; + use ConstantScalarTypeTrait { + looseCompare as private scalarLooseCompare; + } /** @api */ public function __construct(private bool $value) @@ -91,11 +97,31 @@ public function toFloat(): Type return new ConstantFloatType((float) $this->value); } + public function toArrayKey(): Type + { + return new ConstantIntegerType((int) $this->value); + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->value === true); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->value === false); + } + public function generalize(GeneralizePrecision $precision): Type { return new BooleanType(); } + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode($this->value ? 'true' : 'false'); + } + /** * @param mixed[] $properties */ @@ -104,4 +130,13 @@ public static function __set_state(array $properties): Type return new self($properties['value']); } + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type->isObject()->yes()) { + return $this; + } + + return $this->scalarLooseCompare($type, $phpVersion); + } + } diff --git a/src/Type/Constant/ConstantFloatType.php b/src/Type/Constant/ConstantFloatType.php index 1d586d04bf..3468ab5de8 100644 --- a/src/Type/Constant/ConstantFloatType.php +++ b/src/Type/Constant/ConstantFloatType.php @@ -2,8 +2,9 @@ namespace PHPStan\Type\Constant; -use PHPStan\TrinaryLogic; -use PHPStan\Type\CompoundType; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode; +use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Type\ConstantScalarType; use PHPStan\Type\FloatType; use PHPStan\Type\GeneralizePrecision; @@ -11,8 +12,11 @@ use PHPStan\Type\Traits\ConstantScalarTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function ini_get; +use function ini_set; use function is_finite; -use function strpos; +use function is_nan; +use function str_contains; /** @api */ class ConstantFloatType extends FloatType implements ConstantScalarType @@ -33,44 +37,33 @@ public function getValue(): float return $this->value; } - public function describe(VerbosityLevel $level): string + public function equals(Type $type): bool { - return $level->handle( - static fn (): string => 'float', - function (): string { - $formatted = (string) $this->value; - if (is_finite($this->value) && strpos($formatted, '.') === false) { - $formatted .= '.0'; - } - - return $formatted; - }, - ); + return $type instanceof self && ($this->value === $type->value || is_nan($this->value) && is_nan($type->value)); } - public function isSuperTypeOf(Type $type): TrinaryLogic + private function castFloatToString(float $value): string { - if ($type instanceof self) { - if (!$this->equals($type)) { - if ($this->describe(VerbosityLevel::value()) === $type->describe(VerbosityLevel::value())) { - return TrinaryLogic::createMaybe(); - } - - return TrinaryLogic::createNo(); + $precisionBackup = ini_get('precision'); + ini_set('precision', '-1'); + try { + $valueStr = (string) $value; + if (is_finite($value) && !str_contains($valueStr, '.')) { + $valueStr .= '.0'; } - return TrinaryLogic::createYes(); - } - - if ($type instanceof parent) { - return TrinaryLogic::createMaybe(); - } - - if ($type instanceof CompoundType) { - return $type->isSubTypeOf($this); + return $valueStr; + } finally { + ini_set('precision', $precisionBackup); } + } - return TrinaryLogic::createNo(); + public function describe(VerbosityLevel $level): string + { + return $level->handle( + static fn (): string => 'float', + fn (): string => $this->castFloatToString($this->value), + ); } public function toString(): Type @@ -83,11 +76,24 @@ public function toInteger(): Type return new ConstantIntegerType((int) $this->value); } + public function toArrayKey(): Type + { + return new ConstantIntegerType((int) $this->value); + } + public function generalize(GeneralizePrecision $precision): Type { return new FloatType(); } + /** + * @return ConstTypeNode + */ + public function toPhpDocNode(): TypeNode + { + return new ConstTypeNode(new ConstExprFloatNode($this->castFloatToString($this->value))); + } + /** * @param mixed[] $properties */ diff --git a/src/Type/Constant/ConstantIntegerType.php b/src/Type/Constant/ConstantIntegerType.php index 5497ce6ebd..ae3d0511a8 100644 --- a/src/Type/Constant/ConstantIntegerType.php +++ b/src/Type/Constant/ConstantIntegerType.php @@ -2,6 +2,9 @@ namespace PHPStan\Type\Constant; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; +use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; use PHPStan\Type\CompoundType; use PHPStan\Type\ConstantScalarType; @@ -78,11 +81,24 @@ public function toString(): Type return new ConstantStringType((string) $this->value); } + public function toArrayKey(): Type + { + return $this; + } + public function generalize(GeneralizePrecision $precision): Type { return new IntegerType(); } + /** + * @return ConstTypeNode + */ + public function toPhpDocNode(): TypeNode + { + return new ConstTypeNode(new ConstExprIntegerNode((string) $this->value)); + } + /** * @param mixed[] $properties */ diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index 0b0c3f6fbc..010e65777e 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -5,10 +5,16 @@ use Nette\Utils\RegexpException; use Nette\Utils\Strings; use PhpParser\Node\Name; +use PHPStan\Analyser\OutOfClassScope; +use PHPStan\DependencyInjection\BleedingEdgeToggle; +use PHPStan\PhpDocParser\Ast\ConstExpr\QuoteAwareConstExprStringNode; +use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\Callables\FunctionCallableVariant; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\InaccessibleMethod; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\PhpVersionStaticAccessor; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\ShouldNotHappenException; @@ -35,10 +41,14 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; use function addcslashes; +use function in_array; use function is_float; +use function is_int; use function is_numeric; +use function key; use function strlen; use function substr; +use function substr_count; /** @api */ class ConstantStringType extends StringType implements ConstantScalarType @@ -51,6 +61,8 @@ class ConstantStringType extends StringType implements ConstantScalarType private ?ObjectType $objectType = null; + private ?Type $arrayKeyType = null; + /** @api */ public function __construct(private string $value, private bool $isClassString = false) { @@ -62,15 +74,42 @@ public function getValue(): string return $this->value; } - public function isClassString(): bool + public function getConstantStrings(): array + { + return [$this]; + } + + public function isClassStringType(): TrinaryLogic { if ($this->isClassString) { - return true; + return TrinaryLogic::createYes(); } $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - return $reflectionProvider->hasClass($this->value); + return TrinaryLogic::createFromBoolean($reflectionProvider->hasClass($this->value)); + } + + public function getClassStringObjectType(): Type + { + if ($this->isClassStringType()->yes()) { + return new ObjectType($this->value); + } + + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this->getClassStringObjectType(); + } + + /** + * @deprecated use isClassStringType() instead + */ + public function isClassString(): bool + { + return $this->isClassStringType()->yes(); } public function describe(VerbosityLevel $level): string @@ -96,7 +135,8 @@ function (): string { private function export(string $value): string { - if (Strings::match($value, '([\000-\037])') !== null) { + $escapedValue = addcslashes($value, "\0..\37"); + if ($escapedValue !== $value) { return '"' . addcslashes($value, "\0..\37\\\"") . '"'; } @@ -133,7 +173,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return TrinaryLogic::createNo(); } if ($type instanceof ClassStringType) { - return $this->isClassString() ? TrinaryLogic::createMaybe() : TrinaryLogic::createNo(); + return $this->isClassStringType()->yes() ? TrinaryLogic::createMaybe() : TrinaryLogic::createNo(); } if ($type instanceof self) { @@ -171,8 +211,18 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createMaybe(); } + $phpVersion = PhpVersionStaticAccessor::getInstance(); $classRef = $reflectionProvider->getClass($matches[1]); if ($classRef->hasMethod($matches[2])) { + $method = $classRef->getMethod($matches[2], new OutOfClassScope()); + if ( + BleedingEdgeToggle::isBleedingEdge() + && !$phpVersion->supportsCallableInstanceMethods() + && !$method->isStatic() + ) { + return TrinaryLogic::createNo(); + } + return TrinaryLogic::createYes(); } @@ -186,9 +236,6 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createNo(); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); @@ -196,7 +243,8 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) // 'my_function' $functionName = new Name($this->value); if ($reflectionProvider->hasFunction($functionName, null)) { - return $reflectionProvider->getFunction($functionName, null)->getVariants(); + $function = $reflectionProvider->getFunction($functionName, null); + return FunctionCallableVariant::createFromVariants($function, $function->getVariants()); } // 'MyClass::myStaticFunction' @@ -213,7 +261,7 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) return [new InaccessibleMethod($method)]; } - return $method->getVariants(); + return FunctionCallableVariant::createFromVariants($method, $method->getVariants()); } if (!$classReflection->getNativeReflection()->isFinal()) { @@ -227,7 +275,6 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) public function toNumber(): Type { if (is_numeric($this->value)) { - /** @var mixed $value */ $value = $this->value; $value = +$value; if (is_float($value)) { @@ -250,6 +297,17 @@ public function toFloat(): Type return new ConstantFloatType((float) $this->value); } + public function toArrayKey(): Type + { + if ($this->arrayKeyType !== null) { + return $this->arrayKeyType; + } + + /** @var int|string $offsetValue */ + $offsetValue = key([$this->value => null]); + return $this->arrayKeyType = is_int($offsetValue) ? new ConstantIntegerType($offsetValue) : new ConstantStringType($offsetValue); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -267,7 +325,7 @@ public function isNonEmptyString(): TrinaryLogic public function isNonFalsyString(): TrinaryLogic { - return TrinaryLogic::createFromBoolean($this->getValue() !== '' && $this->getValue() !== '0'); + return TrinaryLogic::createFromBoolean(!in_array($this->getValue(), ['', '0'], true)); } public function isLiteralString(): TrinaryLogic @@ -326,6 +384,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return parent::setOffsetValueType($offsetType, $valueType); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return parent::setOffsetValueType($offsetType, $valueType); + } + public function append(self $otherString): self { return new self($this->getValue() . $otherString->getValue()); @@ -428,7 +491,7 @@ public function getGreaterOrEqualType(): Type public function canAccessConstants(): TrinaryLogic { - return TrinaryLogic::createFromBoolean($this->isClassString()); + return $this->isClassStringType(); } public function hasConstant(string $constantName): TrinaryLogic @@ -446,6 +509,15 @@ private function getObjectType(): ObjectType return $this->objectType ??= new ObjectType($this->value); } + public function toPhpDocNode(): TypeNode + { + if (substr_count($this->value, "\n") > 0) { + return $this->generalize(GeneralizePrecision::moreSpecific())->toPhpDocNode(); + } + + return new ConstTypeNode(new QuoteAwareConstExprStringNode($this->value, QuoteAwareConstExprStringNode::SINGLE_QUOTED)); + } + /** * @param mixed[] $properties */ diff --git a/src/Type/Constant/OversizedArrayBuilder.php b/src/Type/Constant/OversizedArrayBuilder.php new file mode 100644 index 0000000000..663f1c1513 --- /dev/null +++ b/src/Type/Constant/OversizedArrayBuilder.php @@ -0,0 +1,106 @@ +items; + for ($i = 0; $i < count($items); $i++) { + $item = $items[$i]; + if ($item === null) { + continue; + } + if (!$item->unpack) { + continue; + } + + $valueType = $getTypeCallback($item->value); + if ($valueType instanceof ConstantArrayType) { + array_splice($items, $i, 1); + foreach ($valueType->getKeyTypes() as $j => $innerKeyType) { + $innerValueType = $valueType->getValueTypes()[$j]; + if ($innerKeyType->isString()->no()) { + $keyExpr = null; + } else { + $keyExpr = new TypeExpr($innerKeyType); + } + array_splice($items, $i++, 0, [new Expr\ArrayItem( + new TypeExpr($innerValueType), + $keyExpr, + )]); + } + } else { + array_splice($items, $i, 1, [new Expr\ArrayItem( + new TypeExpr($valueType->getIterableValueType()), + new TypeExpr($valueType->getIterableKeyType()), + )]); + } + } + foreach ($items as $item) { + if ($item === null) { + continue; + } + if ($item->unpack) { + throw new ShouldNotHappenException(); + } + if ($item->key !== null) { + $itemKeyType = $getTypeCallback($item->key); + if (!$itemKeyType instanceof ConstantIntegerType) { + $isList = false; + } elseif ($itemKeyType->getValue() !== $nextAutoIndex) { + $isList = false; + $nextAutoIndex = $itemKeyType->getValue() + 1; + } else { + $nextAutoIndex++; + } + } else { + $itemKeyType = new ConstantIntegerType($nextAutoIndex); + $nextAutoIndex++; + } + + $generalizedKeyType = $itemKeyType->generalize(GeneralizePrecision::moreSpecific()); + $keyTypes[$generalizedKeyType->describe(VerbosityLevel::precise())] = $generalizedKeyType; + + $itemValueType = $getTypeCallback($item->value); + $generalizedValueType = $itemValueType->generalize(GeneralizePrecision::moreSpecific()); + $valueTypes[$generalizedValueType->describe(VerbosityLevel::precise())] = $generalizedValueType; + } + + $keyType = TypeCombinator::union(...array_values($keyTypes)); + $valueType = TypeCombinator::union(...array_values($valueTypes)); + + $arrayType = new ArrayType($keyType, $valueType); + if ($isList) { + $arrayType = AccessoryArrayListType::intersectWith($arrayType); + } + + return TypeCombinator::intersect($arrayType, new NonEmptyArrayType(), new OversizedArrayType()); + } + +} diff --git a/src/Type/ConstantType.php b/src/Type/ConstantType.php index 0f08d47c81..d5fab2b652 100644 --- a/src/Type/ConstantType.php +++ b/src/Type/ConstantType.php @@ -6,6 +6,4 @@ interface ConstantType extends Type { - public function generalize(GeneralizePrecision $precision): Type; - } diff --git a/src/Type/ConstantTypeHelper.php b/src/Type/ConstantTypeHelper.php index 5dc52ef920..2a1f4cd4f7 100644 --- a/src/Type/ConstantTypeHelper.php +++ b/src/Type/ConstantTypeHelper.php @@ -46,7 +46,7 @@ public static function getTypeFromValue($value): Type } elseif (is_array($value)) { $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); if (count($value) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { - $arrayBuilder->degradeToGeneralArray(); + $arrayBuilder->degradeToGeneralArray(true); } foreach ($value as $k => $v) { $arrayBuilder->setOffsetValueType(self::getTypeFromValue($k), self::getTypeFromValue($v)); diff --git a/src/Type/DynamicFunctionReturnTypeExtension.php b/src/Type/DynamicFunctionReturnTypeExtension.php index 92941735a3..eb7b6222ff 100644 --- a/src/Type/DynamicFunctionReturnTypeExtension.php +++ b/src/Type/DynamicFunctionReturnTypeExtension.php @@ -6,7 +6,23 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -/** @api */ +/** + * This is the interface dynamic return type extensions implement for functions. + * + * To register it in the configuration file use the `phpstan.broker.dynamicFunctionReturnTypeExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.broker.dynamicFunctionReturnTypeExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/dynamic-return-type-extensions + * + * @api + */ interface DynamicFunctionReturnTypeExtension { diff --git a/src/Type/DynamicFunctionThrowTypeExtension.php b/src/Type/DynamicFunctionThrowTypeExtension.php index 46f56ce61b..9e16865c3c 100644 --- a/src/Type/DynamicFunctionThrowTypeExtension.php +++ b/src/Type/DynamicFunctionThrowTypeExtension.php @@ -6,7 +6,23 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -/** @api */ +/** + * This is the interface dynamic throw type extensions implement for functions. + * + * To register it in the configuration file use the `phpstan.dynamicFunctionThrowTypeExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.dynamicFunctionThrowTypeExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/dynamic-throw-type-extensions + * + * @api + */ interface DynamicFunctionThrowTypeExtension { diff --git a/src/Type/DynamicMethodReturnTypeExtension.php b/src/Type/DynamicMethodReturnTypeExtension.php index 35f5b505ca..6d03b43f10 100644 --- a/src/Type/DynamicMethodReturnTypeExtension.php +++ b/src/Type/DynamicMethodReturnTypeExtension.php @@ -6,7 +6,23 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -/** @api */ +/** + * This is the interface dynamic return type extensions implement for non-static methods. + * + * To register it in the configuration file use the `phpstan.broker.dynamicMethodReturnTypeExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.broker.dynamicMethodReturnTypeExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/dynamic-return-type-extensions + * + * @api + */ interface DynamicMethodReturnTypeExtension { diff --git a/src/Type/DynamicMethodThrowTypeExtension.php b/src/Type/DynamicMethodThrowTypeExtension.php index e6fba9fc7f..228604cb83 100644 --- a/src/Type/DynamicMethodThrowTypeExtension.php +++ b/src/Type/DynamicMethodThrowTypeExtension.php @@ -6,7 +6,23 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -/** @api */ +/** + * This is the interface dynamic throw type extensions implement for non-static methods. + * + * To register it in the configuration file use the `phpstan.dynamicMethodThrowTypeExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.dynamicMethodThrowTypeExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/dynamic-throw-type-extensions + * + * @api + */ interface DynamicMethodThrowTypeExtension { diff --git a/src/Type/DynamicReturnTypeExtensionRegistry.php b/src/Type/DynamicReturnTypeExtensionRegistry.php index b747ba4a71..ea3c27a13c 100644 --- a/src/Type/DynamicReturnTypeExtensionRegistry.php +++ b/src/Type/DynamicReturnTypeExtensionRegistry.php @@ -6,6 +6,7 @@ use PHPStan\Reflection\BrokerAwareExtension; use PHPStan\Reflection\ReflectionProvider; use function array_merge; +use function strtolower; class DynamicReturnTypeExtensionRegistry { @@ -46,7 +47,7 @@ public function getDynamicMethodReturnTypeExtensionsForClass(string $className): if ($this->dynamicMethodReturnTypeExtensionsByClass === null) { $byClass = []; foreach ($this->dynamicMethodReturnTypeExtensions as $extension) { - $byClass[$extension->getClass()][] = $extension; + $byClass[strtolower($extension->getClass())][] = $extension; } $this->dynamicMethodReturnTypeExtensionsByClass = $byClass; @@ -62,7 +63,7 @@ public function getDynamicStaticMethodReturnTypeExtensionsForClass(string $class if ($this->dynamicStaticMethodReturnTypeExtensionsByClass === null) { $byClass = []; foreach ($this->dynamicStaticMethodReturnTypeExtensions as $extension) { - $byClass[$extension->getClass()][] = $extension; + $byClass[strtolower($extension->getClass())][] = $extension; } $this->dynamicStaticMethodReturnTypeExtensionsByClass = $byClass; @@ -83,6 +84,7 @@ private function getDynamicExtensionsForType(array $extensions, string $classNam $extensionsForClass = [[]]; $class = $this->reflectionProvider->getClass($className); foreach (array_merge([$className], $class->getParentClassesNames(), $class->getNativeReflection()->getInterfaceNames()) as $extensionClassName) { + $extensionClassName = strtolower($extensionClassName); if (!isset($extensions[$extensionClassName])) { continue; } diff --git a/src/Type/DynamicStaticMethodReturnTypeExtension.php b/src/Type/DynamicStaticMethodReturnTypeExtension.php index 94560039a6..87b74c9af4 100644 --- a/src/Type/DynamicStaticMethodReturnTypeExtension.php +++ b/src/Type/DynamicStaticMethodReturnTypeExtension.php @@ -6,7 +6,23 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -/** @api */ +/** + * This is the interface dynamic return type extensions implement for static methods. + * + * To register it in the configuration file use the `phpstan.broker.dynamicStaticMethodReturnTypeExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.broker.dynamicStaticMethodReturnTypeExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/dynamic-return-type-extensions + * + * @api + */ interface DynamicStaticMethodReturnTypeExtension { diff --git a/src/Type/DynamicStaticMethodThrowTypeExtension.php b/src/Type/DynamicStaticMethodThrowTypeExtension.php index b01735a6bd..fa9926dea3 100644 --- a/src/Type/DynamicStaticMethodThrowTypeExtension.php +++ b/src/Type/DynamicStaticMethodThrowTypeExtension.php @@ -6,7 +6,23 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -/** @api */ +/** + * This is the interface dynamic throw type extensions implement for static methods. + * + * To register it in the configuration file use the `phpstan.dynamicStaticMethodThrowTypeExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.dynamicStaticMethodThrowTypeExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/dynamic-throw-type-extensions + * + * @api + */ interface DynamicStaticMethodThrowTypeExtension { diff --git a/src/Type/Enum/EnumCaseObjectType.php b/src/Type/Enum/EnumCaseObjectType.php index cda6d91a18..4589f8e4c2 100644 --- a/src/Type/Enum/EnumCaseObjectType.php +++ b/src/Type/Enum/EnumCaseObjectType.php @@ -2,12 +2,17 @@ namespace PHPStan\Type\Enum; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; +use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Php\EnumPropertyReflection; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\Php\EnumUnresolvedPropertyPrototypeReflection; +use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\GeneralizePrecision; @@ -54,7 +59,12 @@ public function equals(Type $type): bool public function accepts(Type $type, bool $strictTypes): TrinaryLogic { - return $this->isSuperTypeOf($type); + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + { + return new AcceptsResult($this->isSuperTypeOf($type), []); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -104,15 +114,17 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } - public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { $classReflection = $this->getClassReflection(); if ($classReflection === null) { - return parent::getProperty($propertyName, $scope); + return parent::getUnresolvedPropertyPrototype($propertyName, $scope); } if ($propertyName === 'name') { - return new EnumPropertyReflection($classReflection, new ConstantStringType($this->enumCaseName)); + return new EnumUnresolvedPropertyPrototypeReflection( + new EnumPropertyReflection($classReflection, new ConstantStringType($this->enumCaseName)), + ); } if ($classReflection->isBackedEnum() && $propertyName === 'value') { @@ -123,11 +135,33 @@ public function getProperty(string $propertyName, ClassMemberAccessAnswerer $sco throw new ShouldNotHappenException(); } - return new EnumPropertyReflection($classReflection, $valueType); + return new EnumUnresolvedPropertyPrototypeReflection( + new EnumPropertyReflection($classReflection, $valueType), + ); } } - return parent::getProperty($propertyName, $scope); + return parent::getUnresolvedPropertyPrototype($propertyName, $scope); + } + + public function getBackingValueType(): ?Type + { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return null; + } + + if (!$classReflection->isBackedEnum()) { + return null; + } + + if ($classReflection->hasEnumCase($this->enumCaseName)) { + $enumCase = $classReflection->getEnumCase($this->enumCaseName); + + return $enumCase->getBackingValueType(); + } + + return null; } public function generalize(GeneralizePrecision $precision): Type @@ -145,6 +179,21 @@ public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic return TrinaryLogic::createNo(); } + public function getEnumCases(): array + { + return [$this]; + } + + public function toPhpDocNode(): TypeNode + { + return new ConstTypeNode( + new ConstFetchNode( + $this->getClassName(), + $this->getEnumCaseName(), + ), + ); + } + /** * @param mixed[] $properties */ diff --git a/src/Type/ExponentiateHelper.php b/src/Type/ExponentiateHelper.php new file mode 100644 index 0000000000..4e62ed9a5d --- /dev/null +++ b/src/Type/ExponentiateHelper.php @@ -0,0 +1,108 @@ +getTypes() as $unionType) { + $results[] = self::exponentiate($base, $unionType); + } + return TypeCombinator::union(...$results); + } + + if ($exponent instanceof NeverType) { + return new NeverType(); + } + + $allowedExponentTypes = new UnionType([ + new IntegerType(), + new FloatType(), + new StringType(), + new BooleanType(), + new NullType(), + ]); + if (!$allowedExponentTypes->isSuperTypeOf($exponent)->yes()) { + return new ErrorType(); + } + + if ($base instanceof ConstantScalarType) { + $result = self::exponentiateConstantScalar($base, $exponent); + if ($result !== null) { + return $result; + } + } + + // exponentiation of a float, stays a float + $float = new FloatType(); + $isFloatBase = $float->isSuperTypeOf($base)->yes(); + + $isLooseZero = (new ConstantIntegerType(0))->isSuperTypeOf($exponent->toNumber()); + if ($isLooseZero->yes()) { + if ($isFloatBase) { + return new ConstantFloatType(1); + } + + return new ConstantIntegerType(1); + } + + $isLooseOne = (new ConstantIntegerType(1))->isSuperTypeOf($exponent->toNumber()); + if ($isLooseOne->yes()) { + $possibleResults = new UnionType([ + new FloatType(), + new IntegerType(), + ]); + + if ($possibleResults->isSuperTypeOf($base)->yes()) { + return $base; + } + } + + if ($isFloatBase) { + return new FloatType(); + } + + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + private static function exponentiateConstantScalar(ConstantScalarType $base, Type $exponent): ?Type + { + if ($exponent instanceof IntegerRangeType) { + $min = null; + $max = null; + if ($exponent->getMin() !== null) { + $min = $base->getValue() ** $exponent->getMin(); + } + if ($exponent->getMax() !== null) { + $max = $base->getValue() ** $exponent->getMax(); + } + + if (!is_float($min) && !is_float($max)) { + return IntegerRangeType::fromInterval($min, $max); + } + } + + if ($exponent instanceof ConstantScalarType) { + $result = $base->getValue() ** $exponent->getValue(); + if (is_int($result)) { + return new ConstantIntegerType($result); + } + return new ConstantFloatType($result); + } + + return null; + } + +} diff --git a/src/Type/ExpressionTypeResolverExtension.php b/src/Type/ExpressionTypeResolverExtension.php new file mode 100644 index 0000000000..1bc7a710e5 --- /dev/null +++ b/src/Type/ExpressionTypeResolverExtension.php @@ -0,0 +1,28 @@ + $extensions + */ + public function __construct( + private array $extensions, + ) + { + } + + /** + * @return array + */ + public function getExtensions(): array + { + return $this->extensions; + } + +} diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index c8dd9d77b6..7886d40e7e 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -3,7 +3,6 @@ namespace PHPStan\Type; use Closure; -use PhpParser\Comment\Doc; use PhpParser\Node; use PHPStan\Analyser\NameScope; use PHPStan\BetterReflection\Util\GetLastDocComment; @@ -21,6 +20,8 @@ use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use function array_key_exists; use function array_keys; use function array_map; @@ -34,7 +35,7 @@ use function ltrim; use function md5; use function sprintf; -use function strpos; +use function str_contains; use function strtolower; class FileTypeMapper @@ -48,7 +49,7 @@ class FileTypeMapper private int $memoryCacheCount = 0; - /** @var (false|callable(): NameScope|NameScope)[][] */ + /** @var (true|callable(): NameScope|NameScope)[][] */ private array $inProcess = []; /** @var array */ @@ -76,10 +77,6 @@ public function getResolvedPhpDoc( string $docComment, ): ResolvedPhpDocBlock { - if ($fileName !== null) { - $fileName = $this->fileHelper->normalizePath($fileName); - } - if ($className === null && $traitName !== null) { throw new ShouldNotHappenException(); } @@ -88,6 +85,10 @@ public function getResolvedPhpDoc( return ResolvedPhpDocBlock::createEmpty(); } + if ($fileName !== null) { + $fileName = $this->fileHelper->normalizePath($fileName); + } + $nameScopeKey = $this->getNameScopeKey($fileName, $className, $traitName, $functionName); $phpDocKey = md5(sprintf('%s-%s', $nameScopeKey, $docComment)); if (isset($this->resolvedPhpDocBlockCache[$phpDocKey])) { @@ -112,13 +113,13 @@ public function getResolvedPhpDoc( return ResolvedPhpDocBlock::createEmpty(); } - if ($this->inProcess[$fileName][$nameScopeKey] === false) { // PHPDoc has cyclic dependency + if ($this->inProcess[$fileName][$nameScopeKey] === true) { // PHPDoc has cyclic dependency return ResolvedPhpDocBlock::createEmpty(); } if (is_callable($this->inProcess[$fileName][$nameScopeKey])) { $resolveCallback = $this->inProcess[$fileName][$nameScopeKey]; - $this->inProcess[$fileName][$nameScopeKey] = false; + $this->inProcess[$fileName][$nameScopeKey] = true; $this->inProcess[$fileName][$nameScopeKey] = $resolveCallback(); } @@ -127,7 +128,7 @@ public function getResolvedPhpDoc( private function createResolvedPhpDocBlock(string $phpDocKey, NameScope $nameScope, string $phpDocString, ?string $fileName): ResolvedPhpDocBlock { - $phpDocNode = $this->resolvePhpDocStringToDocNode($phpDocString); + $phpDocNode = $this->phpDocStringResolver->resolve($phpDocString); if ($this->resolvedPhpDocBlockCacheCount >= 2048) { $this->resolvedPhpDocBlockCache = array_slice( $this->resolvedPhpDocBlockCache, @@ -158,17 +159,13 @@ private function createResolvedPhpDocBlock(string $phpDocKey, NameScope $nameSco new TemplateTypeMap($phpDocTemplateTypes), $templateTags, $this->phpDocNodeResolver, + $this->reflectionProviderProvider->getReflectionProvider(), ); $this->resolvedPhpDocBlockCacheCount++; return $this->resolvedPhpDocBlockCache[$phpDocKey]; } - private function resolvePhpDocStringToDocNode(string $phpDocString): PhpDocNode - { - return $this->phpDocStringResolver->resolve($phpDocString); - } - /** * @return NameScope[] */ @@ -198,14 +195,15 @@ private function getNameScopeMap(string $fileName): array */ private function createResolvedPhpDocMap(string $fileName): array { - $nameScopeMap = $this->createNameScopeMap($fileName, null, null, [], $fileName); + $phpDocNodeMap = $this->createPhpDocNodeMap($fileName, null, $fileName, [], $fileName); + $nameScopeMap = $this->createNameScopeMap($fileName, null, null, [], $fileName, $phpDocNodeMap); $resolvedNameScopeMap = []; try { $this->inProcess[$fileName] = $nameScopeMap; foreach ($nameScopeMap as $nameScopeKey => $resolveCallback) { - $this->inProcess[$fileName][$nameScopeKey] = false; + $this->inProcess[$fileName][$nameScopeKey] = true; $this->inProcess[$fileName][$nameScopeKey] = $data = $resolveCallback(); $resolvedNameScopeMap[$nameScopeKey] = $data; } @@ -219,6 +217,175 @@ private function createResolvedPhpDocMap(string $fileName): array /** * @param array $traitMethodAliases + * @return array + */ + private function createPhpDocNodeMap(string $fileName, ?string $lookForTrait, ?string $traitUseClass, array $traitMethodAliases, string $originalClassFileName): array + { + /** @var array $phpDocNodeMap */ + $phpDocNodeMap = []; + + /** @var string[] $classStack */ + $classStack = []; + if ($lookForTrait !== null && $traitUseClass !== null) { + $classStack[] = $traitUseClass; + } + $namespace = null; + + $traitFound = false; + + /** @var array $functionStack */ + $functionStack = []; + $this->processNodes( + $this->phpParser->parseFile($fileName), + function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, &$phpDocNodeMap, &$classStack, &$namespace, &$functionStack): ?int { + if ($node instanceof Node\Stmt\ClassLike) { + if ($traitFound && $fileName === $originalClassFileName) { + return self::SKIP_NODE; + } + + if ($lookForTrait !== null && !$traitFound) { + if (!$node instanceof Node\Stmt\Trait_) { + return self::SKIP_NODE; + } + if ((string) $node->namespacedName !== $lookForTrait) { + return self::SKIP_NODE; + } + + $traitFound = true; + $functionStack[] = null; + } else { + if ($node->name === null) { + if (!$node instanceof Node\Stmt\Class_) { + throw new ShouldNotHappenException(); + } + + $className = $this->anonymousClassNameHelper->getAnonymousClassName($node, $fileName); + } elseif ((bool) $node->getAttribute('anonymousClass', false)) { + $className = $node->name->name; + } else { + if ($traitFound) { + return self::SKIP_NODE; + } + $className = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\'); + } + $classStack[] = $className; + $functionStack[] = null; + } + } elseif ($node instanceof Node\Stmt\ClassMethod) { + if (array_key_exists($node->name->name, $traitMethodAliases)) { + $functionStack[] = $traitMethodAliases[$node->name->name]; + } else { + $functionStack[] = $node->name->name; + } + } elseif ($node instanceof Node\Stmt\Function_) { + $functionStack[] = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\'); + } + + $className = $classStack[count($classStack) - 1] ?? null; + $functionName = $functionStack[count($functionStack) - 1] ?? null; + + if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { + $docComment = GetLastDocComment::forNode($node); + if ($docComment !== null) { + $nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName); + $phpDocNodeMap[$nameScopeKey] = $this->phpDocStringResolver->resolve($docComment); + } + + return null; + } + + if ($node instanceof Node\Stmt\Namespace_) { + $namespace = $node->name !== null ? (string) $node->name : null; + } elseif ($node instanceof Node\Stmt\TraitUse) { + $traitMethodAliases = []; + foreach ($node->adaptations as $traitUseAdaptation) { + if (!$traitUseAdaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) { + continue; + } + + if ($traitUseAdaptation->newName === null) { + continue; + } + + $methodName = $traitUseAdaptation->method->toString(); + $newTraitName = $traitUseAdaptation->newName->toString(); + + if ($traitUseAdaptation->trait === null) { + foreach ($node->traits as $traitName) { + $traitMethodAliases[$traitName->toString()][$methodName] = $newTraitName; + } + continue; + } + + $traitMethodAliases[$traitUseAdaptation->trait->toString()][$methodName] = $newTraitName; + } + + foreach ($node->traits as $traitName) { + /** @var class-string $traitName */ + $traitName = (string) $traitName; + $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider(); + if (!$reflectionProvider->hasClass($traitName)) { + continue; + } + + $traitReflection = $reflectionProvider->getClass($traitName); + if (!$traitReflection->isTrait()) { + continue; + } + if ($traitReflection->getFileName() === null) { + continue; + } + if (!is_file($traitReflection->getFileName())) { + continue; + } + + $className = $classStack[count($classStack) - 1] ?? null; + if ($className === null) { + throw new ShouldNotHappenException(); + } + + $phpDocNodeMap = array_merge($phpDocNodeMap, $this->createPhpDocNodeMap( + $traitReflection->getFileName(), + $traitName, + $className, + $traitMethodAliases[$traitName] ?? [], + $originalClassFileName, + )); + } + } + + return null; + }, + static function (Node $node) use (&$namespace, &$functionStack, &$classStack): void { + if ($node instanceof Node\Stmt\ClassLike) { + if (count($classStack) === 0) { + throw new ShouldNotHappenException(); + } + array_pop($classStack); + + if (count($functionStack) === 0) { + throw new ShouldNotHappenException(); + } + + array_pop($functionStack); + } elseif ($node instanceof Node\Stmt\Namespace_) { + $namespace = null; + } elseif ($node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { + if (count($functionStack) === 0) { + throw new ShouldNotHappenException(); + } + + array_pop($functionStack); + } + }, + ); + + return $phpDocNodeMap; + } + + /** + * @param array $traitMethodAliases + * @param array $phpDocNodeMap * @return (callable(): NameScope)[] */ private function createNameScopeMap( @@ -227,6 +394,7 @@ private function createNameScopeMap( ?string $traitUseClass, array $traitMethodAliases, string $originalClassFileName, + array $phpDocNodeMap, ): array { /** @var (callable(): NameScope)[] $nameScopeMap */ @@ -254,7 +422,7 @@ private function createNameScopeMap( $constUses = []; $this->processNodes( $this->phpParser->parseFile($fileName), - function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, &$nameScopeMap, &$classStack, &$typeAliasStack, &$namespace, &$functionStack, &$uses, &$typeMapStack, &$constUses): ?int { + function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFound, $traitMethodAliases, $originalClassFileName, &$nameScopeMap, &$classStack, &$typeAliasStack, &$namespace, &$functionStack, &$uses, &$typeMapStack, &$constUses): ?int { if ($node instanceof Node\Stmt\ClassLike) { if ($traitFound && $fileName === $originalClassFileName) { return self::SKIP_NODE; @@ -269,6 +437,13 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA } $traitFound = true; + $traitNameScopeKey = $this->getNameScopeKey($originalClassFileName, $classStack[count($classStack) - 1] ?? null, $lookForTrait, null); + if (array_key_exists($traitNameScopeKey, $phpDocNodeMap)) { + $typeAliasStack[] = $this->getTypeAliasesMap($phpDocNodeMap[$traitNameScopeKey]); + } else { + $typeAliasStack[] = []; + } + $functionStack[] = null; } else { if ($node->name === null) { if (!$node instanceof Node\Stmt\Class_) { @@ -285,7 +460,12 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA $className = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\'); } $classStack[] = $className; - $typeAliasStack[] = $this->getTypeAliasesMap($node->getDocComment()); + $classNameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, null); + if (array_key_exists($classNameScopeKey, $phpDocNodeMap)) { + $typeAliasStack[] = $this->getTypeAliasesMap($phpDocNodeMap[$classNameScopeKey]); + } else { + $typeAliasStack[] = []; + } $functionStack[] = null; } } elseif ($node instanceof Node\Stmt\ClassMethod) { @@ -300,15 +480,16 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA $className = $classStack[count($classStack) - 1] ?? null; $functionName = $functionStack[count($functionStack) - 1] ?? null; + $nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName); if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { - $phpDocString = GetLastDocComment::forNode($node); - if ($phpDocString !== null) { - $typeMapStack[] = function () use ($namespace, $uses, $className, $functionName, $phpDocString, $typeMapStack, $constUses): TemplateTypeMap { - $phpDocNode = $this->resolvePhpDocStringToDocNode($phpDocString); + if (array_key_exists($nameScopeKey, $phpDocNodeMap)) { + $phpDocNode = $phpDocNodeMap[$nameScopeKey]; + $typeMapStack[] = function () use ($namespace, $uses, $className, $lookForTrait, $functionName, $phpDocNode, $typeMapStack, $typeAliasStack, $constUses): TemplateTypeMap { $typeMapCb = $typeMapStack[count($typeMapStack) - 1] ?? null; $currentTypeMap = $typeMapCb !== null ? $typeMapCb() : null; - $nameScope = new NameScope($namespace, $uses, $className, $functionName, $currentTypeMap, [], false, $constUses); + $typeAliasesMap = $typeAliasStack[count($typeAliasStack) - 1] ?? []; + $nameScope = new NameScope($namespace, $uses, $className, $functionName, $currentTypeMap, $typeAliasesMap, false, $constUses, $lookForTrait); $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope); $templateTypeScope = $nameScope->getTemplateTypeScope(); if ($templateTypeScope === null) { @@ -330,7 +511,6 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA $typeMapCb = $typeMapStack[count($typeMapStack) - 1] ?? null; $typeAliasesMap = $typeAliasStack[count($typeAliasStack) - 1] ?? []; - $nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName); if ( $node instanceof Node\Stmt && !$node instanceof Node\Stmt\Namespace_ @@ -354,12 +534,12 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA $typeAliasesMap, false, $constUses, + $lookForTrait, ); } if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { - $phpDocString = GetLastDocComment::forNode($node); - if ($phpDocString !== null) { + if (array_key_exists($nameScopeKey, $phpDocNodeMap)) { return self::POP_TYPE_MAP_STACK; } @@ -394,15 +574,21 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA continue; } - if ($traitUseAdaptation->trait === null) { + if ($traitUseAdaptation->newName === null) { continue; } - if ($traitUseAdaptation->newName === null) { + $methodName = $traitUseAdaptation->method->toString(); + $newTraitName = $traitUseAdaptation->newName->toString(); + + if ($traitUseAdaptation->trait === null) { + foreach ($node->traits as $traitName) { + $traitMethodAliases[$traitName->toString()][$methodName] = $newTraitName; + } continue; } - $traitMethodAliases[$traitUseAdaptation->trait->toString()][$traitUseAdaptation->method->toString()] = $traitUseAdaptation->newName->toString(); + $traitMethodAliases[$traitUseAdaptation->trait->toString()][$methodName] = $newTraitName; } $useDocComment = null; @@ -440,6 +626,7 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA $className, $traitMethodAliases[$traitName] ?? [], $originalClassFileName, + $phpDocNodeMap, ); $finalTraitPhpDocMap = []; foreach ($traitPhpDocMap as $nameScopeTraitKey => $callback) { @@ -482,7 +669,7 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA $transformedTraitTypeMap = $traitReflection->typeMapFromList($useType->getTypes()); - return $original->withTemplateTypeMap($traitTemplateTypeMap->map(static fn (string $name, Type $type): Type => TemplateTypeHelper::resolveTemplateTypes($type, $transformedTraitTypeMap))); + return $original->withTemplateTypeMap($traitTemplateTypeMap->map(static fn (string $name, Type $type): Type => TemplateTypeHelper::resolveTemplateTypes($type, $transformedTraitTypeMap, TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createStatic()))); }; } $nameScopeMap = array_merge($nameScopeMap, $finalTraitPhpDocMap); @@ -491,8 +678,8 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA return null; }, - static function (Node $node, $callbackResult) use ($lookForTrait, &$namespace, &$functionStack, &$classStack, &$typeAliasStack, &$uses, &$typeMapStack, &$constUses): void { - if ($node instanceof Node\Stmt\ClassLike && $lookForTrait === null) { + static function (Node $node, $callbackResult) use (&$namespace, &$functionStack, &$classStack, &$typeAliasStack, &$uses, &$typeMapStack, &$constUses): void { + if ($node instanceof Node\Stmt\ClassLike) { if (count($classStack) === 0) { throw new ShouldNotHappenException(); } @@ -541,13 +728,8 @@ static function (Node $node, $callbackResult) use ($lookForTrait, &$namespace, & /** * @return array */ - private function getTypeAliasesMap(?Doc $docComment): array + private function getTypeAliasesMap(PhpDocNode $phpDocNode): array { - if ($docComment === null) { - return []; - } - - $phpDocNode = $this->phpDocStringResolver->resolve($docComment->getText()); $nameScope = new NameScope(null, []); $aliasesMap = []; @@ -563,7 +745,7 @@ private function getTypeAliasesMap(?Doc $docComment): array } /** - * @param Node[]|Node|scalar $node + * @param Node[]|Node|scalar|null $node * @param Closure(Node $node): mixed $nodeCallback * @param Closure(Node $node, mixed $callbackResult): void $endNodeCallback */ @@ -597,7 +779,7 @@ private function getNameScopeKey( return md5(sprintf('%s', $file ?? 'no-file')); } - if ($class !== null && strpos($class, 'class@anonymous') !== false) { + if ($class !== null && str_contains($class, 'class@anonymous')) { throw new ShouldNotHappenException('Wrong anonymous class name, FilTypeMapper should be called with ClassReflection::getName().'); } diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index 2ace3329d1..c82a50e4e7 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -2,10 +2,14 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonCallableTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; @@ -21,6 +25,7 @@ class FloatType implements Type { + use NonArrayTypeTrait; use NonCallableTypeTrait; use NonIterableTypeTrait; use NonObjectTypeTrait; @@ -44,17 +49,37 @@ public function getReferencedClasses(): array return []; } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic { - if ($type instanceof self || $type instanceof IntegerType) { - return TrinaryLogic::createYes(); + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + { + if ($type instanceof self || $type->isInteger()->yes()) { + return AcceptsResult::createYes(); } if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -109,15 +134,62 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], + [], + TrinaryLogic::createYes(), ); } - public function isArray(): TrinaryLogic + public function toArrayKey(): Type + { + return new IntegerType(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function isOversizedArray(): TrinaryLogic + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isInteger(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -147,11 +219,61 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function traverse(callable $cb): Type { return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function exponentiate(Type $exponent): Type + { + return ExponentiateHelper::exponentiate($this, $exponent); + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('float'); + } + + public function getFiniteTypes(): array + { + return []; + } + /** * @param mixed[] $properties */ diff --git a/src/Type/FunctionTypeSpecifyingExtension.php b/src/Type/FunctionTypeSpecifyingExtension.php index 510450ae98..86e2c75a39 100644 --- a/src/Type/FunctionTypeSpecifyingExtension.php +++ b/src/Type/FunctionTypeSpecifyingExtension.php @@ -8,7 +8,23 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; -/** @api */ +/** + * This is the interface type-specifying extensions implement for functions. + * + * To register it in the configuration file use the `phpstan.typeSpecifier.functionTypeSpecifyingExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.typeSpecifier.functionTypeSpecifyingExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/type-specifying-extensions + * + * @api + */ interface FunctionTypeSpecifyingExtension { diff --git a/src/Type/Generic/GenericClassStringType.php b/src/Type/Generic/GenericClassStringType.php index 0d1a8415ad..fd6ecd5f03 100644 --- a/src/Type/Generic/GenericClassStringType.php +++ b/src/Type/Generic/GenericClassStringType.php @@ -2,7 +2,12 @@ namespace PHPStan\Type\Generic; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\ClassStringType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantStringType; @@ -15,9 +20,9 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function count; use function sprintf; /** @api */ @@ -40,20 +45,30 @@ public function getGenericType(): Type return $this->type; } + public function getClassStringObjectType(): Type + { + return $this->getGenericType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this->getClassStringObjectType(); + } + public function describe(VerbosityLevel $level): string { return sprintf('%s<%s>', parent::describe($level), $this->type->describe($level)); } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } if ($type instanceof ConstantStringType) { - if (!$type->isClassString()) { - return TrinaryLogic::createNo(); + if (!$type->isClassStringType()->yes()) { + return AcceptsResult::createNo(); } $objectType = new ObjectType($type->getValue()); @@ -62,12 +77,12 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic } elseif ($type instanceof ClassStringType) { $objectType = new ObjectWithoutClassType(); } elseif ($type instanceof StringType) { - return TrinaryLogic::createMaybe(); + return AcceptsResult::createMaybe(); } else { - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } - return $this->type->accepts($objectType, $strictTypes); + return $this->type->acceptsWithReason($objectType, $strictTypes); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -98,7 +113,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic $isSuperType = $genericType->isSuperTypeOf($objectType); } - if (!$type->isClassString()) { + if (!$type->isClassStringType()->yes()) { $isSuperType = $isSuperType->and(TrinaryLogic::createMaybe()); } @@ -122,6 +137,16 @@ public function traverse(callable $cb): Type return new self($newType); } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + $newType = $cb($this->type, $right->getClassStringObjectType()); + if ($newType === $this->type) { + return $this; + } + + return new self($newType); + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) { @@ -132,7 +157,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap $typeToInfer = new ObjectType($receivedType->getValue()); } elseif ($receivedType instanceof self) { $typeToInfer = $receivedType->type; - } elseif ($receivedType instanceof ClassStringType) { + } elseif ($receivedType->isClassStringType()->yes()) { $typeToInfer = $this->type; if ($typeToInfer instanceof TemplateType) { $typeToInfer = $typeToInfer->getBound(); @@ -178,18 +203,25 @@ public static function __set_state(array $properties): Type return new self($properties['type']); } + public function toPhpDocNode(): TypeNode + { + return new GenericTypeNode( + new IdentifierTypeNode('class-string'), + [ + $this->type->toPhpDocNode(), + ], + ); + } + public function tryRemove(Type $typeToRemove): ?Type { - if ($typeToRemove instanceof ConstantStringType && $typeToRemove->isClassString()) { + if ($typeToRemove instanceof ConstantStringType && $typeToRemove->isClassStringType()->yes()) { $generic = $this->getGenericType(); - if ($generic instanceof TypeWithClassName) { - $classReflection = $generic->getClassReflection(); - if ( - $classReflection !== null - && $classReflection->isFinal() - && $generic->getClassName() === $typeToRemove->getValue() - ) { + $genericObjectClassNames = $generic->getObjectClassNames(); + if (count($genericObjectClassNames) === 1) { + $classReflection = ReflectionProviderStaticAccessor::getInstance()->getClass($genericObjectClassNames[0]); + if ($classReflection->isFinal() && $genericObjectClassNames[0] === $typeToRemove->getValue()) { return new NeverType(); } } diff --git a/src/Type/Generic/GenericObjectType.php b/src/Type/Generic/GenericObjectType.php index cdd7795cdc..775134aab6 100644 --- a/src/Type/Generic/GenericObjectType.php +++ b/src/Type/Generic/GenericObjectType.php @@ -2,15 +2,19 @@ namespace PHPStan\Type\Generic; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; use PHPStan\Type\ErrorType; use PHPStan\Type\IntersectionType; @@ -31,12 +35,14 @@ class GenericObjectType extends ObjectType /** * @api * @param array $types + * @param array $variances */ public function __construct( string $mainType, private array $types, ?Type $subtractedType = null, private ?ClassReflection $classReflection = null, + private array $variances = [], ) { parent::__construct($mainType, $subtractedType, $classReflection); @@ -47,7 +53,11 @@ public function describe(VerbosityLevel $level): string return sprintf( '%s<%s>', parent::describe($level), - implode(', ', array_map(static fn (Type $type): string => $type->describe($level), $this->types)), + implode(', ', array_map( + static fn (Type $type, ?TemplateTypeVariance $variance = null): string => TypeProjectionHelper::describe($type, $variance, $level), + $this->types, + $this->variances, + )), ); } @@ -70,6 +80,12 @@ public function equals(Type $type): bool if (!$genericType->equals($otherGenericType)) { return false; } + + $variance = $this->variances[$i] ?? TemplateTypeVariance::createInvariant(); + $otherVariance = $type->variances[$i] ?? TemplateTypeVariance::createInvariant(); + if (!$variance->equals($otherVariance)) { + return false; + } } return true; @@ -96,10 +112,21 @@ public function getTypes(): array return $this->types; } + /** @return array */ + public function getVariances(): array + { + return $this->variances; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } return $this->isSuperTypeOfInternal($type, true); @@ -111,12 +138,12 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return $type->isSubTypeOf($this); } - return $this->isSuperTypeOfInternal($type, false); + return $this->isSuperTypeOfInternal($type, false)->result; } - private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): TrinaryLogic + private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): AcceptsResult { - $nakedSuperTypeOf = parent::isSuperTypeOf($type); + $nakedSuperTypeOf = new AcceptsResult(parent::isSuperTypeOf($type), []); if ($nakedSuperTypeOf->no()) { return $nakedSuperTypeOf; } @@ -134,11 +161,11 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): Trinar return $nakedSuperTypeOf; } - return $nakedSuperTypeOf->and(TrinaryLogic::createMaybe()); + return $nakedSuperTypeOf->and(AcceptsResult::createMaybe()); } if (count($this->types) !== count($ancestor->types)) { - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } $classReflection = $this->getClassReflection(); @@ -162,14 +189,27 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): Trinar throw new ShouldNotHappenException(); } - $results[] = $templateType->isValidVariance($this->types[$i], $ancestor->types[$i]); + $thisVariance = $this->variances[$i] ?? TemplateTypeVariance::createInvariant(); + $ancestorVariance = $ancestor->variances[$i] ?? TemplateTypeVariance::createInvariant(); + if (!$thisVariance->invariant()) { + $results[] = $thisVariance->isValidVarianceWithReason($templateType, $this->types[$i], $ancestor->types[$i]); + } else { + $results[] = $templateType->isValidVarianceWithReason($this->types[$i], $ancestor->types[$i]); + } + + $results[] = AcceptsResult::createFromBoolean($thisVariance->validPosition($ancestorVariance)); } if (count($results) === 0) { return $nakedSuperTypeOf; } - return $nakedSuperTypeOf->and(...$results); + $result = AcceptsResult::createYes(); + foreach ($results as $innerResult) { + $result = $result->and($innerResult); + } + + return $result; } public function getClassReflection(): ?ClassReflection @@ -183,7 +223,9 @@ public function getClassReflection(): ?ClassReflection return null; } - return $this->classReflection = $reflectionProvider->getClass($this->getClassName())->withTypes($this->types); + return $this->classReflection = $reflectionProvider->getClass($this->getClassName()) + ->withTypes($this->types) + ->withVariances($this->variances); } public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection @@ -198,7 +240,7 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember return $prototype->doNotResolveTemplateTypeMapToBounds(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -253,11 +295,12 @@ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVarianc $references = []; foreach ($this->types as $i => $type) { - $variance = $positionVariance->compose( - isset($typeList[$i]) && $typeList[$i] instanceof TemplateType - ? $typeList[$i]->getVariance() - : TemplateTypeVariance::createInvariant(), - ); + $effectiveVariance = $this->variances[$i] ?? TemplateTypeVariance::createInvariant(); + if ($effectiveVariance->invariant() && isset($typeList[$i]) && $typeList[$i] instanceof TemplateType) { + $effectiveVariance = $typeList[$i]->getVariance(); + } + + $variance = $positionVariance->compose($effectiveVariance); foreach ($type->getReferencedTemplateTypes($variance) as $reference) { $references[] = $reference; } @@ -283,7 +326,42 @@ public function traverse(callable $cb): Type } if ($subtractedType !== $this->getSubtractedType() || $typesChanged) { - return $this->recreate($this->getClassName(), $types, $subtractedType); + return $this->recreate($this->getClassName(), $types, $subtractedType, $this->variances); + } + + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof TypeWithClassName) { + return $this; + } + + $ancestor = $right->getAncestorWithClassName($this->getClassName()); + if (!$ancestor instanceof self) { + return $this; + } + + if (count($this->types) !== count($ancestor->types)) { + return $this; + } + + $typesChanged = false; + $types = []; + foreach ($this->types as $i => $leftType) { + $rightType = $ancestor->types[$i]; + $newType = $cb($leftType, $rightType); + $types[] = $newType; + if ($newType === $leftType) { + continue; + } + + $typesChanged = true; + } + + if ($typesChanged) { + return $this->recreate($this->getClassName(), $types, null); } return $this; @@ -291,19 +369,33 @@ public function traverse(callable $cb): Type /** * @param Type[] $types + * @param TemplateTypeVariance[] $variances */ - protected function recreate(string $className, array $types, ?Type $subtractedType): self + protected function recreate(string $className, array $types, ?Type $subtractedType, array $variances = []): self { return new self( $className, $types, $subtractedType, + null, + $variances, ); } public function changeSubtractedType(?Type $subtractedType): Type { - return new self($this->getClassName(), $this->types, $subtractedType); + return new self($this->getClassName(), $this->types, $subtractedType, null, $this->variances); + } + + public function toPhpDocNode(): TypeNode + { + /** @var IdentifierTypeNode $parent */ + $parent = parent::toPhpDocNode(); + return new GenericTypeNode( + $parent, + array_map(static fn (Type $type) => $type->toPhpDocNode(), $this->types), + array_map(static fn (TemplateTypeVariance $variance) => $variance->toPhpDocNodeVariance(), $this->variances), + ); } /** @@ -315,6 +407,8 @@ public static function __set_state(array $properties): Type $properties['className'], $properties['types'], $properties['subtractedType'] ?? null, + null, + $properties['variances'] ?? [], ); } diff --git a/src/Type/Generic/TemplateConstantArrayType.php b/src/Type/Generic/TemplateConstantArrayType.php index 637de876e1..8bb8aa696d 100644 --- a/src/Type/Generic/TemplateConstantArrayType.php +++ b/src/Type/Generic/TemplateConstantArrayType.php @@ -21,7 +21,7 @@ public function __construct( ConstantArrayType $bound, ) { - parent::__construct($bound->getKeyTypes(), $bound->getValueTypes(), $bound->getNextAutoIndexes(), $bound->getOptionalKeys()); + parent::__construct($bound->getKeyTypes(), $bound->getValueTypes(), $bound->getNextAutoIndexes(), $bound->getOptionalKeys(), $bound->isList()); $this->scope = $scope; $this->strategy = $templateTypeStrategy; $this->variance = $templateTypeVariance; diff --git a/src/Type/Generic/TemplateGenericObjectType.php b/src/Type/Generic/TemplateGenericObjectType.php index be8861ab06..485781aaca 100644 --- a/src/Type/Generic/TemplateGenericObjectType.php +++ b/src/Type/Generic/TemplateGenericObjectType.php @@ -21,7 +21,7 @@ public function __construct( GenericObjectType $bound, ) { - parent::__construct($bound->getClassName(), $bound->getTypes()); + parent::__construct($bound->getClassName(), $bound->getTypes(), null, null, $bound->getVariances()); $this->scope = $scope; $this->strategy = $templateTypeStrategy; @@ -30,7 +30,7 @@ public function __construct( $this->bound = $bound; } - protected function recreate(string $className, array $types, ?Type $subtractedType): GenericObjectType + protected function recreate(string $className, array $types, ?Type $subtractedType, array $variances = []): GenericObjectType { return new self( $this->scope, diff --git a/src/Type/Generic/TemplateMixedType.php b/src/Type/Generic/TemplateMixedType.php index 0afbe59fe5..11af221db4 100644 --- a/src/Type/Generic/TemplateMixedType.php +++ b/src/Type/Generic/TemplateMixedType.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Generic; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\MixedType; use PHPStan\Type\StrictMixedType; use PHPStan\Type\Type; @@ -38,11 +39,16 @@ public function isSuperTypeOfMixed(MixedType $type): TrinaryLogic public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - $isSuperType = $this->isSuperTypeOf($acceptingType); + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + $isSuperType = new AcceptsResult($this->isSuperTypeOf($acceptingType), []); if ($isSuperType->no()) { return $isSuperType; } - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } public function toStrictMixedType(): TemplateStrictMixedType diff --git a/src/Type/Generic/TemplateObjectShapeType.php b/src/Type/Generic/TemplateObjectShapeType.php new file mode 100644 index 0000000000..7fe78cbdbd --- /dev/null +++ b/src/Type/Generic/TemplateObjectShapeType.php @@ -0,0 +1,37 @@ + */ + use TemplateTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + ObjectShapeType $bound, + ) + { + parent::__construct($bound->getProperties(), $bound->getOptionalProperties()); + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + } + + protected function shouldGeneralizeInferredType(): bool + { + return false; + } + +} diff --git a/src/Type/Generic/TemplateStrictMixedType.php b/src/Type/Generic/TemplateStrictMixedType.php index 76ce5f813e..8949142920 100644 --- a/src/Type/Generic/TemplateStrictMixedType.php +++ b/src/Type/Generic/TemplateStrictMixedType.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Generic; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\MixedType; use PHPStan\Type\StrictMixedType; use PHPStan\Type\Type; @@ -36,7 +37,12 @@ public function isSuperTypeOfMixed(MixedType $type): TrinaryLogic public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - return $this->isSubTypeOf($acceptingType); + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return new AcceptsResult($this->isSubTypeOf($acceptingType), []); } } diff --git a/src/Type/Generic/TemplateType.php b/src/Type/Generic/TemplateType.php index 94ec17e863..01287c374a 100644 --- a/src/Type/Generic/TemplateType.php +++ b/src/Type/Generic/TemplateType.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Generic; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; use PHPStan\Type\Type; @@ -22,6 +23,8 @@ public function isArgument(): bool; public function isValidVariance(Type $a, Type $b): TrinaryLogic; + public function isValidVarianceWithReason(Type $a, Type $b): AcceptsResult; + public function getVariance(): TemplateTypeVariance; public function getStrategy(): TemplateTypeStrategy; diff --git a/src/Type/Generic/TemplateTypeArgumentStrategy.php b/src/Type/Generic/TemplateTypeArgumentStrategy.php index 996f826653..414ffc12aa 100644 --- a/src/Type/Generic/TemplateTypeArgumentStrategy.php +++ b/src/Type/Generic/TemplateTypeArgumentStrategy.php @@ -2,9 +2,12 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; use PHPStan\Type\Type; +use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function sprintf; /** * Template type strategy suitable for return type acceptance contexts @@ -12,13 +15,24 @@ class TemplateTypeArgumentStrategy implements TemplateTypeStrategy { - public function accepts(TemplateType $left, Type $right, bool $strictTypes): TrinaryLogic + public function accepts(TemplateType $left, Type $right, bool $strictTypes): AcceptsResult { if ($right instanceof CompoundType) { - $accepts = $right->isAcceptedBy($left, $strictTypes); + $accepts = $right->isAcceptedWithReasonBy($left, $strictTypes); } else { - $accepts = $left->getBound()->accepts($right, $strictTypes) - ->and(TrinaryLogic::createMaybe()); + $accepts = $left->getBound()->acceptsWithReason($right, $strictTypes) + ->and(AcceptsResult::createMaybe()); + if ($accepts->maybe()) { + $verbosity = VerbosityLevel::getRecommendedLevelByType($left, $right); + + return new AcceptsResult($accepts->result, array_merge($accepts->reasons, [ + sprintf( + 'Type %s is not always the same as %s. It breaks the contract for some argument types, typically subtypes.', + $right->describe($verbosity), + $left->getName(), + ), + ])); + } } return $accepts; diff --git a/src/Type/Generic/TemplateTypeFactory.php b/src/Type/Generic/TemplateTypeFactory.php index 26d6e067bb..6d71e1dea5 100644 --- a/src/Type/Generic/TemplateTypeFactory.php +++ b/src/Type/Generic/TemplateTypeFactory.php @@ -14,6 +14,7 @@ use PHPStan\Type\IntersectionType; use PHPStan\Type\KeyOfType; use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectShapeType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; @@ -53,6 +54,10 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou return new TemplateConstantArrayType($scope, $strategy, $variance, $name, $bound); } + if ($bound instanceof ObjectShapeType && ($boundClass === ObjectShapeType::class || $bound instanceof TemplateType)) { + return new TemplateObjectShapeType($scope, $strategy, $variance, $name, $bound); + } + if ($bound instanceof StringType && ($boundClass === StringType::class || $bound instanceof TemplateType)) { return new TemplateStringType($scope, $strategy, $variance, $name, $bound); } diff --git a/src/Type/Generic/TemplateTypeHelper.php b/src/Type/Generic/TemplateTypeHelper.php index f8e72514cf..166464a884 100644 --- a/src/Type/Generic/TemplateTypeHelper.php +++ b/src/Type/Generic/TemplateTypeHelper.php @@ -2,9 +2,13 @@ namespace PHPStan\Type\Generic; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Type\ErrorType; +use PHPStan\Type\GeneralizePrecision; +use PHPStan\Type\NonAcceptingNeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; +use PHPStan\Type\VerbosityLevel; class TemplateTypeHelper { @@ -12,11 +16,30 @@ class TemplateTypeHelper /** * Replaces template types with standin types */ - public static function resolveTemplateTypes(Type $type, TemplateTypeMap $standins, bool $keepErrorTypes = false): Type + public static function resolveTemplateTypes( + Type $type, + TemplateTypeMap $standins, + TemplateTypeVarianceMap $callSiteVariances, + TemplateTypeVariance $positionVariance, + bool $keepErrorTypes = false, + ): Type { - return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($standins, $keepErrorTypes): Type { + $references = $type->getReferencedTemplateTypes($positionVariance); + + return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($standins, $references, $callSiteVariances, $keepErrorTypes): Type { if ($type instanceof TemplateType && !$type->isArgument()) { $newType = $standins->getType($type->getName()); + + $variance = TemplateTypeVariance::createInvariant(); + foreach ($references as $reference) { + // this uses identity to distinguish between different occurrences of the same template type + // see https://github.com/phpstan/phpstan-src/pull/2485#discussion_r1328555397 for details + if ($reference->getType() === $type) { + $variance = $reference->getPositionVariance(); + break; + } + } + if ($newType === null) { return $traverse($type); } @@ -25,6 +48,19 @@ public static function resolveTemplateTypes(Type $type, TemplateTypeMap $standin return $traverse($type->getBound()); } + $callSiteVariance = $callSiteVariances->getVariance($type->getName()); + if ($callSiteVariance === null || $callSiteVariance->invariant()) { + return $newType; + } + + if (!$callSiteVariance->covariant() && $variance->covariant()) { + return $traverse($type->getBound()); + } + + if (!$callSiteVariance->contravariant() && $variance->contravariant()) { + return new NonAcceptingNeverType(); + } + return $newType; } @@ -50,8 +86,35 @@ public static function resolveToBounds(Type $type): Type */ public static function toArgument(Type $type): Type { + $ownedTemplates = []; + /** @var T */ - return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + return TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$ownedTemplates): Type { + if ($type instanceof ParametersAcceptor) { + $templateTypeMap = $type->getTemplateTypeMap(); + + foreach ($type->getParameters() as $parameter) { + $parameterType = $parameter->getType(); + if (!($parameterType instanceof TemplateType) || !$templateTypeMap->hasType($parameterType->getName())) { + continue; + } + + $ownedTemplates[] = $parameterType; + } + + $returnType = $type->getReturnType(); + + if ($returnType instanceof TemplateType && $templateTypeMap->hasType($returnType->getName())) { + $ownedTemplates[] = $returnType; + } + } + + foreach ($ownedTemplates as $ownedTemplate) { + if ($ownedTemplate === $type) { + return $traverse($type); + } + } + if ($type instanceof TemplateType) { return $traverse($type->toArgument()); } @@ -60,4 +123,18 @@ public static function toArgument(Type $type): Type }); } + public static function generalizeInferredTemplateType(TemplateType $templateType, Type $type): Type + { + if (!$templateType->getVariance()->covariant()) { + $isArrayKey = $templateType->getBound()->describe(VerbosityLevel::precise()) === '(int|string)'; + if ($type->isScalar()->yes() && $isArrayKey) { + $type = $type->generalize(GeneralizePrecision::templateArgument()); + } elseif ($type->isConstantValue()->yes() && (!$templateType->getBound()->isScalar()->yes() || $isArrayKey)) { + $type = $type->generalize(GeneralizePrecision::templateArgument()); + } + } + + return $type; + } + } diff --git a/src/Type/Generic/TemplateTypeParameterStrategy.php b/src/Type/Generic/TemplateTypeParameterStrategy.php index 5d9a8256d6..5ad7793665 100644 --- a/src/Type/Generic/TemplateTypeParameterStrategy.php +++ b/src/Type/Generic/TemplateTypeParameterStrategy.php @@ -2,7 +2,7 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; use PHPStan\Type\Type; @@ -12,13 +12,13 @@ class TemplateTypeParameterStrategy implements TemplateTypeStrategy { - public function accepts(TemplateType $left, Type $right, bool $strictTypes): TrinaryLogic + public function accepts(TemplateType $left, Type $right, bool $strictTypes): AcceptsResult { if ($right instanceof CompoundType) { - return $right->isAcceptedBy($left, $strictTypes); + return $right->isAcceptedWithReasonBy($left, $strictTypes); } - return $left->getBound()->accepts($right, $strictTypes); + return $left->getBound()->acceptsWithReason($right, $strictTypes); } public function isArgument(): bool diff --git a/src/Type/Generic/TemplateTypeScope.php b/src/Type/Generic/TemplateTypeScope.php index f9a7625720..e40eab90d3 100644 --- a/src/Type/Generic/TemplateTypeScope.php +++ b/src/Type/Generic/TemplateTypeScope.php @@ -7,6 +7,11 @@ class TemplateTypeScope { + public static function createWithAnonymousFunction(): self + { + return new self(null, null); + } + public static function createWithFunction(string $functionName): self { return new self(null, $functionName); @@ -48,6 +53,10 @@ public function equals(self $other): bool /** @api */ public function describe(): string { + if ($this->className === null && $this->functionName === null) { + return 'anonymous function'; + } + if ($this->className === null) { return sprintf('function %s()', $this->functionName); } diff --git a/src/Type/Generic/TemplateTypeStrategy.php b/src/Type/Generic/TemplateTypeStrategy.php index d90dc732e1..843710ae7c 100644 --- a/src/Type/Generic/TemplateTypeStrategy.php +++ b/src/Type/Generic/TemplateTypeStrategy.php @@ -2,13 +2,13 @@ namespace PHPStan\Type\Generic; -use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\Type; interface TemplateTypeStrategy { - public function accepts(TemplateType $left, Type $right, bool $strictTypes): TrinaryLogic; + public function accepts(TemplateType $left, Type $right, bool $strictTypes): AcceptsResult; public function isArgument(): bool; diff --git a/src/Type/Generic/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index acd3859627..803ec31348 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -2,7 +2,11 @@ namespace PHPStan\Type\Generic; +use PHPStan\DependencyInjection\BleedingEdgeToggle; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; @@ -51,10 +55,10 @@ public function getBound(): Type public function describe(VerbosityLevel $level): string { $basicDescription = function () use ($level): string { - // @phpstan-ignore-next-line + // @phpstan-ignore booleanAnd.alwaysFalse, instanceof.alwaysFalse, booleanAnd.alwaysFalse, instanceof.alwaysFalse, instanceof.alwaysTrue if ($this->bound instanceof MixedType && $this->bound->getSubtractedType() === null && !$this->bound instanceof TemplateMixedType) { $boundDescription = ''; - } else { // @phpstan-ignore-line + } else { $boundDescription = sprintf(' of %s', $this->bound->describe($level)); } return sprintf( @@ -89,7 +93,12 @@ public function toArgument(): TemplateType public function isValidVariance(Type $a, Type $b): TrinaryLogic { - return $this->variance->isValidVariance($a, $b); + return $this->isValidVarianceWithReason($a, $b)->result; + } + + public function isValidVarianceWithReason(Type $a, Type $b): AcceptsResult + { + return $this->variance->isValidVarianceWithReason($this, $a, $b); } public function subtract(Type $typeToRemove): Type @@ -107,7 +116,7 @@ public function subtract(Type $typeToRemove): Type public function getTypeWithoutSubtractedType(): Type { $bound = $this->getBound(); - if (!$bound instanceof SubtractableType) { // @phpstan-ignore-line + if (!$bound instanceof SubtractableType) { // @phpstan-ignore instanceof.alwaysTrue return $this; } @@ -123,7 +132,7 @@ public function getTypeWithoutSubtractedType(): Type public function changeSubtractedType(?Type $subtractedType): Type { $bound = $this->getBound(); - if (!$bound instanceof SubtractableType) { // @phpstan-ignore-line + if (!$bound instanceof SubtractableType) { // @phpstan-ignore instanceof.alwaysTrue return $this; } @@ -139,7 +148,7 @@ public function changeSubtractedType(?Type $subtractedType): Type public function getSubtractedType(): ?Type { $bound = $this->getBound(); - if (!$bound instanceof SubtractableType) { // @phpstan-ignore-line + if (!$bound instanceof SubtractableType) { // @phpstan-ignore instanceof.alwaysTrue return null; } @@ -156,7 +165,12 @@ public function equals(Type $type): bool public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - /** @var Type $bound */ + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + /** @var TBound $bound */ $bound = $this->getBound(); if ( !$acceptingType instanceof $bound @@ -164,22 +178,27 @@ public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLog && !$acceptingType instanceof TemplateType && ($acceptingType instanceof UnionType || $acceptingType instanceof IntersectionType) ) { - return $acceptingType->accepts($this, $strictTypes); + return $acceptingType->acceptsWithReason($this, $strictTypes); } if (!$acceptingType instanceof TemplateType) { - return $acceptingType->accepts($this->getBound(), $strictTypes); + return $acceptingType->acceptsWithReason($this->getBound(), $strictTypes); } if ($this->getScope()->equals($acceptingType->getScope()) && $this->getName() === $acceptingType->getName()) { - return $acceptingType->getBound()->accepts($this->getBound(), $strictTypes); + return $acceptingType->getBound()->acceptsWithReason($this->getBound(), $strictTypes); } - return $acceptingType->getBound()->accepts($this->getBound(), $strictTypes) - ->and(TrinaryLogic::createMaybe()); + return $acceptingType->getBound()->acceptsWithReason($this->getBound(), $strictTypes) + ->and(new AcceptsResult(TrinaryLogic::createMaybe(), [])); } public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { return $this->strategy->accepts($this, $type, $strictTypes); } @@ -200,7 +219,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic public function isSubTypeOf(Type $type): TrinaryLogic { - /** @var Type $bound */ + /** @var TBound $bound */ $bound = $this->getBound(); if ( !$type instanceof $bound @@ -223,6 +242,11 @@ public function isSubTypeOf(Type $type): TrinaryLogic ->and(TrinaryLogic::createMaybe()); } + public function toArrayKey(): Type + { + return $this; + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { if ( @@ -235,14 +259,20 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap } $map = $this->getBound()->inferTemplateTypes($receivedType); - $resolvedBound = TypeUtils::resolveLateResolvableTypes(TemplateTypeHelper::resolveTemplateTypes($this->getBound(), $map)); + $resolvedBound = TypeUtils::resolveLateResolvableTypes(TemplateTypeHelper::resolveTemplateTypes( + $this->getBound(), + $map, + TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createStatic(), + )); if ($resolvedBound->isSuperTypeOf($receivedType)->yes()) { - if ($this->shouldGeneralizeInferredType()) { + if (!BleedingEdgeToggle::isBleedingEdge() && $this->shouldGeneralizeInferredType()) { $generalizedType = $receivedType->generalize(GeneralizePrecision::templateArgument()); if ($resolvedBound->isSuperTypeOf($generalizedType)->yes()) { $receivedType = $generalizedType; } } + return (new TemplateTypeMap([ $this->name => $receivedType, ]))->union($map); @@ -287,13 +317,45 @@ public function traverse(callable $cb): Type ); } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof TemplateType) { + return $this; + } + + $bound = $cb($this->getBound(), $right->getBound()); + if ($this->getBound() === $bound) { + return $this; + } + + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $bound, + $this->getVariance(), + $this->getStrategy(), + ); + } + public function tryRemove(Type $typeToRemove): ?Type { - if ($this->getBound()->isSuperTypeOf($typeToRemove)->yes()) { - return $this->subtract($typeToRemove); + $bound = TypeCombinator::remove($this->getBound(), $typeToRemove); + if ($this->getBound() === $bound) { + return null; } - return null; + return TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $bound, + $this->getVariance(), + $this->getStrategy(), + ); + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode($this->name); } /** diff --git a/src/Type/Generic/TemplateTypeVariance.php b/src/Type/Generic/TemplateTypeVariance.php index edd59b7c24..a3ab7a04bd 100644 --- a/src/Type/Generic/TemplateTypeVariance.php +++ b/src/Type/Generic/TemplateTypeVariance.php @@ -2,12 +2,15 @@ namespace PHPStan\Type\Generic; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; +use function sprintf; /** @api */ class TemplateTypeVariance @@ -17,10 +20,13 @@ class TemplateTypeVariance private const COVARIANT = 2; private const CONTRAVARIANT = 3; private const STATIC = 4; + private const BIVARIANT = 5; /** @var self[] */ private static array $registry; + private static bool $invarianceCompositionEnabled = false; + private function __construct(private int $value) { } @@ -51,6 +57,11 @@ public static function createStatic(): self return self::create(self::STATIC); } + public static function createBivariant(): self + { + return self::create(self::BIVARIANT); + } + public function invariant(): bool { return $this->value === self::INVARIANT; @@ -71,6 +82,11 @@ public function static(): bool return $this->value === self::STATIC; } + public function bivariant(): bool + { + return $this->value === self::BIVARIANT; + } + public function compose(self $other): self { if ($this->contravariant()) { @@ -80,58 +96,97 @@ public function compose(self $other): self if ($other->covariant()) { return self::createContravariant(); } + if ($other->bivariant()) { + return self::createBivariant(); + } return self::createInvariant(); } if ($this->covariant()) { if ($other->contravariant()) { - return self::createCovariant(); + return self::createContravariant(); } if ($other->covariant()) { return self::createCovariant(); } + if ($other->bivariant()) { + return self::createBivariant(); + } return self::createInvariant(); } + if (self::$invarianceCompositionEnabled && $this->invariant()) { + return self::createInvariant(); + } + + if ($this->bivariant()) { + return self::createBivariant(); + } + return $other; } public function isValidVariance(Type $a, Type $b): TrinaryLogic + { + return $this->isValidVarianceWithReason(null, $a, $b)->result; + } + + public function isValidVarianceWithReason(?TemplateType $templateType, Type $a, Type $b): AcceptsResult { if ($b instanceof NeverType) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } if ($a instanceof MixedType && !$a instanceof TemplateType) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } if ($a instanceof BenevolentUnionType) { if (!$a->isSuperTypeOf($b)->no()) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } } if ($b instanceof BenevolentUnionType) { if (!$b->isSuperTypeOf($a)->no()) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } } if ($b instanceof MixedType && !$b instanceof TemplateType) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } if ($this->invariant()) { - return TrinaryLogic::createFromBoolean($a->equals($b)); + $result = $a->equals($b); + $reasons = []; + if (!$result) { + if ( + $templateType !== null + && $templateType->getScope()->getClassName() !== null + && $a->isSuperTypeOf($b)->yes() + ) { + $reasons[] = sprintf( + 'Template type %s on class %s is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', + $templateType->getName(), + $templateType->getScope()->getClassName(), + ); + } + } + + return new AcceptsResult(TrinaryLogic::createFromBoolean($result), $reasons); } if ($this->covariant()) { - return $a->isSuperTypeOf($b); + return new AcceptsResult($a->isSuperTypeOf($b), []); } if ($this->contravariant()) { - return $b->isSuperTypeOf($a); + return new AcceptsResult($b->isSuperTypeOf($a), []); + } + + if ($this->bivariant()) { + return AcceptsResult::createYes(); } throw new ShouldNotHappenException(); @@ -146,6 +201,7 @@ public function validPosition(self $other): bool { return $other->value === $this->value || $other->invariant() + || $this->bivariant() || $this->static(); } @@ -160,6 +216,27 @@ public function describe(): string return 'contravariant'; case self::STATIC: return 'static'; + case self::BIVARIANT: + return 'bivariant'; + } + + throw new ShouldNotHappenException(); + } + + /** + * @return GenericTypeNode::VARIANCE_* + */ + public function toPhpDocNodeVariance(): string + { + switch ($this->value) { + case self::INVARIANT: + return GenericTypeNode::VARIANCE_INVARIANT; + case self::COVARIANT: + return GenericTypeNode::VARIANCE_COVARIANT; + case self::CONTRAVARIANT: + return GenericTypeNode::VARIANCE_CONTRAVARIANT; + case self::BIVARIANT: + return GenericTypeNode::VARIANCE_BIVARIANT; } throw new ShouldNotHappenException(); @@ -173,4 +250,9 @@ public static function __set_state(array $properties): self return new self($properties['value']); } + public static function setInvarianceCompositionEnabled(bool $enabled): void + { + self::$invarianceCompositionEnabled = $enabled; + } + } diff --git a/src/Type/Generic/TemplateTypeVarianceMap.php b/src/Type/Generic/TemplateTypeVarianceMap.php new file mode 100644 index 0000000000..55d3a18aa3 --- /dev/null +++ b/src/Type/Generic/TemplateTypeVarianceMap.php @@ -0,0 +1,51 @@ + $variances + */ + public function __construct(private array $variances) + { + } + + public static function createEmpty(): self + { + $empty = self::$empty; + + if ($empty !== null) { + return $empty; + } + + $empty = new self([]); + self::$empty = $empty; + + return $empty; + } + + /** @return array */ + public function getVariances(): array + { + return $this->variances; + } + + public function hasVariance(string $name): bool + { + return array_key_exists($name, $this->getVariances()); + } + + public function getVariance(string $name): ?TemplateTypeVariance + { + return $this->getVariances()[$name] ?? null; + } + +} diff --git a/src/Type/Generic/TypeProjectionHelper.php b/src/Type/Generic/TypeProjectionHelper.php new file mode 100644 index 0000000000..217103bd09 --- /dev/null +++ b/src/Type/Generic/TypeProjectionHelper.php @@ -0,0 +1,31 @@ +describe($level); + + if ($variance === null || $variance->invariant()) { + return $describedType; + } + + if ($variance->bivariant()) { + return '*'; + } + + return sprintf('%s %s', $variance->describe(), $describedType); + } + +} diff --git a/src/Type/GenericTypeVariableResolver.php b/src/Type/GenericTypeVariableResolver.php index fc670ee662..4f13c5eec9 100644 --- a/src/Type/GenericTypeVariableResolver.php +++ b/src/Type/GenericTypeVariableResolver.php @@ -8,6 +8,9 @@ class GenericTypeVariableResolver { + /** + * @deprecated Use Type::getTemplateType() instead. + */ public static function getType( TypeWithClassName $type, string $genericClassName, diff --git a/src/Type/Helper/GetTemplateTypeType.php b/src/Type/Helper/GetTemplateTypeType.php new file mode 100644 index 0000000000..12e03fe1b0 --- /dev/null +++ b/src/Type/Helper/GetTemplateTypeType.php @@ -0,0 +1,118 @@ +type->getReferencedClasses(); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return $this->type->getReferencedTemplateTypes($positionVariance); + } + + public function equals(Type $type): bool + { + return $type instanceof self + && $this->type->equals($type->type); + } + + public function describe(VerbosityLevel $level): string + { + return sprintf('template-type<%s, %s, %s>', $this->type->describe($level), $this->ancestorClassName, $this->templateTypeName); + } + + public function isResolvable(): bool + { + return !TypeUtils::containsTemplateType($this->type); + } + + protected function getResult(): Type + { + return $this->type->getTemplateType($this->ancestorClassName, $this->templateTypeName); + } + + /** + * @param callable(Type): Type $cb + */ + public function traverse(callable $cb): Type + { + $type = $cb($this->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type, $this->ancestorClassName, $this->templateTypeName); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } + + $type = $cb($this->type, $right->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type, $this->ancestorClassName, $this->templateTypeName); + } + + public function toPhpDocNode(): TypeNode + { + return new GenericTypeNode( + new IdentifierTypeNode('template-type'), + [ + $this->type->toPhpDocNode(), + new IdentifierTypeNode($this->ancestorClassName), + new ConstTypeNode(new QuoteAwareConstExprStringNode($this->templateTypeName, QuoteAwareConstExprStringNode::SINGLE_QUOTED)), + ], + ); + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): Type + { + return new self( + $properties['type'], + $properties['ancestorClassName'], + $properties['templateTypeName'], + ); + } + +} diff --git a/src/Type/IntegerRangeType.php b/src/Type/IntegerRangeType.php index 973297b753..1779cb41ad 100644 --- a/src/Type/IntegerRangeType.php +++ b/src/Type/IntegerRangeType.php @@ -2,6 +2,12 @@ namespace PHPStan\Type; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; +use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -11,6 +17,7 @@ use function count; use function floor; use function get_class; +use function is_float; use function is_int; use function max; use function min; @@ -192,16 +199,21 @@ public function shift(int $amount): Type } public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof parent) { - return $this->isSuperTypeOf($type); + return new AcceptsResult($this->isSuperTypeOf($type), []); } if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -275,7 +287,12 @@ private function isSubTypeOfUnion(UnionType $otherType): TrinaryLogic public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - return $this->isSubTypeOf($acceptingType); + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return new AcceptsResult($this->isSubTypeOf($acceptingType), []); } public function equals(Type $type): bool @@ -553,6 +570,88 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } + public function exponentiate(Type $exponent): Type + { + if ($exponent instanceof UnionType) { + $results = []; + foreach ($exponent->getTypes() as $unionType) { + $results[] = $this->exponentiate($unionType); + } + return TypeCombinator::union(...$results); + } + + if ($exponent instanceof IntegerRangeType) { + $min = null; + $max = null; + if ($this->getMin() !== null && $exponent->getMin() !== null) { + $min = $this->getMin() ** $exponent->getMin(); + } + if ($this->getMax() !== null && $exponent->getMax() !== null) { + $max = $this->getMax() ** $exponent->getMax(); + } + + if (($min !== null || $max !== null) && !is_float($min) && !is_float($max)) { + return self::fromInterval($min, $max); + } + } + + if ($exponent instanceof ConstantScalarType) { + $exponentValue = $exponent->getValue(); + if (is_int($exponentValue)) { + $min = null; + $max = null; + if ($this->getMin() !== null) { + $min = $this->getMin() ** $exponentValue; + } + if ($this->getMax() !== null) { + $max = $this->getMax() ** $exponentValue; + } + + if (!is_float($min) && !is_float($max)) { + return self::fromInterval($min, $max); + } + } + } + + return parent::exponentiate($exponent); + } + + public function getFiniteTypes(): array + { + if ($this->min === null || $this->max === null) { + return []; + } + + $size = $this->max - $this->min; + if ($size > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return []; + } + + $types = []; + for ($i = $this->min; $i <= $this->max; $i++) { + $types[] = new ConstantIntegerType($i); + } + + return $types; + } + + public function toPhpDocNode(): TypeNode + { + if ($this->min === null) { + $min = new IdentifierTypeNode('min'); + } else { + $min = new ConstTypeNode(new ConstExprIntegerNode((string) $this->min)); + } + + if ($this->max === null) { + $max = new IdentifierTypeNode('max'); + } else { + $max = new ConstTypeNode(new ConstExprIntegerNode((string) $this->max)); + } + + return new GenericTypeNode(new IdentifierTypeNode('int'), [$min, $max]); + } + /** * @param mixed[] $properties */ diff --git a/src/Type/IntegerType.php b/src/Type/IntegerType.php index 42bb93638a..5e81e00519 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -2,9 +2,14 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonCallableTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; @@ -19,6 +24,7 @@ class IntegerType implements Type { use JustNullableTypeTrait; + use NonArrayTypeTrait; use NonCallableTypeTrait; use NonIterableTypeTrait; use NonObjectTypeTrait; @@ -38,6 +44,11 @@ public function describe(VerbosityLevel $level): string return 'int'; } + public function getConstantStrings(): array + { + return []; + } + /** * @param mixed[] $properties */ @@ -75,9 +86,56 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], + [], + TrinaryLogic::createYes(), ); } + public function toArrayKey(): Type + { + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function tryRemove(Type $typeToRemove): ?Type { if ($typeToRemove instanceof IntegerRangeType || $typeToRemove instanceof ConstantIntegerType) { @@ -99,4 +157,19 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } + public function getFiniteTypes(): array + { + return []; + } + + public function exponentiate(Type $exponent): Type + { + return ExponentiateHelper::exponentiate($this, $exponent); + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('int'); + } + } diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 074990846d..cc2a3f957c 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -2,10 +2,15 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\IntersectionTypeUnresolvedMethodPrototypeReflection; @@ -14,22 +19,31 @@ use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\Accessory\NonEmptyArrayType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonRemoveableTypeTrait; +use function array_intersect_key; use function array_map; +use function array_shift; +use function array_unique; +use function array_values; use function count; use function implode; use function in_array; +use function ksort; +use function md5; use function sprintf; +use function str_starts_with; use function strlen; use function substr; @@ -91,23 +105,118 @@ public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap return $types; } - /** - * @return string[] - */ public function getReferencedClasses(): array { - return UnionTypeHelper::getReferencedClasses($this->types); + $classes = []; + foreach ($this->types as $type) { + foreach ($type->getReferencedClasses() as $className) { + $classes[] = $className; + } + } + + return $classes; + } + + public function getObjectClassNames(): array + { + $objectClassNames = []; + foreach ($this->types as $type) { + $innerObjectClassNames = $type->getObjectClassNames(); + foreach ($innerObjectClassNames as $innerObjectClassName) { + $objectClassNames[] = $innerObjectClassName; + } + } + + return array_values(array_unique($objectClassNames)); + } + + public function getObjectClassReflections(): array + { + $reflections = []; + foreach ($this->types as $type) { + foreach ($type->getObjectClassReflections() as $reflection) { + $reflections[] = $reflection; + } + } + + return $reflections; + } + + public function getArrays(): array + { + $arrays = []; + foreach ($this->types as $type) { + foreach ($type->getArrays() as $array) { + $arrays[] = $array; + } + } + + return $arrays; + } + + public function getConstantArrays(): array + { + $constantArrays = []; + foreach ($this->types as $type) { + foreach ($type->getConstantArrays() as $constantArray) { + $constantArrays[] = $constantArray; + } + } + + return $constantArrays; + } + + public function getConstantStrings(): array + { + $strings = []; + foreach ($this->types as $type) { + foreach ($type->getConstantStrings() as $string) { + $strings[] = $string; + } + } + + return $strings; + } + + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; } - public function accepts(Type $otherType, bool $strictTypes): TrinaryLogic + public function acceptsWithReason(Type $otherType, bool $strictTypes): AcceptsResult { + $result = AcceptsResult::createYes(); foreach ($this->types as $type) { - if (!$type->accepts($otherType, $strictTypes)->yes()) { - return TrinaryLogic::createNo(); + $result = $result->and($type->acceptsWithReason($otherType, $strictTypes)); + } + + if (!$result->yes()) { + $isList = $otherType->isList(); + $reasons = $result->reasons; + $verbosity = VerbosityLevel::getRecommendedLevelByType($this, $otherType); + if ($this->isList()->yes() && !$isList->yes()) { + $reasons[] = sprintf( + '%s %s a list.', + $otherType->describe($verbosity), + $isList->no() ? 'is not' : 'might not be', + ); + } + + $isNonEmpty = $otherType->isIterableAtLeastOnce(); + if ($this->isIterableAtLeastOnce()->yes() && !$isNonEmpty->yes()) { + $reasons[] = sprintf( + '%s %s empty.', + $otherType->describe($verbosity), + $isNonEmpty->no() ? 'is' : 'might be', + ); + } + + if (count($reasons) > 0) { + return new AcceptsResult($result->result, $reasons); } } - return TrinaryLogic::createYes(); + return $result; } public function isSuperTypeOf(Type $otherType): TrinaryLogic @@ -129,12 +238,31 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic return $otherType->isSuperTypeOf($this); } - return TrinaryLogic::lazyMaxMin($this->getTypes(), static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType)); + $result = TrinaryLogic::lazyMaxMin($this->getTypes(), static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType)); + if ($this->isOversizedArray()->yes()) { + if (!$result->no()) { + return TrinaryLogic::createYes(); + } + } + + return $result; } public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - return TrinaryLogic::lazyMaxMin($this->getTypes(), static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes)); + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + $result = AcceptsResult::maxMin(...array_map(static fn (Type $innerType) => $acceptingType->acceptsWithReason($innerType, $strictTypes), $this->types)); + if ($this->isOversizedArray()->yes()) { + if (!$result->no()) { + return AcceptsResult::createYes(); + } + } + + return $result; } public function equals(Type $type): bool @@ -189,12 +317,13 @@ function () use ($level): string { private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes): string { + $baseTypes = []; $typesToDescribe = []; $skipTypeNames = []; $nonEmptyStr = false; $nonFalsyStr = false; - foreach ($this->getSortedTypes() as $type) { + foreach ($this->getSortedTypes() as $i => $type) { if ($type instanceof AccessoryNonEmptyStringType || $type instanceof AccessoryLiteralStringType || $type instanceof AccessoryNumericStringType @@ -217,46 +346,76 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) } } - $typesToDescribe[] = $type; + $typesToDescribe[$i] = $type; $skipTypeNames[] = 'string'; continue; } - if ($type instanceof NonEmptyArrayType) { - $typesToDescribe[] = $type; + if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) { + $typesToDescribe[$i] = $type; $skipTypeNames[] = 'array'; continue; } - if ($skipAccessoryTypes) { + if ($type instanceof CallableType && $type->isCommonCallable()) { + $typesToDescribe[$i] = $type; + $skipTypeNames[] = 'object'; + $skipTypeNames[] = 'string'; continue; } if (!$type instanceof AccessoryType) { + $baseTypes[$i] = $type; + continue; + } + + if ($skipAccessoryTypes) { continue; } - $typesToDescribe[] = $type; + $typesToDescribe[$i] = $type; } $describedTypes = []; - foreach ($this->getSortedTypes() as $type) { - if ($type instanceof AccessoryType) { - continue; - } + foreach ($baseTypes as $i => $type) { $typeDescription = $type->describe($level); + + if (in_array($typeDescription, ['object', 'string'], true) && in_array($typeDescription, $skipTypeNames, true)) { + foreach ($typesToDescribe as $j => $typeToDescribe) { + if ($typeToDescribe instanceof CallableType && $typeToDescribe->isCommonCallable()) { + $describedTypes[$i] = 'callable-' . $typeDescription; + unset($typesToDescribe[$j]); + continue 2; + } + } + } + if ( - substr($typeDescription, 0, strlen('array<')) === 'array<' + str_starts_with($typeDescription, 'array<') && in_array('array', $skipTypeNames, true) ) { + $nonEmpty = false; + $typeName = 'array'; foreach ($typesToDescribe as $j => $typeToDescribe) { - if (!$typeToDescribe instanceof NonEmptyArrayType) { + if ( + $typeToDescribe instanceof AccessoryArrayListType + && substr($typeDescription, 0, strlen('array, ')) === 'array, ' + ) { + $typeName = 'list'; + $typeDescription = 'array<' . substr($typeDescription, strlen('array, ')); + } elseif ($typeToDescribe instanceof NonEmptyArrayType) { + $nonEmpty = true; + } else { continue; } unset($typesToDescribe[$j]); } - $describedTypes[] = 'non-empty-array<' . substr($typeDescription, strlen('array<')); + if ($nonEmpty) { + $typeName = 'non-empty-' . $typeName; + } + + $describedTypes[$i] = $typeName . '<' . substr($typeDescription, strlen('array<')); continue; } @@ -264,16 +423,33 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) continue; } - $describedTypes[] = $type->describe($level); + $describedTypes[$i] = $type->describe($level); } - foreach ($typesToDescribe as $typeToDescribe) { - $describedTypes[] = $typeToDescribe->describe($level); + foreach ($typesToDescribe as $i => $typeToDescribe) { + $describedTypes[$i] = $typeToDescribe->describe($level); } + ksort($describedTypes); + return implode('&', $describedTypes); } + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getTemplateType($ancestorClassName, $templateTypeName)); + } + + public function isObject(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isObject()); + } + + public function isEnum(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isEnum()); + } + public function canAccessProperties(): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessProperties()); @@ -322,7 +498,7 @@ public function hasMethod(string $methodName): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName)); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -381,26 +557,61 @@ public function isIterableAtLeastOnce(): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce()); } + public function getArraySize(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getArraySize()); + } + public function getIterableKeyType(): Type { return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType()); } + public function getFirstIterableKeyType(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getFirstIterableKeyType()); + } + + public function getLastIterableKeyType(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getLastIterableKeyType()); + } + public function getIterableValueType(): Type { return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType()); } + public function getFirstIterableValueType(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getFirstIterableValueType()); + } + + public function getLastIterableValueType(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getLastIterableValueType()); + } + public function isArray(): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isArray()); } + public function isConstantArray(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantArray()); + } + public function isOversizedArray(): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOversizedArray()); } + public function isList(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isList()); + } + public function isString(): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isString()); @@ -426,6 +637,36 @@ public function isLiteralString(): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString()); } + public function isClassStringType(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isClassStringType()); + } + + public function getClassStringObjectType(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getClassStringObjectType()); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getObjectTypeOrClassStringObjectType()); + } + + public function isVoid(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isVoid()); + } + + public function isScalar(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isScalar()); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function isOffsetAccessible(): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible()); @@ -433,12 +674,24 @@ public function isOffsetAccessible(): TrinaryLogic public function hasOffsetValueType(Type $offsetType): TrinaryLogic { + if ($this->isList()->yes() && $this->isIterableAtLeastOnce()->yes()) { + $arrayKeyOffsetType = $offsetType->toArrayKey(); + if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) { + return TrinaryLogic::createYes(); + } + } + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType)); } public function getOffsetValueType(Type $offsetType): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->getOffsetValueType($offsetType)); + $result = $this->intersectTypes(static fn (Type $type): Type => $type->getOffsetValueType($offsetType)); + if ($this->isOversizedArray()->yes()) { + return TypeUtils::toBenevolentUnion($result); + } + + return $result; } public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type @@ -446,19 +699,80 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType)); + } + public function unsetOffset(Type $offsetType): Type { return $this->intersectTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType)); } + public function getKeysArray(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getKeysArray()); + } + + public function getValuesArray(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getValuesArray()); + } + + public function fillKeysArray(Type $valueType): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType)); + } + + public function flipArray(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->flipArray()); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType)); + } + + public function popArray(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->popArray()); + } + + public function searchArray(Type $needleType): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->searchArray($needleType)); + } + + public function shiftArray(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->shiftArray()); + } + + public function shuffleArray(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->shuffleArray()); + } + + public function getEnumCases(): array + { + $compare = []; + foreach ($this->types as $type) { + $oneType = []; + foreach ($type->getEnumCases() as $enumCase) { + $oneType[md5($enumCase->describe(VerbosityLevel::typeOnly()))] = $enumCase; + } + $compare[] = $oneType; + } + + return array_values(array_intersect_key(...$compare)); + } + public function isCallable(): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCallable()); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { if ($this->isCallable()->no()) { @@ -483,6 +797,70 @@ public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType)); } + public function isNull(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNull()); + } + + public function isConstantValue(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue()); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue()); + } + + public function getConstantScalarTypes(): array + { + $scalarTypes = []; + foreach ($this->types as $type) { + foreach ($type->getConstantScalarTypes() as $scalarType) { + $scalarTypes[] = $scalarType; + } + } + + return $scalarTypes; + } + + public function getConstantScalarValues(): array + { + $values = []; + foreach ($this->types as $type) { + foreach ($type->getConstantScalarValues() as $value) { + $values[] = $value; + } + } + + return $values; + } + + public function isTrue(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isTrue()); + } + + public function isFalse(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFalse()); + } + + public function isBoolean(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isBoolean()); + } + + public function isFloat(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFloat()); + } + + public function isInteger(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isInteger()); + } + public function isGreaterThan(Type $otherType): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type)); @@ -559,6 +937,19 @@ public function toArray(): Type return $type; } + public function toArrayKey(): Type + { + if ($this->isNumericString()->yes()) { + return new IntegerType(); + } + + if ($this->isString()->yes()) { + return $this; + } + + return $this->intersectTypes(static fn (Type $type): Type => $type->toArrayKey()); + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { $types = TemplateTypeMap::createEmpty(); @@ -603,11 +994,65 @@ public function traverse(callable $cb): Type return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + $types = []; + $changed = false; + + if (!$right instanceof self) { + return $this; + } + + if (count($this->getTypes()) !== count($right->getTypes())) { + return $this; + } + + foreach ($this->getSortedTypes() as $i => $leftType) { + $rightType = $right->getSortedTypes()[$i]; + $newType = $cb($leftType, $rightType); + if ($leftType !== $newType) { + $changed = true; + } + $types[] = $newType; + } + + if ($changed) { + return TypeCombinator::intersect(...$types); + } + + return $this; + } + public function tryRemove(Type $typeToRemove): ?Type { return $this->intersectTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove)); } + public function exponentiate(Type $exponent): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->exponentiate($exponent)); + } + + public function getFiniteTypes(): array + { + $compare = []; + foreach ($this->types as $type) { + $oneType = []; + foreach ($type->getFiniteTypes() as $finiteType) { + $oneType[md5($finiteType->describe(VerbosityLevel::typeOnly()))] = $finiteType; + } + $compare[] = $oneType; + } + + $result = array_values(array_intersect_key(...$compare)); + + if (count($result) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return []; + } + + return $result; + } + /** * @param mixed[] $properties */ @@ -633,4 +1078,112 @@ private function intersectTypes(callable $getType): Type return TypeCombinator::intersect(...$operands); } + public function toPhpDocNode(): TypeNode + { + $baseTypes = []; + $typesToDescribe = []; + $skipTypeNames = []; + + $nonEmptyStr = false; + $nonFalsyStr = false; + + foreach ($this->getSortedTypes() as $i => $type) { + if ($type instanceof AccessoryNonEmptyStringType + || $type instanceof AccessoryLiteralStringType + || $type instanceof AccessoryNumericStringType + || $type instanceof AccessoryNonFalsyStringType + ) { + if ($type instanceof AccessoryNonFalsyStringType) { + $nonFalsyStr = true; + } + if ($type instanceof AccessoryNonEmptyStringType) { + $nonEmptyStr = true; + } + if ($nonEmptyStr && $nonFalsyStr) { + // prevent redundant 'non-empty-string&non-falsy-string' + foreach ($typesToDescribe as $key => $typeToDescribe) { + if (!($typeToDescribe instanceof AccessoryNonEmptyStringType)) { + continue; + } + + unset($typesToDescribe[$key]); + } + } + + $typesToDescribe[$i] = $type; + $skipTypeNames[] = 'string'; + continue; + } + if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) { + $typesToDescribe[$i] = $type; + $skipTypeNames[] = 'array'; + continue; + } + if (!$type instanceof AccessoryType) { + $baseTypes[$i] = $type; + continue; + } + + $accessoryPhpDocNode = $type->toPhpDocNode(); + if ($accessoryPhpDocNode instanceof IdentifierTypeNode && $accessoryPhpDocNode->name === '') { + continue; + } + + $typesToDescribe[$i] = $type; + } + + $describedTypes = []; + foreach ($baseTypes as $i => $type) { + $typeNode = $type->toPhpDocNode(); + if ($typeNode instanceof GenericTypeNode && $typeNode->type->name === 'array') { + $nonEmpty = false; + $typeName = 'array'; + foreach ($typesToDescribe as $j => $typeToDescribe) { + if ($typeToDescribe instanceof AccessoryArrayListType) { + $typeName = 'list'; + if (count($typeNode->genericTypes) > 1) { + array_shift($typeNode->genericTypes); + } + } elseif ($typeToDescribe instanceof NonEmptyArrayType) { + $nonEmpty = true; + } else { + continue; + } + + unset($typesToDescribe[$j]); + } + + if ($nonEmpty) { + $typeName = 'non-empty-' . $typeName; + } + + $describedTypes[$i] = new GenericTypeNode( + new IdentifierTypeNode($typeName), + $typeNode->genericTypes, + ); + continue; + } + + if ($typeNode instanceof IdentifierTypeNode && in_array($typeNode->name, $skipTypeNames, true)) { + continue; + } + + $describedTypes[$i] = $typeNode; + } + + foreach ($typesToDescribe as $i => $typeToDescribe) { + $describedTypes[$i] = $typeToDescribe->toPhpDocNode(); + } + + ksort($describedTypes); + + $describedTypes = array_values($describedTypes); + + if (count($describedTypes) === 1) { + return $describedTypes[0]; + } + + return new IntersectionTypeNode($describedTypes); + } + } diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index 9b381ef3da..c675aff51d 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -2,13 +2,16 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateMixedType; -use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Traits\MaybeArrayTypeTrait; use PHPStan\Type\Traits\MaybeCallableTypeTrait; use PHPStan\Type\Traits\MaybeObjectTypeTrait; use PHPStan\Type\Traits\MaybeOffsetAccessibleTypeTrait; @@ -23,6 +26,7 @@ class IterableType implements CompoundType { + use MaybeArrayTypeTrait; use MaybeCallableTypeTrait; use MaybeObjectTypeTrait; use MaybeOffsetAccessibleTypeTrait; @@ -59,21 +63,41 @@ public function getReferencedClasses(): array ); } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic { - if ($type instanceof ConstantArrayType && $type->isEmpty()) { - return TrinaryLogic::createYes(); + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + { + if ($type->isConstantArray()->yes() && $type->isIterableAtLeastOnce()->no()) { + return AcceptsResult::createYes(); } if ($type->isIterable()->yes()) { - return $this->getIterableValueType()->accepts($type->getIterableValueType(), $strictTypes) - ->and($this->getIterableKeyType()->accepts($type->getIterableKeyType(), $strictTypes)); + return $this->getIterableValueType()->acceptsWithReason($type->getIterableValueType(), $strictTypes) + ->and($this->getIterableKeyType()->acceptsWithReason($type->getIterableKeyType(), $strictTypes)); } if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -133,7 +157,7 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic $limit = TrinaryLogic::createMaybe(); } - if ($otherType instanceof ConstantArrayType && $otherType->isEmpty()) { + if ($otherType->isConstantArray()->yes() && $otherType->isIterableAtLeastOnce()->no()) { return TrinaryLogic::createMaybe(); } @@ -146,7 +170,12 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - return $this->isSubTypeOf($acceptingType); + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return new AcceptsResult($this->isSubTypeOf($acceptingType), []); } public function equals(Type $type): bool @@ -161,9 +190,8 @@ public function equals(Type $type): bool public function describe(VerbosityLevel $level): string { - $isMixedKeyType = $this->keyType instanceof MixedType && !$this->keyType instanceof TemplateType; - $isMixedItemType = $this->itemType instanceof MixedType && !$this->itemType instanceof TemplateType; - + $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed'; + $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed'; if ($isMixedKeyType) { if ($isMixedItemType) { return 'iterable'; @@ -209,6 +237,11 @@ public function toArray(): Type return new ArrayType($this->keyType, $this->getItemType()); } + public function toArrayKey(): Type + { + return new ErrorType(); + } + public function isIterable(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -219,24 +252,89 @@ public function isIterableAtLeastOnce(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getArraySize(): Type + { + return IntegerRangeType::fromInterval(0, null); + } + public function getIterableKeyType(): Type { return $this->keyType; } + public function getFirstIterableKeyType(): Type + { + return $this->keyType; + } + + public function getLastIterableKeyType(): Type + { + return $this->keyType; + } + public function getIterableValueType(): Type { return $this->getItemType(); } - public function isArray(): TrinaryLogic + public function getFirstIterableValueType(): Type { - return TrinaryLogic::createMaybe(); + return $this->getItemType(); } - public function isOversizedArray(): TrinaryLogic + public function getLastIterableValueType(): Type { - return TrinaryLogic::createMaybe(); + return $this->getItemType(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); } public function isString(): TrinaryLogic @@ -264,6 +362,41 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function getEnumCases(): array + { + return []; + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) { @@ -282,9 +415,11 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array { + $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant()); + return array_merge( - $this->getIterableKeyType()->getReferencedTemplateTypes(TemplateTypeVariance::createCovariant()), - $this->getIterableValueType()->getReferencedTemplateTypes(TemplateTypeVariance::createCovariant()), + $this->getIterableKeyType()->getReferencedTemplateTypes($variance), + $this->getIterableValueType()->getReferencedTemplateTypes($variance), ); } @@ -300,6 +435,18 @@ public function traverse(callable $cb): Type return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + $keyType = $cb($this->keyType, $right->getIterableKeyType()); + $itemType = $cb($this->itemType, $right->getIterableValueType()); + + if ($keyType !== $this->keyType || $itemType !== $this->itemType) { + return new self($keyType, $itemType); + } + + return $this; + } + public function tryRemove(Type $typeToRemove): ?Type { $arrayType = new ArrayType(new MixedType(), new MixedType()); @@ -318,6 +465,43 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + $isMixedKeyType = $this->keyType instanceof MixedType && $this->keyType->describe(VerbosityLevel::precise()) === 'mixed'; + $isMixedItemType = $this->itemType instanceof MixedType && $this->itemType->describe(VerbosityLevel::precise()) === 'mixed'; + + if ($isMixedKeyType) { + if ($isMixedItemType) { + return new IdentifierTypeNode('iterable'); + } + + return new GenericTypeNode( + new IdentifierTypeNode('iterable'), + [ + $this->itemType->toPhpDocNode(), + ], + ); + } + + return new GenericTypeNode( + new IdentifierTypeNode('iterable'), + [ + $this->keyType->toPhpDocNode(), + $this->itemType->toPhpDocNode(), + ], + ); + } + /** * @param mixed[] $properties */ diff --git a/src/Type/JustNullableTypeTrait.php b/src/Type/JustNullableTypeTrait.php index a2895ff246..294b7d1d2b 100644 --- a/src/Type/JustNullableTypeTrait.php +++ b/src/Type/JustNullableTypeTrait.php @@ -16,17 +16,32 @@ public function getReferencedClasses(): array return []; } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof static) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -52,12 +67,57 @@ public function traverse(callable $cb): Type return $this; } - public function isArray(): TrinaryLogic + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function isOversizedArray(): TrinaryLogic + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -87,4 +147,24 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + } diff --git a/src/Type/KeyOfType.php b/src/Type/KeyOfType.php index d2cc37cc0b..32eab8b019 100644 --- a/src/Type/KeyOfType.php +++ b/src/Type/KeyOfType.php @@ -2,6 +2,9 @@ namespace PHPStan\Type; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Traits\LateResolvableTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; @@ -65,7 +68,27 @@ public function traverse(callable $cb): Type return $this; } - return new KeyOfType($type); + return new self($type); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } + + $type = $cb($this->type, $right->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type); + } + + public function toPhpDocNode(): TypeNode + { + return new GenericTypeNode(new IdentifierTypeNode('key-of'), [$this->type->toPhpDocNode()]); } /** diff --git a/src/Type/LooseComparisonHelper.php b/src/Type/LooseComparisonHelper.php new file mode 100644 index 0000000000..b4df432c6f --- /dev/null +++ b/src/Type/LooseComparisonHelper.php @@ -0,0 +1,50 @@ +castsNumbersToStringsOnLooseComparison()) { + $isNumber = new UnionType([ + new IntegerType(), + new FloatType(), + ]); + + if ($leftType->isString()->yes() && $leftType->isNumericString()->no() && $isNumber->isSuperTypeOf($rightType)->yes()) { + $stringValue = (string) $rightType->getValue(); + return new ConstantBooleanType($stringValue === $leftType->getValue()); + } + if ($rightType->isString()->yes() && $rightType->isNumericString()->no() && $isNumber->isSuperTypeOf($leftType)->yes()) { + $stringValue = (string) $leftType->getValue(); + return new ConstantBooleanType($stringValue === $rightType->getValue()); + } + } else { + if ($leftType->isString()->yes() && $leftType->isNumericString()->no() && $rightType->isFloat()->yes()) { + $numericPart = (float) $leftType->getValue(); + return new ConstantBooleanType($numericPart === $rightType->getValue()); + } + if ($rightType->isString()->yes() && $rightType->isNumericString()->no() && $leftType->isFloat()->yes()) { + $numericPart = (float) $rightType->getValue(); + return new ConstantBooleanType($numericPart === $leftType->getValue()); + } + if ($leftType->isString()->yes() && $leftType->isNumericString()->no() && $rightType->isInteger()->yes()) { + $numericPart = (int) $leftType->getValue(); + return new ConstantBooleanType($numericPart === $rightType->getValue()); + } + if ($rightType->isString()->yes() && $rightType->isNumericString()->no() && $leftType->isInteger()->yes()) { + $numericPart = (int) $rightType->getValue(); + return new ConstantBooleanType($numericPart === $leftType->getValue()); + } + } + + // @phpstan-ignore equal.notAllowed + return new ConstantBooleanType($leftType->getValue() == $rightType->getValue()); // phpcs:ignore + } + +} diff --git a/src/Type/MethodTypeSpecifyingExtension.php b/src/Type/MethodTypeSpecifyingExtension.php index 11543d6063..9e56ea330f 100644 --- a/src/Type/MethodTypeSpecifyingExtension.php +++ b/src/Type/MethodTypeSpecifyingExtension.php @@ -8,7 +8,23 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\MethodReflection; -/** @api */ +/** + * This is the interface type-specifying extensions implement for non-static methods. + * + * To register it in the configuration file use the `phpstan.typeSpecifier.methodTypeSpecifyingExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.typeSpecifier.methodTypeSpecifyingExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/type-specifying-extensions + * + * @api + */ interface MethodTypeSpecifyingExtension { diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index b0ecf936da..14badc4786 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -3,13 +3,15 @@ namespace PHPStan\Type; use ArrayAccess; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\Dummy\DummyConstantReflection; use PHPStan\Reflection\Dummy\DummyMethodReflection; use PHPStan\Reflection\Dummy\DummyPropertyReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection; @@ -17,6 +19,7 @@ use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; @@ -62,9 +65,39 @@ public function getReferencedClasses(): array return []; } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getArrays(): array + { + return []; + } + + public function getConstantArrays(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic { - return TrinaryLogic::createYes(); + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + { + return AcceptsResult::createYes(); } public function isSuperTypeOfMixed(MixedType $type): TrinaryLogic @@ -125,6 +158,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new self($this->isExplicitMixed); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return new self($this->isExplicitMixed); + } + public function unsetOffset(Type $offsetType): Type { if ($this->subtractedType !== null) { @@ -133,6 +171,87 @@ public function unsetOffset(Type $offsetType): Type return $this; } + public function getKeysArray(): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new UnionType([new IntegerType(), new StringType()]))); + } + + public function getValuesArray(): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new MixedType($this->isExplicitMixed))); + } + + public function fillKeysArray(Type $valueType): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType($this->getIterableValueType(), $valueType); + } + + public function flipArray(): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); + } + + public function popArray(): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); + } + + public function searchArray(Type $needleType): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return TypeCombinator::union(new IntegerType(), new StringType(), new ConstantBooleanType(false)); + } + + public function shiftArray(): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed)); + } + + public function shuffleArray(): Type + { + if ($this->isArray()->no()) { + return new ErrorType(); + } + + return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new MixedType($this->isExplicitMixed))); + } + public function isCallable(): TrinaryLogic { if ($this->subtractedType !== null) { @@ -144,9 +263,11 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createMaybe(); } - /** - * @return ParametersAcceptor[] - */ + public function getEnumCases(): array + { + return []; + } + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return [new TrivialParametersAcceptor()]; @@ -191,11 +312,41 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - $isSuperType = $this->isSuperTypeOf($acceptingType); + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + $isSuperType = new AcceptsResult($this->isSuperTypeOf($acceptingType), []); if ($isSuperType->no()) { return $isSuperType; } - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); + } + + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return new self(); + } + + public function isObject(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new ObjectWithoutClassType())->yes()) { + return TrinaryLogic::createNo(); + } + } + return TrinaryLogic::createMaybe(); + } + + public function isEnum(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new ObjectWithoutClassType())->yes()) { + return TrinaryLogic::createNo(); + } + } + return TrinaryLogic::createMaybe(); } public function canAccessProperties(): TrinaryLogic @@ -234,7 +385,7 @@ public function hasMethod(string $methodName): TrinaryLogic return TrinaryLogic::createYes(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -339,6 +490,11 @@ public function toArray(): Type return new ArrayType($mixed, $mixed); } + public function toArrayKey(): Type + { + return new UnionType([new IntegerType(), new StringType()]); + } + public function isIterable(): TrinaryLogic { if ($this->subtractedType !== null) { @@ -355,16 +511,45 @@ public function isIterableAtLeastOnce(): TrinaryLogic return $this->isIterable(); } + public function getArraySize(): Type + { + if ($this->isIterable()->no()) { + return new ErrorType(); + } + + return IntegerRangeType::fromInterval(0, null); + } + public function getIterableKeyType(): Type { return new self($this->isExplicitMixed); } + public function getFirstIterableKeyType(): Type + { + return new self($this->isExplicitMixed); + } + + public function getLastIterableKeyType(): Type + { + return new self($this->isExplicitMixed); + } + public function getIterableValueType(): Type { return new self($this->isExplicitMixed); } + public function getFirstIterableValueType(): Type + { + return new self($this->isExplicitMixed); + } + + public function getLastIterableValueType(): Type + { + return new self($this->isExplicitMixed); + } + public function isOffsetAccessible(): TrinaryLogic { if ($this->subtractedType !== null) { @@ -383,6 +568,10 @@ public function isOffsetAccessible(): TrinaryLogic public function hasOffsetValueType(Type $offsetType): TrinaryLogic { + if ($this->isOffsetAccessible()->no()) { + return TrinaryLogic::createNo(); + } + return TrinaryLogic::createMaybe(); } @@ -428,6 +617,11 @@ public function traverse(callable $cb): Type return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + public function isArray(): TrinaryLogic { if ($this->subtractedType !== null) { @@ -439,6 +633,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isConstantArray(): TrinaryLogic + { + return $this->isArray(); + } + public function isOversizedArray(): TrinaryLogic { if ($this->subtractedType !== null) { @@ -455,6 +654,108 @@ public function isOversizedArray(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isList(): TrinaryLogic + { + if ($this->subtractedType !== null) { + $list = TypeCombinator::intersect( + new ArrayType(new IntegerType(), new MixedType()), + new AccessoryArrayListType(), + ); + + if ($this->subtractedType->isSuperTypeOf($list)->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isNull(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new NullType())->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new ConstantBooleanType(true))->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isFalse(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new ConstantBooleanType(false))->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isBoolean(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new BooleanType())->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isFloat(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new FloatType())->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isInteger(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new IntegerType())->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + public function isString(): TrinaryLogic { if ($this->subtractedType !== null) { @@ -529,6 +830,69 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isClassStringType(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new StringType())->yes()) { + return TrinaryLogic::createNo(); + } + if ($this->subtractedType->isSuperTypeOf(new ClassStringType())->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + if (!$this->isClassStringType()->no()) { + return new ObjectWithoutClassType(); + } + + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + $objectOrClass = new UnionType([ + new ObjectWithoutClassType(), + new ClassStringType(), + ]); + if (!$this->isSuperTypeOf($objectOrClass)->no()) { + return new ObjectWithoutClassType(); + } + + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new VoidType())->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function isScalar(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new UnionType([new BooleanType(), new FloatType(), new IntegerType(), new StringType()]))->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function tryRemove(Type $typeToRemove): ?Type { if ($this->isSuperTypeOf($typeToRemove)->yes()) { @@ -538,6 +902,24 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } + public function exponentiate(Type $exponent): Type + { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('mixed'); + } + /** * @param mixed[] $properties */ diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index bb9e34b915..2e9721f940 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -2,10 +2,12 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; @@ -46,9 +48,39 @@ public function getReferencedClasses(): array return []; } + public function getArrays(): array + { + return []; + } + + public function getConstantArrays(): array + { + return []; + } + + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic { - return TrinaryLogic::createYes(); + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + { + return AcceptsResult::createYes(); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -72,7 +104,12 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - return $this->isSubTypeOf($acceptingType); + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return new AcceptsResult($this->isSubTypeOf($acceptingType), []); } public function describe(VerbosityLevel $level): string @@ -80,6 +117,21 @@ public function describe(VerbosityLevel $level): string return '*NEVER*'; } + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return new NeverType(); + } + + public function isObject(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isEnum(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function canAccessProperties(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -110,7 +162,7 @@ public function hasMethod(string $methodName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { throw new ShouldNotHappenException(); } @@ -145,16 +197,61 @@ public function isIterableAtLeastOnce(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getArraySize(): Type + { + return new NeverType(); + } + public function getIterableKeyType(): Type { return new NeverType(); } + public function getFirstIterableKeyType(): Type + { + return new NeverType(); + } + + public function getLastIterableKeyType(): Type + { + return new NeverType(); + } + public function getIterableValueType(): Type { return new NeverType(); } + public function getFirstIterableValueType(): Type + { + return new NeverType(); + } + + public function getLastIterableValueType(): Type + { + return new NeverType(); + } + + public function isArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isList(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isOffsetAccessible(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -175,19 +272,66 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new ErrorType(); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return new ErrorType(); + } + public function unsetOffset(Type $offsetType): Type { return new NeverType(); } + public function getKeysArray(): Type + { + return new NeverType(); + } + + public function getValuesArray(): Type + { + return new NeverType(); + } + + public function fillKeysArray(Type $valueType): Type + { + return new NeverType(); + } + + public function flipArray(): Type + { + return new NeverType(); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + return new NeverType(); + } + + public function popArray(): Type + { + return new NeverType(); + } + + public function searchArray(Type $needleType): Type + { + return new NeverType(); + } + + public function shiftArray(): Type + { + return new NeverType(); + } + + public function shuffleArray(): Type + { + return new NeverType(); + } + public function isCallable(): TrinaryLogic { return TrinaryLogic::createYes(); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return [new TrivialParametersAcceptor()]; @@ -223,17 +367,67 @@ public function toArray(): Type return $this; } + public function toArrayKey(): Type + { + return $this; + } + public function traverse(callable $cb): Type { return $this; } - public function isArray(): TrinaryLogic + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function isNull(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function isOversizedArray(): TrinaryLogic + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -263,6 +457,56 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function getEnumCases(): array + { + return []; + } + + public function exponentiate(Type $exponent): Type + { + return $this; + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('never'); + } + /** * @param mixed[] $properties */ diff --git a/src/Type/NonAcceptingNeverType.php b/src/Type/NonAcceptingNeverType.php new file mode 100644 index 0000000000..3eaddf53cb --- /dev/null +++ b/src/Type/NonAcceptingNeverType.php @@ -0,0 +1,43 @@ +acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof self) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -139,6 +164,11 @@ public function toArray(): Type return new ConstantArrayType([], []); } + public function toArrayKey(): Type + { + return new ConstantStringType(''); + } + public function isOffsetAccessible(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -160,6 +190,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $array->setOffsetValueType($offsetType, $valueType, $unionValues); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { return $this; @@ -170,12 +205,57 @@ public function traverse(callable $cb): Type return $this; } - public function isArray(): TrinaryLogic + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getConstantScalarTypes(): array + { + return [$this]; + } + + public function getConstantScalarValues(): array + { + return [$this->getValue()]; + } + + public function isTrue(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function isOversizedArray(): TrinaryLogic + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -205,6 +285,45 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type instanceof ConstantScalarType) { + return LooseComparisonHelper::compareConstantScalars($this, $type, $phpVersion); + } + + if ($type->isConstantArray()->yes() && $type->isIterableAtLeastOnce()->no()) { + // @phpstan-ignore equal.alwaysTrue, equal.notAllowed + return new ConstantBooleanType($this->getValue() == []); // phpcs:ignore + } + + return new BooleanType(); + } + public function getSmallerType(): Type { return new NeverType(); @@ -241,6 +360,26 @@ public function getGreaterOrEqualType(): Type return new MixedType(); } + public function getFiniteTypes(): array + { + return [$this]; + } + + public function exponentiate(Type $exponent): Type + { + return new UnionType( + [ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ], + ); + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('null'); + } + /** * @param mixed[] $properties */ diff --git a/src/Type/ObjectShapePropertyReflection.php b/src/Type/ObjectShapePropertyReflection.php new file mode 100644 index 0000000000..a79f924417 --- /dev/null +++ b/src/Type/ObjectShapePropertyReflection.php @@ -0,0 +1,85 @@ +getClass(stdClass::class); + } + + public function isStatic(): bool + { + return false; + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function getDocComment(): ?string + { + return null; + } + + public function getReadableType(): Type + { + return $this->type; + } + + public function getWritableType(): Type + { + return new NeverType(); + } + + public function canChangeTypeAfterAssignment(): bool + { + return false; + } + + public function isReadable(): bool + { + return true; + } + + public function isWritable(): bool + { + return false; + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getDeprecatedDescription(): ?string + { + return null; + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + +} diff --git a/src/Type/ObjectShapeType.php b/src/Type/ObjectShapeType.php new file mode 100644 index 0000000000..17bffaa7bb --- /dev/null +++ b/src/Type/ObjectShapeType.php @@ -0,0 +1,536 @@ + $properties + * @param list $optionalProperties + */ + public function __construct(private array $properties, private array $optionalProperties) + { + } + + /** + * @return array + */ + public function getProperties(): array + { + return $this->properties; + } + + /** + * @return list + */ + public function getOptionalProperties(): array + { + return $this->optionalProperties; + } + + public function getReferencedClasses(): array + { + $classes = []; + foreach ($this->properties as $propertyType) { + foreach ($propertyType->getReferencedClasses() as $referencedClass) { + $classes[] = $referencedClass; + } + } + + return $classes; + } + + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function hasProperty(string $propertyName): TrinaryLogic + { + if (!array_key_exists($propertyName, $this->properties)) { + return TrinaryLogic::createNo(); + } + + if (in_array($propertyName, $this->optionalProperties, true)) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createYes(); + } + + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + { + return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); + } + + public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection + { + if (!array_key_exists($propertyName, $this->properties)) { + throw new ShouldNotHappenException(); + } + + $property = new ObjectShapePropertyReflection($this->properties[$propertyName]); + return new CallbackUnresolvedPropertyPrototypeReflection( + $property, + $property->getDeclaringClass(), + false, + static fn (Type $type): Type => $type, + ); + } + + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + { + if ($type instanceof CompoundType) { + return $type->isAcceptedWithReasonBy($this, $strictTypes); + } + + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + foreach ($type->getObjectClassReflections() as $classReflection) { + if (!UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( + $reflectionProvider, + Broker::getInstance()->getUniversalObjectCratesClasses(), + $classReflection, + )) { + continue; + } + + return AcceptsResult::createMaybe(); + } + + $result = AcceptsResult::createYes(); + $scope = new OutOfClassScope(); + foreach ($this->properties as $propertyName => $propertyType) { + $typeHasProperty = $type->hasProperty($propertyName); + $hasProperty = new AcceptsResult( + $typeHasProperty, + $typeHasProperty->yes() ? [] : [ + sprintf( + '%s %s have property $%s.', + $type->describe(VerbosityLevel::typeOnly()), + $typeHasProperty->no() ? 'does not' : 'might not', + $propertyName, + ), + ], + ); + if ($hasProperty->no()) { + if (in_array($propertyName, $this->optionalProperties, true)) { + continue; + } + return $hasProperty; + } + if ($hasProperty->maybe() && in_array($propertyName, $this->optionalProperties, true)) { + $hasProperty = AcceptsResult::createYes(); + } + + $result = $result->and($hasProperty); + + try { + $otherProperty = $type->getProperty($propertyName, $scope); + } catch (MissingPropertyFromReflectionException) { + return new AcceptsResult( + $result->result, + [ + sprintf( + '%s %s not have property $%s.', + $type->describe(VerbosityLevel::typeOnly()), + $result->no() ? 'does' : 'might', + $propertyName, + ), + ], + ); + } + if (!$otherProperty->isPublic()) { + return new AcceptsResult(TrinaryLogic::createNo(), [ + sprintf('Property %s::$%s is not public.', $otherProperty->getDeclaringClass()->getDisplayName(), $propertyName), + ]); + } + + if ($otherProperty->isStatic()) { + return new AcceptsResult(TrinaryLogic::createNo(), [ + sprintf('Property %s::$%s is static.', $otherProperty->getDeclaringClass()->getDisplayName(), $propertyName), + ]); + } + + if (!$otherProperty->isReadable()) { + return new AcceptsResult(TrinaryLogic::createNo(), [ + sprintf('Property %s::$%s is not readable.', $otherProperty->getDeclaringClass()->getDisplayName(), $propertyName), + ]); + } + + $otherPropertyType = $otherProperty->getReadableType(); + $verbosity = VerbosityLevel::getRecommendedLevelByType($propertyType, $otherPropertyType); + $acceptsValue = $propertyType->acceptsWithReason($otherPropertyType, $strictTypes)->decorateReasons( + static fn (string $reason) => sprintf( + 'Property ($%s) type %s does not accept type %s: %s', + $propertyName, + $propertyType->describe($verbosity), + $otherPropertyType->describe($verbosity), + $reason, + ), + ); + if (!$acceptsValue->yes() && count($acceptsValue->reasons) === 0) { + $acceptsValue = new AcceptsResult($acceptsValue->result, [ + sprintf( + 'Property ($%s) type %s does not accept type %s.', + $propertyName, + $propertyType->describe($verbosity), + $otherPropertyType->describe($verbosity), + ), + ]); + } + if ($acceptsValue->no()) { + return $acceptsValue; + } + $result = $result->and($acceptsValue); + } + + return $result->and(new AcceptsResult($type->isObject(), [])); + } + + public function isSuperTypeOf(Type $type): TrinaryLogic + { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + if ($type instanceof ObjectWithoutClassType) { + return TrinaryLogic::createMaybe(); + } + + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + foreach ($type->getObjectClassReflections() as $classReflection) { + if (!UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( + $reflectionProvider, + Broker::getInstance()->getUniversalObjectCratesClasses(), + $classReflection, + )) { + continue; + } + + return TrinaryLogic::createMaybe(); + } + + $result = TrinaryLogic::createYes(); + $scope = new OutOfClassScope(); + foreach ($this->properties as $propertyName => $propertyType) { + $hasProperty = $type->hasProperty($propertyName); + if ($hasProperty->no()) { + if (in_array($propertyName, $this->optionalProperties, true)) { + continue; + } + return $hasProperty; + } + if ($hasProperty->maybe() && in_array($propertyName, $this->optionalProperties, true)) { + $hasProperty = TrinaryLogic::createYes(); + } + + $result = $result->and($hasProperty); + + try { + $otherProperty = $type->getProperty($propertyName, $scope); + } catch (MissingPropertyFromReflectionException) { + return $result; + } + + if (!$otherProperty->isPublic()) { + return TrinaryLogic::createNo(); + } + + if ($otherProperty->isStatic()) { + return TrinaryLogic::createNo(); + } + + if (!$otherProperty->isReadable()) { + return TrinaryLogic::createNo(); + } + + $otherPropertyType = $otherProperty->getReadableType(); + $isSuperType = $propertyType->isSuperTypeOf($otherPropertyType); + if ($isSuperType->no()) { + return $isSuperType; + } + $result = $result->and($isSuperType); + } + + return $result->and($type->isObject()); + } + + public function equals(Type $type): bool + { + if (!$type instanceof self) { + return false; + } + + if (count($this->properties) !== count($type->properties)) { + return false; + } + + foreach ($this->properties as $name => $propertyType) { + if (!array_key_exists($name, $type->properties)) { + return false; + } + + if (!$propertyType->equals($type->properties[$name])) { + return false; + } + } + + if (count($this->optionalProperties) !== count($type->optionalProperties)) { + return false; + } + + foreach ($this->optionalProperties as $name) { + if (in_array($name, $type->optionalProperties, true)) { + continue; + } + + return false; + } + + return true; + } + + public function tryRemove(Type $typeToRemove): ?Type + { + if ($typeToRemove instanceof HasPropertyType) { + $properties = $this->properties; + unset($properties[$typeToRemove->getPropertyName()]); + $optionalProperties = array_values(array_filter($this->optionalProperties, static fn (string $propertyName) => $propertyName !== $typeToRemove->getPropertyName())); + + return new self($properties, $optionalProperties); + } + + return null; + } + + public function makePropertyRequired(string $propertyName): self + { + if (array_key_exists($propertyName, $this->properties)) { + $optionalProperties = array_values(array_filter($this->optionalProperties, static fn (string $currentPropertyName) => $currentPropertyName !== $propertyName)); + + return new self($this->properties, $optionalProperties); + } + + return $this; + } + + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap + { + if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) { + return $receivedType->inferTemplateTypesOn($this); + } + + if ($receivedType instanceof self) { + $typeMap = TemplateTypeMap::createEmpty(); + $scope = new OutOfClassScope(); + foreach ($this->properties as $name => $propertyType) { + if ($receivedType->hasProperty($name)->no()) { + continue; + } + + try { + $receivedProperty = $receivedType->getProperty($name, $scope); + } catch (MissingPropertyFromReflectionException) { + continue; + } + if (!$receivedProperty->isPublic()) { + continue; + } + if ($receivedProperty->isStatic()) { + continue; + } + $receivedPropertyType = $receivedProperty->getReadableType(); + $typeMap = $typeMap->union($propertyType->inferTemplateTypes($receivedPropertyType)); + } + + return $typeMap; + } + + return TemplateTypeMap::createEmpty(); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant()); + $references = []; + foreach ($this->properties as $propertyType) { + foreach ($propertyType->getReferencedTemplateTypes($variance) as $reference) { + $references[] = $reference; + } + } + + return $references; + } + + public function describe(VerbosityLevel $level): string + { + $callback = function () use ($level): string { + $items = []; + foreach ($this->properties as $name => $propertyType) { + $optional = in_array($name, $this->optionalProperties, true); + $items[] = sprintf('%s%s: %s', $name, $optional ? '?' : '', $propertyType->describe($level)); + } + return sprintf('object{%s}', implode(', ', $items)); + }; + return $level->handle( + $callback, + $callback, + ); + } + + public function getEnumCases(): array + { + return []; + } + + public function traverse(callable $cb): Type + { + $properties = []; + $stillOriginal = true; + + foreach ($this->properties as $name => $propertyType) { + $transformed = $cb($propertyType); + if ($transformed !== $propertyType) { + $stillOriginal = false; + } + + $properties[$name] = $transformed; + } + + if ($stillOriginal) { + return $this; + } + + return new self($properties, $this->optionalProperties); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right->isObject()->yes()) { + return $this; + } + + $properties = []; + $stillOriginal = true; + + $scope = new OutOfClassScope(); + foreach ($this->properties as $name => $propertyType) { + if (!$right->hasProperty($name)->yes()) { + return $this; + } + $transformed = $cb($propertyType, $right->getProperty($name, $scope)->getReadableType()); + if ($transformed !== $propertyType) { + $stillOriginal = false; + } + + $properties[$name] = $transformed; + } + + if ($stillOriginal) { + return $this; + } + + return new self($properties, $this->optionalProperties); + } + + public function exponentiate(Type $exponent): Type + { + if (!$exponent instanceof NeverType && !$this->isSuperTypeOf($exponent)->no()) { + return TypeCombinator::union($this, $exponent); + } + + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + $items = []; + foreach ($this->properties as $name => $type) { + if (ConstantArrayType::isValidIdentifier($name)) { + $keyNode = new IdentifierTypeNode($name); + } else { + $keyPhpDocNode = (new ConstantStringType($name))->toPhpDocNode(); + if (!$keyPhpDocNode instanceof ConstTypeNode) { + continue; + } + + /** @var ConstExprStringNode $keyNode */ + $keyNode = $keyPhpDocNode->constExpr; + } + $items[] = new ObjectShapeItemNode( + $keyNode, + in_array($name, $this->optionalProperties, true), + $type->toPhpDocNode(), + ); + } + + return new ObjectShapeNode($items); + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): Type + { + return new self($properties['properties'], $properties['optionalProperties']); + } + +} diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 7a78eafd2d..6bba05fa01 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -4,19 +4,26 @@ use ArrayAccess; use Closure; +use Countable; use DateTime; use DateTimeImmutable; use DateTimeInterface; +use Error; +use Exception; use Iterator; use IteratorAggregate; use PHPStan\Analyser\OutOfClassScope; use PHPStan\Broker\Broker; use PHPStan\Broker\ClassNotFoundException; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; +use PHPStan\Reflection\Callables\FunctionCallableVariant; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension; use PHPStan\Reflection\PropertyReflection; @@ -24,6 +31,7 @@ use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\CalledOnTypeUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\CalledOnTypeUnresolvedPropertyPrototypeReflection; +use PHPStan\Reflection\Type\UnionTypeUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; @@ -33,14 +41,16 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\TemplateTypeHelper; +use PHPStan\Type\Traits\MaybeIterableTypeTrait; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +use Throwable; use Traversable; use function array_key_exists; -use function array_keys; use function array_map; -use function array_merge; use function array_values; use function count; use function implode; @@ -52,6 +62,8 @@ class ObjectType implements TypeWithClassName, SubtractableType { + use MaybeIterableTypeTrait; + use NonArrayTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonTypeTrait; use NonGeneralizableTypeTrait; @@ -80,6 +92,8 @@ class ObjectType implements TypeWithClassName, SubtractableType /** @var array */ private array $currentAncestors = []; + private ?string $cachedDescription = null; + /** @api */ public function __construct( private string $className, @@ -111,6 +125,9 @@ private static function createFromReflection(ClassReflection $reflection): self return new GenericObjectType( $reflection->getName(), $reflection->typeMapToList($reflection->getActiveTemplateTypeMap()), + null, + null, + $reflection->varianceMapToList($reflection->getCallSiteVarianceMap()), ); } @@ -134,6 +151,10 @@ public function hasProperty(string $propertyName): TrinaryLogic return TrinaryLogic::createMaybe(); } + if (!$classReflection->isFinal()) { + return TrinaryLogic::createMaybe(); + } + return TrinaryLogic::createNo(); } @@ -160,6 +181,26 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember throw new ClassNotFoundException($this->className); } + if ($nakedClassReflection->isEnum()) { + if ( + $propertyName === 'name' + || ($propertyName === 'value' && $nakedClassReflection->isBackedEnum()) + ) { + $properties = []; + foreach ($this->getEnumCases() as $enumCase) { + $properties[] = $enumCase->getUnresolvedPropertyPrototype($propertyName, $scope); + } + + if (count($properties) > 0) { + if (count($properties) === 1) { + return $properties[0]; + } + + return new UnionTypeUnresolvedPropertyPrototypeReflection($propertyName, $properties); + } + } + } + if (!$nakedClassReflection->hasNativeProperty($propertyName)) { $nakedClassReflection = $this->getClassReflection(); } @@ -172,7 +213,7 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember $ancestor = $this->getAncestorWithClassName($property->getDeclaringClass()->getName()); $resolvedClassReflection = null; - if ($ancestor !== null) { + if ($ancestor !== null && $ancestor->hasProperty($propertyName)->yes()) { $resolvedClassReflection = $ancestor->getClassReflection(); if ($ancestor !== $this) { $property = $ancestor->getUnresolvedPropertyPrototype($propertyName, $scope)->getNakedProperty(); @@ -216,34 +257,60 @@ public function getReferencedClasses(): array return [$this->className]; } + public function getObjectClassNames(): array + { + return [$this->className]; + } + + public function getObjectClassReflections(): array + { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return []; + } + + return [$classReflection]; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof StaticType) { return $this->checkSubclassAcceptability($type->getClassName()); } if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } if ($type instanceof ClosureType) { - return $this->isInstanceOf(Closure::class); + return new AcceptsResult($this->isInstanceOf(Closure::class), []); } if ($type instanceof ObjectWithoutClassType) { - return TrinaryLogic::createMaybe(); + return AcceptsResult::createMaybe(); } - if (!$type instanceof TypeWithClassName) { - return TrinaryLogic::createNo(); + $thatClassNames = $type->getObjectClassNames(); + if (count($thatClassNames) > 1) { + throw new ShouldNotHappenException(); } - return $this->checkSubclassAcceptability($type->getClassName()); + if ($thatClassNames === []) { + return AcceptsResult::createNo(); + } + + return $this->checkSubclassAcceptability($thatClassNames[0]); } public function isSuperTypeOf(Type $type): TrinaryLogic { - if (!$type instanceof CompoundType && !$type instanceof TypeWithClassName && !$type instanceof ObjectWithoutClassType) { + $thatClassNames = $type->getObjectClassNames(); + if (!$type instanceof CompoundType && $thatClassNames === [] && !$type instanceof ObjectWithoutClassType) { return TrinaryLogic::createNo(); } @@ -263,6 +330,10 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return self::$superTypes[$thisDescription][$description] = $type->isSubTypeOf($this); } + if ($type instanceof ClosureType) { + return self::$superTypes[$thisDescription][$description] = $this->isInstanceOf(Closure::class); + } + if ($type instanceof ObjectWithoutClassType) { if ($type->getSubtractedType() !== null) { $isSuperType = $type->getSubtractedType()->isSuperTypeOf($this); @@ -295,20 +366,22 @@ public function isSuperTypeOf(Type $type): TrinaryLogic } $thisClassName = $this->className; - $thatClassName = $type->getClassName(); + if (count($thatClassNames) > 1) { + throw new ShouldNotHappenException(); + } - if ($thatClassName === $thisClassName) { + if ($thatClassNames[0] === $thisClassName) { return $transformResult(TrinaryLogic::createYes()); } $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + $thisClassReflection = $this->getClassReflection(); - if ($this->getClassReflection() === null || !$reflectionProvider->hasClass($thatClassName)) { + if ($thisClassReflection === null || !$reflectionProvider->hasClass($thatClassNames[0])) { return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe(); } - $thisClassReflection = $this->getClassReflection(); - $thatClassReflection = $reflectionProvider->getClass($thatClassName); + $thatClassReflection = $reflectionProvider->getClass($thatClassNames[0]); if ($thisClassReflection->isTrait() || $thatClassReflection->isTrait()) { return TrinaryLogic::createNo(); @@ -322,7 +395,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return self::$superTypes[$thisDescription][$description] = $transformResult(TrinaryLogic::createYes()); } - if ($thisClassReflection->isSubclassOf($thatClassName)) { + if ($thisClassReflection->isSubclassOf($thatClassNames[0])) { return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe(); } @@ -352,11 +425,7 @@ public function equals(Type $type): bool } if ($this->subtractedType === null) { - if ($type->subtractedType === null) { - return true; - } - - return false; + return $type->subtractedType === null; } if ($type->subtractedType === null) { @@ -366,16 +435,16 @@ public function equals(Type $type): bool return $this->subtractedType->equals($type->subtractedType); } - private function checkSubclassAcceptability(string $thatClass): TrinaryLogic + private function checkSubclassAcceptability(string $thatClass): AcceptsResult { if ($this->className === $thatClass) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); if ($this->getClassReflection() === null || !$reflectionProvider->hasClass($thatClass)) { - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } $thisReflection = $this->getClassReflection(); @@ -383,17 +452,17 @@ private function checkSubclassAcceptability(string $thatClass): TrinaryLogic if ($thisReflection->getName() === $thatReflection->getName()) { // class alias - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } if ($thisReflection->isInterface() && $thatReflection->isInterface()) { - return TrinaryLogic::createFromBoolean( - $thatReflection->implementsInterface($this->className), + return AcceptsResult::createFromBoolean( + $thatReflection->implementsInterface($thisReflection->getName()), ); } - return TrinaryLogic::createFromBoolean( - $thatReflection->isSubclassOf($this->className), + return AcceptsResult::createFromBoolean( + $thatReflection->isSubclassOf($thisReflection->getName()), ); } @@ -442,8 +511,12 @@ protected function describeAdditionalCacheKey(): string private function describeCache(): string { + if ($this->cachedDescription !== null) { + return $this->cachedDescription; + } + if (static::class !== self::class) { - return $this->describe(VerbosityLevel::cache()); + return $this->cachedDescription = $this->describe(VerbosityLevel::cache()); } $description = $this->className; @@ -468,7 +541,7 @@ private function describeCache(): string $description .= '-'; } - return $description; + return $this->cachedDescription = $description; } public function toNumber(): Type @@ -579,6 +652,11 @@ public function toArray(): Type return new ConstantArrayType($arrayKeys, $arrayValues); } + public function toArrayKey(): Type + { + return $this->toString(); + } + public function toBoolean(): BooleanType { if ($this->isInstanceOf('SimpleXMLElement')->yes()) { @@ -588,6 +666,21 @@ public function toBoolean(): BooleanType return new ConstantBooleanType(true); } + public function isObject(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isEnum(): TrinaryLogic + { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createFromBoolean($classReflection->isEnum()); + } + public function canAccessProperties(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -620,7 +713,7 @@ public function hasMethod(string $methodName): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -699,6 +792,46 @@ public function getConstant(string $constantName): ConstantReflection return $class->getConstant($constantName); } + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return new ErrorType(); + } + + $ancestorClassReflection = $classReflection->getAncestorWithClassName($ancestorClassName); + if ($ancestorClassReflection === null) { + return new ErrorType(); + } + + $activeTemplateTypeMap = $ancestorClassReflection->getPossiblyIncompleteActiveTemplateTypeMap(); + $type = $activeTemplateTypeMap->getType($templateTypeName); + if ($type === null) { + return new ErrorType(); + } + if ($type instanceof ErrorType) { + $templateTypeMap = $ancestorClassReflection->getTemplateTypeMap(); + $templateType = $templateTypeMap->getType($templateTypeName); + if ($templateType === null) { + return $type; + } + + $bound = TemplateTypeHelper::resolveToBounds($templateType); + if ($bound instanceof MixedType && $bound->isExplicitMixed()) { + return new MixedType(false); + } + + return $bound; + } + + return $type; + } + + public function getConstantStrings(): array + { + return []; + } + public function isIterable(): TrinaryLogic { return $this->isInstanceOf(Traversable::class); @@ -710,6 +843,15 @@ public function isIterableAtLeastOnce(): TrinaryLogic ->and(TrinaryLogic::createMaybe()); } + public function getArraySize(): Type + { + if ($this->isInstanceOf(Countable::class)->no()) { + return new ErrorType(); + } + + return IntegerRangeType::fromInterval(0, null); + } + public function getIterableKeyType(): Type { $isTraversable = false; @@ -724,10 +866,10 @@ public function getIterableKeyType(): Type } $extraOffsetAccessible = $this->isExtraOffsetAccessibleClass()->yes(); - if ($this->isInstanceOf(Traversable::class)->yes() && !$extraOffsetAccessible) { + if (!$extraOffsetAccessible && $this->isInstanceOf(Traversable::class)->yes()) { $isTraversable = true; - $tKey = GenericTypeVariableResolver::getType($this, Traversable::class, 'TKey'); - if ($tKey !== null) { + $tKey = $this->getTemplateType(Traversable::class, 'TKey'); + if (!$tKey instanceof ErrorType) { if (!$tKey instanceof MixedType || $tKey->isExplicitMixed()) { return $tKey; } @@ -751,6 +893,16 @@ public function getIterableKeyType(): Type return new ErrorType(); } + public function getFirstIterableKeyType(): Type + { + return $this->getIterableKeyType(); + } + + public function getLastIterableKeyType(): Type + { + return $this->getIterableKeyType(); + } + public function getIterableValueType(): Type { $isTraversable = false; @@ -765,10 +917,10 @@ public function getIterableValueType(): Type } $extraOffsetAccessible = $this->isExtraOffsetAccessibleClass()->yes(); - if ($this->isInstanceOf(Traversable::class)->yes() && !$extraOffsetAccessible) { + if (!$extraOffsetAccessible && $this->isInstanceOf(Traversable::class)->yes()) { $isTraversable = true; - $tValue = GenericTypeVariableResolver::getType($this, Traversable::class, 'TValue'); - if ($tValue !== null) { + $tValue = $this->getTemplateType(Traversable::class, 'TValue'); + if (!$tValue instanceof ErrorType) { if (!$tValue instanceof MixedType || $tValue->isExplicitMixed()) { return $tValue; } @@ -792,12 +944,62 @@ public function getIterableValueType(): Type return new ErrorType(); } - public function isArray(): TrinaryLogic + public function getFirstIterableValueType(): Type + { + return $this->getIterableValueType(); + } + + public function getLastIterableValueType(): Type + { + return $this->getIterableValueType(); + } + + public function isNull(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function isOversizedArray(): TrinaryLogic + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -827,6 +1029,42 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this; + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type->isTrue()->yes()) { + return new ConstantBooleanType(true); + } + + return $type->isFalse()->yes() + ? new ConstantBooleanType(false) + : new BooleanType(); + } + private function isExtraOffsetAccessibleClass(): TrinaryLogic { $classReflection = $this->getClassReflection(); @@ -943,6 +1181,15 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + if ($this->isOffsetAccessible()->no()) { + return new ErrorType(); + } + + return $this; + } + public function unsetOffset(Type $offsetType): Type { if ($this->isOffsetAccessible()->no()) { @@ -952,6 +1199,36 @@ public function unsetOffset(Type $offsetType): Type return $this; } + public function getEnumCases(): array + { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return []; + } + + if (!$classReflection->isEnum()) { + return []; + } + + $subtracted = []; + if ($this->subtractedType !== null) { + foreach ($this->subtractedType->getEnumCases() as $enumCase) { + $subtracted[$enumCase->getEnumCaseName()] = true; + } + } + + $cases = []; + $className = $classReflection->getName(); + foreach ($classReflection->getEnumCases() as $enumCase) { + if (array_key_exists($enumCase->getName(), $subtracted)) { + continue; + } + $cases[] = new EnumCaseObjectType($className, $enumCase->getName(), $classReflection); + } + + return $cases; + } + public function isCallable(): TrinaryLogic { $parametersAcceptors = $this->findCallableParametersAcceptors(); @@ -969,13 +1246,10 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createYes(); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { if ($this->className === Closure::class) { - return [new TrivialParametersAcceptor()]; + return [new TrivialParametersAcceptor('Closure')]; } $parametersAcceptors = $this->findCallableParametersAcceptors(); if ($parametersAcceptors === null) { @@ -986,7 +1260,7 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) } /** - * @return ParametersAcceptor[]|null + * @return CallableParametersAcceptor[]|null */ private function findCallableParametersAcceptors(): ?array { @@ -996,7 +1270,11 @@ private function findCallableParametersAcceptors(): ?array } if ($classReflection->hasNativeMethod('__invoke')) { - return $this->getMethod('__invoke', new OutOfClassScope())->getVariants(); + $method = $this->getMethod('__invoke', new OutOfClassScope()); + return FunctionCallableVariant::createFromVariants( + $method, + $method->getVariants(), + ); } if (!$classReflection->getNativeReflection()->isFinal()) { @@ -1029,10 +1307,18 @@ public function isInstanceOf(string $className): TrinaryLogic return TrinaryLogic::createMaybe(); } - if ($classReflection->isSubclassOf($className) || $classReflection->getName() === $className) { + if ($classReflection->getName() === $className || $classReflection->isSubclassOf($className)) { return TrinaryLogic::createYes(); } + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if ($reflectionProvider->hasClass($className)) { + $thatClassReflection = $reflectionProvider->getClass($className); + if ($thatClassReflection->isFinal()) { + return TrinaryLogic::createNo(); + } + } + if ($classReflection->isInterface()) { return TrinaryLogic::createMaybe(); } @@ -1058,55 +1344,53 @@ public function changeSubtractedType(?Type $subtractedType): Type { if ($subtractedType !== null) { $classReflection = $this->getClassReflection(); - if ($classReflection !== null && $classReflection->isEnum()) { - $cases = []; - foreach (array_keys($classReflection->getEnumCases()) as $name) { - $cases[$name] = new EnumCaseObjectType($classReflection->getName(), $name); + $allowedSubTypesList = $classReflection !== null ? $classReflection->getAllowedSubTypes() : null; + if ($allowedSubTypesList !== null) { + $allowedSubTypes = []; + foreach ($allowedSubTypesList as $allowedSubType) { + $allowedSubTypes[$allowedSubType->describe(VerbosityLevel::precise())] = $allowedSubType; } - $originalCases = $cases; + $originalAllowedSubTypes = $allowedSubTypes; + $subtractedSubTypes = []; - $subtractedTypes = TypeUtils::flattenTypes($subtractedType); - if ($this->subtractedType !== null) { - $subtractedTypes = array_merge($subtractedTypes, TypeUtils::flattenTypes($this->subtractedType)); + $subtractedTypesList = TypeUtils::flattenTypes($subtractedType); + $subtractedTypes = []; + foreach ($subtractedTypesList as $type) { + $subtractedTypes[$type->describe(VerbosityLevel::precise())] = $type; } - $subtractedCases = []; - foreach ($subtractedTypes as $subType) { - if (!$subType instanceof EnumCaseObjectType) { - return new self($this->className, $subtractedType); - } - - if ($subType->getClassName() !== $this->getClassName()) { - return new self($this->className, $subtractedType); - } - if (!array_key_exists($subType->getEnumCaseName(), $cases)) { - return new self($this->className, $subtractedType); + foreach ($subtractedTypes as $subType) { + foreach ($allowedSubTypes as $description => $allowedSubType) { + if ($subType->equals($allowedSubType)) { + $subtractedSubTypes[$description] = $subType; + unset($allowedSubTypes[$description]); + continue 2; + } } - $subtractedCases[$subType->getEnumCaseName()] = $subType; - unset($originalCases[$subType->getEnumCaseName()]); + return new self($this->className, $subtractedType); } - if (count($originalCases) === 1) { - return array_values($originalCases)[0]; + if (count($allowedSubTypes) === 1) { + return array_values($allowedSubTypes)[0]; } - $subtractedCases = array_values($subtractedCases); - $subtractedCasesCount = count($subtractedCases); - if ($subtractedCasesCount === count($cases)) { + $subtractedSubTypes = array_values($subtractedSubTypes); + $subtractedSubTypesCount = count($subtractedSubTypes); + if ($subtractedSubTypesCount === count($originalAllowedSubTypes)) { return new NeverType(); } - if ($subtractedCasesCount === 0) { + if ($subtractedSubTypesCount === 0) { return new self($this->className); } - if (count($subtractedCases) === 1) { - return new self($this->className, $subtractedCases[0]); + if ($subtractedSubTypesCount === 1) { + return new self($this->className, $subtractedSubTypes[0]); } - return new self($this->className, new UnionType($subtractedCases)); + return new self($this->className, new UnionType($subtractedSubTypes)); } } @@ -1136,6 +1420,15 @@ public function traverse(callable $cb): Type return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if ($this->subtractedType === null) { + return $this; + } + + return new self($this->className); + } + public function getNakedClassReflection(): ?ClassReflection { if ($this->classReflection !== null) { @@ -1270,6 +1563,16 @@ public function tryRemove(Type $typeToRemove): ?Type } } + if ($this->getClassName() === Throwable::class) { + if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === Error::class) { + return new ObjectType(Exception::class); // phpcs:ignore SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly.ReferencedGeneralException + } + + if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === Exception::class) { // phpcs:ignore SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly.ReferencedGeneralException + return new ObjectType(Error::class); + } + } + if ($this->isSuperTypeOf($typeToRemove)->yes()) { return $this->subtract($typeToRemove); } @@ -1277,4 +1580,23 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } + public function getFiniteTypes(): array + { + return $this->getEnumCases(); + } + + public function exponentiate(Type $exponent): Type + { + $object = new ObjectWithoutClassType(); + if (!$exponent instanceof NeverType && !$object->isSuperTypeOf($this)->no() && !$object->isSuperTypeOf($exponent)->no()) { + return TypeCombinator::union($this, $exponent); + } + return new ErrorType(); + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode($this->getClassName()); + } + } diff --git a/src/Type/ObjectWithoutClassType.php b/src/Type/ObjectWithoutClassType.php index ffac3045bb..51a3d07937 100644 --- a/src/Type/ObjectWithoutClassType.php +++ b/src/Type/ObjectWithoutClassType.php @@ -2,6 +2,8 @@ namespace PHPStan\Type; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; @@ -40,14 +42,29 @@ public function getReferencedClasses(): array return []; } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return TrinaryLogic::createFromBoolean( - $type instanceof self || $type instanceof TypeWithClassName, + return AcceptsResult::createFromBoolean( + $type instanceof self || $type instanceof ObjectShapeType || $type->getObjectClassNames() !== [], ); } @@ -71,15 +88,19 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return TrinaryLogic::createMaybe(); } - if ($type instanceof TypeWithClassName) { - if ($this->subtractedType === null) { - return TrinaryLogic::createYes(); - } + if ($type instanceof ObjectShapeType) { + return TrinaryLogic::createYes(); + } + + if ($type->getObjectClassNames() === []) { + return TrinaryLogic::createNo(); + } - return $this->subtractedType->isSuperTypeOf($type)->negate(); + if ($this->subtractedType === null) { + return TrinaryLogic::createYes(); } - return TrinaryLogic::createNo(); + return $this->subtractedType->isSuperTypeOf($type)->negate(); } public function equals(Type $type): bool @@ -119,6 +140,11 @@ function () use ($level): string { ); } + public function getEnumCases(): array + { + return []; + } + public function subtract(Type $type): Type { if ($type instanceof self) { @@ -157,6 +183,15 @@ public function traverse(callable $cb): Type return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if ($this->subtractedType === null) { + return $this; + } + + return new self(); + } + public function tryRemove(Type $typeToRemove): ?Type { if ($this->isSuperTypeOf($typeToRemove)->yes()) { @@ -166,6 +201,28 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } + public function exponentiate(Type $exponent): Type + { + if (!$exponent instanceof NeverType && !$this->isSuperTypeOf($exponent)->no()) { + return TypeCombinator::union($this, $exponent); + } + + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('object'); + } + /** * @param mixed[] $properties */ diff --git a/src/Type/OffsetAccessType.php b/src/Type/OffsetAccessType.php index 6edeb55c9b..430d38332c 100644 --- a/src/Type/OffsetAccessType.php +++ b/src/Type/OffsetAccessType.php @@ -2,11 +2,13 @@ namespace PHPStan\Type; +use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\PhpDocParser\Printer\Printer; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Traits\LateResolvableTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use function array_merge; -use function sprintf; /** @api */ final class OffsetAccessType implements CompoundType, LateResolvableType @@ -30,6 +32,16 @@ public function getReferencedClasses(): array ); } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array { return array_merge( @@ -47,11 +59,9 @@ public function equals(Type $type): bool public function describe(VerbosityLevel $level): string { - return sprintf( - '%s[%s]', - $this->type->describe($level), - $this->offset->describe($level), - ); + $printer = new Printer(); + + return $printer->print($this->toPhpDocNode()); } public function isResolvable(): bool @@ -77,7 +87,31 @@ public function traverse(callable $cb): Type return $this; } - return new OffsetAccessType($type, $offset); + return new self($type, $offset); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } + + $type = $cb($this->type, $right->type); + $offset = $cb($this->offset, $right->offset); + + if ($this->type === $type && $this->offset === $offset) { + return $this; + } + + return new self($type, $offset); + } + + public function toPhpDocNode(): TypeNode + { + return new OffsetAccessTypeNode( + $this->type->toPhpDocNode(), + $this->offset->toPhpDocNode(), + ); } /** diff --git a/src/Type/OperatorTypeSpecifyingExtension.php b/src/Type/OperatorTypeSpecifyingExtension.php index dc26ce3633..a9d2fbe129 100644 --- a/src/Type/OperatorTypeSpecifyingExtension.php +++ b/src/Type/OperatorTypeSpecifyingExtension.php @@ -2,7 +2,25 @@ namespace PHPStan\Type; -/** @api */ +/** + * This is the extension interface to implement if you want to describe + * how arithmetic operators like +, -, *, ^, / should infer types + * for PHP extensions that overload the behaviour, like GMP. + * + * To register it in the configuration file use the `phpstan.broker.operatorTypeSpecifyingExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.broker.operatorTypeSpecifyingExtension + * ``` + * + * Learn more: https://github.com/phpstan/phpstan/pull/2114 + * + * @api + */ interface OperatorTypeSpecifyingExtension { diff --git a/src/Type/ParserNodeTypeToPHPStanType.php b/src/Type/ParserNodeTypeToPHPStanType.php index 7d906b459f..7299a08100 100644 --- a/src/Type/ParserNodeTypeToPHPStanType.php +++ b/src/Type/ParserNodeTypeToPHPStanType.php @@ -53,7 +53,7 @@ public static function resolve($type, ?ClassReflection $classReflection): Type $types = []; foreach ($type->types as $intersectionTypeType) { $innerType = self::resolve($intersectionTypeType, $classReflection); - if (!$innerType instanceof ObjectType) { + if (!$innerType->isObject()->yes()) { return new NeverType(); } @@ -93,7 +93,7 @@ public static function resolve($type, ?ClassReflection $classReflection): Type } elseif ($type === 'mixed') { return new MixedType(true); } elseif ($type === 'never') { - return new NeverType(true); + return new NonAcceptingNeverType(); } return new MixedType(); diff --git a/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php b/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php index 00a4cabed3..c4ab9e8b9c 100644 --- a/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php @@ -5,19 +5,18 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_key_exists; class ArgumentBasedFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - /** @var int[] */ - private array $functionNames = [ + private const FUNCTION_NAMES = [ 'array_unique' => 0, 'array_change_key_case' => 0, 'array_diff_assoc' => 0, @@ -39,15 +38,15 @@ class ArgumentBasedFunctionReturnTypeExtension implements DynamicFunctionReturnT public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return isset($this->functionNames[$functionReflection->getName()]); + return array_key_exists($functionReflection->getName(), self::FUNCTION_NAMES); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $argumentPosition = $this->functionNames[$functionReflection->getName()]; + $argumentPosition = self::FUNCTION_NAMES[$functionReflection->getName()]; if (!isset($functionCall->getArgs()[$argumentPosition])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argument = $functionCall->getArgs()[$argumentPosition]; diff --git a/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php b/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php index 41e4dceda4..1c8530ec3b 100644 --- a/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php @@ -4,24 +4,31 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; -use PHPStan\Type\IntersectionType; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeTraverser; -use PHPStan\Type\UnionType; use function count; final class ArrayChunkFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + private const FINITE_TYPES_LIMIT = 5; + + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_chunk'; @@ -35,31 +42,67 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($functionCall->getArgs()[0]->value); $lengthType = $scope->getType($functionCall->getArgs()[1]->value); - $preserveKeysType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null; - $preserveKeys = $preserveKeysType instanceof ConstantBooleanType ? $preserveKeysType->getValue() : false; + if (isset($functionCall->getArgs()[2])) { + $preserveKeysType = $scope->getType($functionCall->getArgs()[2]->value); + $preserveKeys = $preserveKeysType instanceof ConstantBooleanType ? $preserveKeysType->getValue() : null; + } else { + $preserveKeys = false; + } - if (!$arrayType->isArray()->yes() || !$lengthType instanceof ConstantIntegerType || $lengthType->getValue() < 1) { + $negativeOrZero = IntegerRangeType::fromInterval(null, 0); + if ($negativeOrZero->isSuperTypeOf($lengthType)->yes()) { + return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new NullType(); + } + + if (!$arrayType->isArray()->yes()) { return null; } - return TypeTraverser::map($arrayType, static function (Type $type, callable $traverse) use ($lengthType, $preserveKeys): Type { - if ($type instanceof UnionType || $type instanceof IntersectionType) { - return $traverse($type); - } + if ($preserveKeys !== null) { + $constantArrays = $arrayType->getConstantArrays(); + $biggerOne = IntegerRangeType::fromInterval(1, null); + $finiteTypes = $lengthType->getFiniteTypes(); + if (count($constantArrays) > 0 + && $biggerOne->isSuperTypeOf($lengthType)->yes() + && count($finiteTypes) < self::FINITE_TYPES_LIMIT + ) { + $results = []; + foreach ($constantArrays as $constantArray) { + foreach ($finiteTypes as $finiteType) { + if (!$finiteType instanceof ConstantIntegerType || $finiteType->getValue() < 1) { + return null; + } + + $results[] = $constantArray->chunk($finiteType->getValue(), $preserveKeys); + } + } - if ($type instanceof ConstantArrayType) { - return $type->chunk($lengthType->getValue(), $preserveKeys); + return TypeCombinator::union(...$results); } + } - $chunkType = $preserveKeys ? $type : new ArrayType(new IntegerType(), $type->getIterableValueType()); - $chunkType = TypeCombinator::intersect($chunkType, new NonEmptyArrayType()); + $chunkType = self::getChunkType($arrayType, $preserveKeys); - $resultType = new ArrayType(new IntegerType(), $chunkType); - if ($type->isIterableAtLeastOnce()->yes()) { - $resultType = TypeCombinator::intersect($type, new NonEmptyArrayType()); - } - return $resultType; - }); + $resultType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $chunkType)); + if ($arrayType->isIterableAtLeastOnce()->yes()) { + $resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType()); + } + + return $resultType; + } + + private static function getChunkType(Type $type, ?bool $preserveKeys): Type + { + if ($preserveKeys === null) { + $chunkType = new ArrayType(TypeCombinator::union($type->getIterableKeyType(), new IntegerType()), $type->getIterableValueType()); + } elseif ($preserveKeys) { + $chunkType = $type; + } else { + $chunkType = new ArrayType(new IntegerType(), $type->getIterableValueType()); + $chunkType = AccessoryArrayListType::intersectWith($chunkType); + } + + return TypeCombinator::intersect($chunkType, new NonEmptyArrayType()); } } diff --git a/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php b/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php index 8359241a9f..a7edf8777c 100644 --- a/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php @@ -6,9 +6,9 @@ use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; @@ -17,10 +17,8 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; -use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; class ArrayColumnFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -35,18 +33,18 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_column'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { $numArgs = count($functionCall->getArgs()); if ($numArgs < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $arrayType = $scope->getType($functionCall->getArgs()[0]->value); $columnType = $scope->getType($functionCall->getArgs()[1]->value); $indexType = $numArgs >= 3 ? $scope->getType($functionCall->getArgs()[2]->value) : null; - $constantArrayTypes = TypeUtils::getOldConstantArrays($arrayType); + $constantArrayTypes = $arrayType->getConstantArrays(); if (count($constantArrayTypes) === 1) { $type = $this->handleConstantArray($constantArrayTypes[0], $columnType, $indexType, $scope); if ($type !== null) { @@ -100,6 +98,9 @@ private function handleAnyArray(Type $arrayType, Type $columnType, ?Type $indexT if ($iterableAtLeastOnce->yes()) { $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); } + if ($indexType === null) { + $returnType = AccessoryArrayListType::intersectWith($returnType); + } return $returnType; } @@ -113,6 +114,9 @@ private function handleConstantArray(ConstantArrayType $arrayType, Type $columnT if ($valueType === null) { return null; } + if ($valueType instanceof NeverType) { + continue; + } if ($indexType !== null) { $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, false); @@ -141,7 +145,7 @@ private function handleConstantArray(ConstantArrayType $arrayType, Type $columnT private function getOffsetOrProperty(Type $type, Type $offsetOrProperty, Scope $scope, bool $allowMaybe): ?Type { - $offsetIsNull = (new NullType())->isSuperTypeOf($offsetOrProperty); + $offsetIsNull = $offsetOrProperty->isNull(); if ($offsetIsNull->yes()) { return $type; } @@ -153,7 +157,7 @@ private function getOffsetOrProperty(Type $type, Type $offsetOrProperty, Scope $ } if (!$type->canAccessProperties()->no()) { - $propertyTypes = TypeUtils::getConstantStrings($offsetOrProperty); + $propertyTypes = $offsetOrProperty->getConstantStrings(); if ($propertyTypes === []) { return new MixedType(); } @@ -195,10 +199,10 @@ private function castToArrayKeyType(Type $type): Type return $this->phpVersion->throwsTypeErrorForInternalFunctions() ? new NeverType() : new IntegerType(); } if ($isArray->no()) { - return ArrayType::castToArrayKeyType($type); + return $type->toArrayKey(); } $withoutArrayType = TypeCombinator::remove($type, new ArrayType(new MixedType(), new MixedType())); - $keyType = ArrayType::castToArrayKeyType($withoutArrayType); + $keyType = $withoutArrayType->toArrayKey(); if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return $keyType; } diff --git a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php index c127bf408b..62e1d8435b 100644 --- a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php @@ -7,7 +7,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; @@ -16,7 +15,6 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\ErrorType; -use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; @@ -36,10 +34,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_combine'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $firstArg = $functionCall->getArgs()[0]->value; @@ -56,6 +54,9 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $valueTypes = $valuesParamType->getValueTypes(); if (count($keyTypes) !== count($valueTypes)) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } return new ConstantBooleanType(false); } @@ -72,7 +73,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($keysParamType->isArray()->yes()) { $itemType = $keysParamType->getIterableValueType(); - if ((new IntegerType())->isSuperTypeOf($itemType)->no()) { + if ($itemType->isInteger()->no()) { if ($itemType->toString() instanceof ErrorType) { return new NeverType(); } @@ -115,7 +116,7 @@ private function sanitizeConstantArrayKeyTypes(array $types): ?array $sanitizedTypes = []; foreach ($types as $type) { - if ((new IntegerType())->isSuperTypeOf($type)->no() && ! $type->toString() instanceof ErrorType) { + if ($type->isInteger()->no() && ! $type->toString() instanceof ErrorType) { $type = $type->toString(); } diff --git a/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php b/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php index 0830d0e3c6..3ca47235f8 100644 --- a/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; @@ -19,10 +18,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'current'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); diff --git a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php index 9456a3e92e..2e85f59099 100644 --- a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php @@ -6,7 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; @@ -15,7 +15,6 @@ use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; -use PHPStan\Type\IntersectionType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -35,33 +34,26 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_fill'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) < 3) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } - $startIndexType = $scope->getType($functionCall->getArgs()[0]->value); $numberType = $scope->getType($functionCall->getArgs()[1]->value); - $valueType = $scope->getType($functionCall->getArgs()[2]->value); - - if ($numberType instanceof IntegerRangeType) { - if ($numberType->getMin() < 0) { - return TypeCombinator::union( - new ArrayType(new IntegerType(), $valueType), - new ConstantBooleanType(false), - ); - } - } + $isValidNumberType = IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($numberType); // check against negative-int, which is not allowed - if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($numberType)->yes()) { + if ($isValidNumberType->no()) { if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { return new NeverType(); } return new ConstantBooleanType(false); } + $startIndexType = $scope->getType($functionCall->getArgs()[0]->value); + $valueType = $scope->getType($functionCall->getArgs()[2]->value); + if ( $startIndexType instanceof ConstantIntegerType && $numberType instanceof ConstantIntegerType @@ -84,14 +76,19 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $arrayBuilder->getArray(); } + $resultType = new ArrayType(new IntegerType(), $valueType); + if ((new ConstantIntegerType(0))->isSuperTypeOf($startIndexType)->yes()) { + $resultType = AccessoryArrayListType::intersectWith($resultType); + } if (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($numberType)->yes()) { - return new IntersectionType([ - new ArrayType(new IntegerType(), $valueType), - new NonEmptyArrayType(), - ]); + $resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType()); + } + + if (!$isValidNumberType->yes() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + $resultType = TypeCombinator::union($resultType, new ConstantBooleanType(false)); } - return new ArrayType(new IntegerType(), $valueType); + return $resultType; } } diff --git a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php index e912e4b828..785991b2c1 100644 --- a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php @@ -4,70 +4,38 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\ErrorType; -use PHPStan\Type\IntegerType; use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; class ArrayFillKeysFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_fill_keys'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } - $valueType = $scope->getType($functionCall->getArgs()[1]->value); $keysType = $scope->getType($functionCall->getArgs()[0]->value); - $constantArrays = TypeUtils::getOldConstantArrays($keysType); - if (count($constantArrays) === 0) { - if ($keysType->isArray()->yes()) { - $itemType = $keysType->getIterableValueType(); - - if ((new IntegerType())->isSuperTypeOf($itemType)->no()) { - if ($itemType->toString() instanceof ErrorType) { - return new ArrayType($itemType, $valueType); - } - - return new ArrayType($itemType->toString(), $valueType); - } - } - - return new ArrayType($keysType->getIterableValueType(), $valueType); - } - - $arrayTypes = []; - foreach ($constantArrays as $constantArray) { - $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($constantArray->getValueTypes() as $i => $keyType) { - if ((new IntegerType())->isSuperTypeOf($keyType)->no()) { - if ($keyType->toString() instanceof ErrorType) { - return new NeverType(); - } - - $arrayBuilder->setOffsetValueType($keyType->toString(), $valueType, $constantArray->isOptionalKey($i)); - } else { - $arrayBuilder->setOffsetValueType($keyType, $valueType, $constantArray->isOptionalKey($i)); - } - } - $arrayTypes[] = $arrayBuilder->getArray(); + if ($keysType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - return TypeCombinator::union(...$arrayTypes); + return $keysType->fillKeysArray($scope->getType($functionCall->getArgs()[1]->value)); } } diff --git a/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php b/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php index e38b58c30d..73d0f31e36 100644 --- a/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php +++ b/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php @@ -34,6 +34,7 @@ use function count; use function is_string; use function strtolower; +use function substr; class ArrayFilterFunctionReturnTypeReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -54,6 +55,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $arrayArgType = $scope->getType($arrayArg); + $arrayArgType = TypeUtils::toBenevolentUnion($arrayArgType); $keyType = $arrayArgType->getIterableKeyType(); $itemType = $arrayArgType->getIterableValueType(); @@ -68,9 +70,9 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ]); } - if ($callbackArg === null || ($callbackArg instanceof ConstFetch && strtolower($callbackArg->name->parts[0]) === 'null')) { + if ($callbackArg === null || ($callbackArg instanceof ConstFetch && strtolower($callbackArg->name->getParts()[0]) === 'null')) { return TypeCombinator::union( - ...array_map([$this, 'removeFalsey'], TypeUtils::getArrays($arrayArgType)), + ...array_map([$this, 'removeFalsey'], $arrayArgType->getArrays()), ); } @@ -84,12 +86,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, null, $callbackArg->expr); } elseif ($callbackArg instanceof String_) { $itemVar = new Variable('item'); - $expr = new FuncCall(new Name($callbackArg->value), [new Arg($itemVar)]); + $expr = new FuncCall(self::createFunctionName($callbackArg->value), [new Arg($itemVar)]); return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, null, $expr); } } - if ($flagArg instanceof ConstFetch && $flagArg->name->parts[0] === 'ARRAY_FILTER_USE_KEY') { + if ($flagArg instanceof ConstFetch && $flagArg->name->getParts()[0] === 'ARRAY_FILTER_USE_KEY') { if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) { $statement = $callbackArg->stmts[0]; if ($statement instanceof Return_ && $statement->expr !== null) { @@ -99,12 +101,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $this->filterByTruthyValue($scope, null, $arrayArgType, $callbackArg->params[0]->var, $callbackArg->expr); } elseif ($callbackArg instanceof String_) { $keyVar = new Variable('key'); - $expr = new FuncCall(new Name($callbackArg->value), [new Arg($keyVar)]); + $expr = new FuncCall(self::createFunctionName($callbackArg->value), [new Arg($keyVar)]); return $this->filterByTruthyValue($scope, null, $arrayArgType, $keyVar, $expr); } } - if ($flagArg instanceof ConstFetch && $flagArg->name->parts[0] === 'ARRAY_FILTER_USE_BOTH') { + if ($flagArg instanceof ConstFetch && $flagArg->name->getParts()[0] === 'ARRAY_FILTER_USE_BOTH') { if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) { $statement = $callbackArg->stmts[0]; if ($statement instanceof Return_ && $statement->expr !== null) { @@ -115,7 +117,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } elseif ($callbackArg instanceof String_) { $itemVar = new Variable('item'); $keyVar = new Variable('key'); - $expr = new FuncCall(new Name($callbackArg->value), [new Arg($itemVar), new Arg($keyVar)]); + $expr = new FuncCall(self::createFunctionName($callbackArg->value), [new Arg($itemVar), new Arg($keyVar)]); return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $expr); } } @@ -127,23 +129,28 @@ public function removeFalsey(Type $type): Type { $falseyTypes = StaticTypeFactory::falsey(); - if ($type instanceof ConstantArrayType) { - $keys = $type->getKeyTypes(); - $values = $type->getValueTypes(); + if (count($type->getConstantArrays()) > 0) { + $result = []; + foreach ($type->getConstantArrays() as $constantArray) { + $keys = $constantArray->getKeyTypes(); + $values = $constantArray->getValueTypes(); - $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($values as $offset => $value) { - $isFalsey = $falseyTypes->isSuperTypeOf($value); + foreach ($values as $offset => $value) { + $isFalsey = $falseyTypes->isSuperTypeOf($value); - if ($isFalsey->maybe()) { - $builder->setOffsetValueType($keys[$offset], TypeCombinator::remove($value, $falseyTypes), true); - } elseif ($isFalsey->no()) { - $builder->setOffsetValueType($keys[$offset], $value); + if ($isFalsey->maybe()) { + $builder->setOffsetValueType($keys[$offset], TypeCombinator::remove($value, $falseyTypes), true); + } elseif ($isFalsey->no()) { + $builder->setOffsetValueType($keys[$offset], $value, $constantArray->isOptionalKey($offset)); + } } + + $result[] = $builder->getArray(); } - return $builder->getArray(); + return TypeCombinator::union(...$result); } $keyType = $type->getIterableKeyType(); @@ -164,7 +171,7 @@ private function filterByTruthyValue(Scope $scope, Error|Variable|null $itemVar, throw new ShouldNotHappenException(); } - $constantArrays = TypeUtils::getOldConstantArrays($arrayType); + $constantArrays = $arrayType->getConstantArrays(); if (count($constantArrays) > 0) { $results = []; foreach ($constantArrays as $constantArray) { @@ -209,7 +216,7 @@ private function processKeyAndItemType(MutatingScope $scope, Type $keyType, Type throw new ShouldNotHappenException(); } $itemVarName = $itemVar->name; - $scope = $scope->assignVariable($itemVarName, $itemType); + $scope = $scope->assignVariable($itemVarName, $itemType, new MixedType()); } $keyVarName = null; @@ -218,11 +225,11 @@ private function processKeyAndItemType(MutatingScope $scope, Type $keyType, Type throw new ShouldNotHappenException(); } $keyVarName = $keyVar->name; - $scope = $scope->assignVariable($keyVarName, $keyType); + $scope = $scope->assignVariable($keyVarName, $keyType, new MixedType()); } $booleanResult = $scope->getType($expr)->toBoolean(); - if ($booleanResult instanceof ConstantBooleanType && !$booleanResult->getValue()) { + if ($booleanResult->isFalse()->yes()) { return [new NeverType(), new NeverType(), false]; } @@ -235,4 +242,13 @@ private function processKeyAndItemType(MutatingScope $scope, Type $keyType, Type ]; } + private static function createFunctionName(string $funcName): Name + { + if ($funcName[0] === '\\') { + return new Name\FullyQualified(substr($funcName, 1)); + } + + return new Name($funcName); + } + } diff --git a/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php index 29d53b6432..1beb76de40 100644 --- a/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php @@ -4,72 +4,38 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\Accessory\NonEmptyArrayType; -use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; class ArrayFlipFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_flip'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) !== 1) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } - $array = $functionCall->getArgs()[0]->value; - $argType = $scope->getType($array); - - $constantArrays = TypeUtils::getOldConstantArrays($argType); - if (count($constantArrays) > 0) { - $flipped = []; - foreach ($constantArrays as $constantArray) { - $builder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($constantArray->getKeyTypes() as $i => $keyType) { - $valueType = $constantArray->getValueTypes()[$i]; - $builder->setOffsetValueType( - ArrayType::castToArrayKeyType($valueType), - $keyType, - $constantArray->isOptionalKey($i), - ); - } - $flipped[] = $builder->getArray(); - } - - return TypeCombinator::union(...$flipped); - } - - if ($argType->isArray()->yes()) { - $keyType = $argType->getIterableKeyType(); - $itemType = $argType->getIterableValueType(); - - $itemType = ArrayType::castToArrayKeyType($itemType); - - $flippedArrayType = new ArrayType( - $itemType, - $keyType, - ); - - if ($argType->isIterableAtLeastOnce()->yes()) { - $flippedArrayType = TypeCombinator::intersect($flippedArrayType, new NonEmptyArrayType()); - } - - return $flippedArrayType; + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + if ($arrayType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return $arrayType->flipArray(); } } diff --git a/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php b/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php index c8a969539c..10335fa3e8 100644 --- a/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php @@ -4,30 +4,33 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function array_slice; use function count; class ArrayIntersectKeyFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_intersect_key'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { $args = $functionCall->getArgs(); if (count($args) === 0) { - return ParametersAcceptorSelector::selectFromArgs($scope, $args, $functionReflection->getVariants())->getReturnType(); + return null; } $argTypes = []; @@ -41,33 +44,19 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $argTypes[] = $argType; } - $firstArray = $argTypes[0]; - $otherArrays = array_slice($argTypes, 1); - if (count($otherArrays) === 0) { - return $firstArray; - } + $firstArrayType = $argTypes[0]; + $otherArraysType = TypeCombinator::union(...array_slice($argTypes, 1)); + $onlyOneArrayGiven = count($argTypes) === 1; - $constantArrays = TypeUtils::getOldConstantArrays($firstArray); - if (count($constantArrays) === 0) { - return new ArrayType($firstArray->getIterableKeyType(), $firstArray->getIterableValueType()); + if ($firstArrayType->isArray()->no() || (!$onlyOneArrayGiven && $otherArraysType->isArray()->no())) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - $otherArraysType = TypeCombinator::union(...$otherArrays); - $results = []; - foreach ($constantArrays as $constantArray) { - $builder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($constantArray->getKeyTypes() as $i => $keyType) { - $valueType = $constantArray->getValueTypes()[$i]; - $has = $otherArraysType->hasOffsetValueType($keyType); - if ($has->no()) { - continue; - } - $builder->setOffsetValueType($keyType, $valueType, $constantArray->isOptionalKey($i) || !$has->yes()); - } - $results[] = $builder->getArray(); + if ($onlyOneArrayGiven) { + return $firstArrayType; } - return TypeCombinator::union(...$results); + return $firstArrayType->intersectKeyArray($otherArraysType); } } diff --git a/src/Type/Php/ArrayIsListFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayIsListFunctionTypeSpecifyingExtension.php deleted file mode 100644 index da9de1508c..0000000000 --- a/src/Type/Php/ArrayIsListFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,66 +0,0 @@ -getName()) === 'array_is_list' - && !$context->null(); - } - - public function specifyTypes( - FunctionReflection $functionReflection, - FuncCall $node, - Scope $scope, - TypeSpecifierContext $context, - ): SpecifiedTypes - { - $arrayArg = $node->getArgs()[0]->value ?? null; - if ($arrayArg === null) { - return new SpecifiedTypes(); - } - - $valueType = $scope->getType($arrayArg); - if ($valueType instanceof ConstantArrayType) { - return $this->typeSpecifier->create($arrayArg, $valueType->getValuesArray(), $context, false, $scope); - } - - return $this->typeSpecifier->create( - $arrayArg, - TypeCombinator::intersect(new ArrayType(new IntegerType(), $valueType->getIterableValueType()), ...TypeUtils::getAccessoryTypes($valueType)), - $context, - false, - $scope, - ); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php index 2a698e153f..16e343e12d 100644 --- a/src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; @@ -19,10 +18,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'key'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); diff --git a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php index d504a260de..d2d5ed8360 100644 --- a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php @@ -57,19 +57,45 @@ public function specifyTypes( $array = $node->getArgs()[1]->value; $keyType = $scope->getType($key); $arrayType = $scope->getType($array); - if (!$keyType instanceof ConstantIntegerType && !$keyType instanceof ConstantStringType && !$arrayType->isIterableAtLeastOnce()->no()) { - if ($context->truthy()) { + + if (!$keyType instanceof ConstantIntegerType + && !$keyType instanceof ConstantStringType + && !$arrayType->isIterableAtLeastOnce()->no()) { + if ($context->true()) { + $arrayKeyType = $arrayType->getIterableKeyType(); + if ($keyType->isString()->yes()) { + $arrayKeyType = $arrayKeyType->toString(); + } elseif ($keyType->isString()->maybe()) { + $arrayKeyType = TypeCombinator::union($arrayKeyType, $arrayKeyType->toString()); + } + + $specifiedTypes = $this->typeSpecifier->create( + $key, + $arrayKeyType, + $context, + false, + $scope, + ); + $arrayDimFetch = new ArrayDimFetch( $array, $key, ); - return $this->typeSpecifier->create($arrayDimFetch, $arrayType->getIterableValueType(), $context, false, $scope, new Identical($arrayDimFetch, new ConstFetch(new Name('__PHPSTAN_FAUX_CONSTANT')))); + + return $specifiedTypes->unionWith($this->typeSpecifier->create( + $arrayDimFetch, + $arrayType->getIterableValueType(), + $context, + false, + $scope, + new Identical($arrayDimFetch, new ConstFetch(new Name('__PHPSTAN_FAUX_CONSTANT'))), + )); } return new SpecifiedTypes(); } - if ($context->truthy()) { + if ($context->true()) { $type = TypeCombinator::intersect( new ArrayType(new MixedType(), new MixedType()), new HasOffsetType($keyType), diff --git a/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php index da0063afe4..06fcfb7c74 100644 --- a/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php @@ -5,13 +5,10 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use function count; class ArrayKeyFirstDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -21,10 +18,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_key_first'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); @@ -33,25 +30,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new NullType(); } - $constantArrays = TypeUtils::getOldConstantArrays($argType); - if (count($constantArrays) > 0) { - $keyTypes = []; - foreach ($constantArrays as $constantArray) { - $iterableAtLeastOnce = $constantArray->isIterableAtLeastOnce(); - if (!$iterableAtLeastOnce->yes()) { - $keyTypes[] = new NullType(); - } - if ($iterableAtLeastOnce->no()) { - continue; - } - - $keyTypes[] = $constantArray->getFirstKeyType(); - } - - return TypeCombinator::union(...$keyTypes); - } - - $keyType = $argType->getIterableKeyType(); + $keyType = $argType->getFirstIterableKeyType(); if ($iterableAtLeastOnce->yes()) { return $keyType; } diff --git a/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php index fbe3f8739e..c1684afa9d 100644 --- a/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php @@ -5,13 +5,10 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use function count; class ArrayKeyLastDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -21,10 +18,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_key_last'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); @@ -33,25 +30,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new NullType(); } - $constantArrays = TypeUtils::getOldConstantArrays($argType); - if (count($constantArrays) > 0) { - $keyTypes = []; - foreach ($constantArrays as $constantArray) { - $iterableAtLeastOnce = $constantArray->isIterableAtLeastOnce(); - if (!$iterableAtLeastOnce->yes()) { - $keyTypes[] = new NullType(); - } - if ($iterableAtLeastOnce->no()) { - continue; - } - - $keyTypes[] = $constantArray->getLastKeyType(); - } - - return TypeCombinator::union(...$keyTypes); - } - - $keyType = $argType->getIterableKeyType(); + $keyType = $argType->getLastIterableKeyType(); if ($iterableAtLeastOnce->yes()) { return $keyType; } diff --git a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php index 40488348f7..2139222a9a 100644 --- a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php @@ -4,49 +4,39 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\Accessory\NonEmptyArrayType; -use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntegerType; -use PHPStan\Type\StringType; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\UnionType; +use function count; use function strtolower; class ArrayKeysFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return strtolower($functionReflection->getName()) === 'array_keys'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $arrayArg = $functionCall->getArgs()[0]->value ?? null; - if ($arrayArg !== null) { - $valueType = $scope->getType($arrayArg); - if ($valueType->isArray()->yes()) { - if ($valueType instanceof ConstantArrayType) { - return $valueType->getKeysArray(); - } - - $keyType = $valueType->getIterableKeyType(); - $array = new ArrayType(new IntegerType(), $keyType); - if ($valueType->isIterableAtLeastOnce()->yes()) { - $array = TypeCombinator::intersect($array, new NonEmptyArrayType()); - } - return $array; - } + if (count($functionCall->getArgs()) !== 1) { + return null; + } + + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + if ($arrayType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - return new ArrayType( - new IntegerType(), - new UnionType([new StringType(), new IntegerType()]), - ); + return $arrayType->getKeysArray(); } } diff --git a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index b41aee0bf7..50841093a8 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; @@ -14,7 +14,6 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; -use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; @@ -29,21 +28,22 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_map'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $singleArrayArgument = !isset($functionCall->getArgs()[2]); $callableType = $scope->getType($functionCall->getArgs()[0]->value); - $callableIsNull = (new NullType())->isSuperTypeOf($callableType)->yes(); + $callableIsNull = $callableType->isNull()->yes(); if ($callableType->isCallable()->yes()) { - $valueType = new NeverType(); + $valueTypes = [new NeverType()]; foreach ($callableType->getCallableParametersAcceptors($scope) as $parametersAcceptor) { - $valueType = TypeCombinator::union($valueType, $parametersAcceptor->getReturnType()); + $valueTypes[] = $parametersAcceptor->getReturnType(); } + $valueType = TypeCombinator::union(...$valueTypes); } elseif ($callableIsNull) { $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach (array_slice($functionCall->getArgs(), 1) as $index => $arg) { @@ -63,7 +63,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($callableIsNull) { return $arrayType; } - $constantArrays = TypeUtils::getOldConstantArrays($arrayType); + $constantArrays = $arrayType->getConstantArrays(); if (count($constantArrays) > 0) { $arrayTypes = []; foreach ($constantArrays as $constantArray) { @@ -75,7 +75,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $constantArray->isOptionalKey($i), ); } - $arrayTypes[] = $returnedArrayBuilder->getArray(); + $returnedArray = $returnedArrayBuilder->getArray(); + if ($constantArray->isList()->yes()) { + $returnedArray = AccessoryArrayListType::intersectWith($returnedArray); + } + $arrayTypes[] = $returnedArray; } $mappedArrayType = TypeCombinator::union(...$arrayTypes); diff --git a/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php index 460d67b9a5..5bde2a454e 100644 --- a/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php @@ -5,14 +5,15 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -28,12 +29,12 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_merge'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { $args = $functionCall->getArgs(); if (!isset($args[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argTypes = []; @@ -100,10 +101,16 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $keyTypes = []; $valueTypes = []; $nonEmpty = false; + $isList = true; foreach ($argTypes as $key => $argType) { - $keyTypes[] = $argType->getIterableKeyType(); + $keyType = $argType->getIterableKeyType(); + $keyTypes[] = $keyType; $valueTypes[] = $argType->getIterableValueType(); + if (!(new IntegerType())->isSuperTypeOf($keyType)->yes()) { + $isList = false; + } + if (in_array($key, $optionalArgTypes, true) || !$argType->isIterableAtLeastOnce()->yes()) { continue; } @@ -124,6 +131,9 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($nonEmpty) { $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); } + if ($isList) { + $arrayType = AccessoryArrayListType::intersectWith($arrayType); + } return $arrayType; } diff --git a/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php b/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php index f5c0fed29c..dff732d028 100644 --- a/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; @@ -20,10 +19,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return in_array($functionReflection->getName(), ['next', 'prev'], true); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); diff --git a/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php index c4fd56bdc7..cd12f678d1 100644 --- a/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php @@ -5,12 +5,10 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; use function in_array; @@ -32,10 +30,10 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { if (count($functionCall->getArgs()) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); @@ -44,27 +42,9 @@ public function getTypeFromFunctionCall( return new ConstantBooleanType(false); } - $constantArrays = TypeUtils::getOldConstantArrays($argType); - if (count($constantArrays) > 0) { - $keyTypes = []; - foreach ($constantArrays as $constantArray) { - $iterableAtLeastOnce = $argType->isIterableAtLeastOnce(); - if (!$iterableAtLeastOnce->yes()) { - $keyTypes[] = new ConstantBooleanType(false); - } - if ($iterableAtLeastOnce->no()) { - continue; - } - - $keyTypes[] = $functionReflection->getName() === 'reset' - ? $constantArray->getFirstValueType() - : $constantArray->getLastValueType(); - } - - return TypeCombinator::union(...$keyTypes); - } - - $itemType = $argType->getIterableValueType(); + $itemType = $functionReflection->getName() === 'reset' + ? $argType->getFirstIterableValueType() + : $argType->getLastIterableValueType(); if ($iterableAtLeastOnce->yes()) { return $itemType; } diff --git a/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php b/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php index 2d0436de43..64935edaea 100644 --- a/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php @@ -5,13 +5,10 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use function count; class ArrayPopFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -21,10 +18,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_pop'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); @@ -33,24 +30,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new NullType(); } - $constantArrays = TypeUtils::getOldConstantArrays($argType); - if (count($constantArrays) > 0) { - $valueTypes = []; - foreach ($constantArrays as $constantArray) { - $iterableAtLeastOnce = $constantArray->isIterableAtLeastOnce(); - if (!$iterableAtLeastOnce->yes()) { - $valueTypes[] = new NullType(); - } - if ($iterableAtLeastOnce->no()) { - continue; - } - $valueTypes[] = $constantArray->getLastValueType(); - } - - return TypeCombinator::union(...$valueTypes); - } - - $itemType = $argType->getIterableValueType(); + $itemType = $argType->getLastIterableValueType(); if ($iterableAtLeastOnce->yes()) { return $itemType; } diff --git a/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php b/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php index 090c8d5ea9..315acd8488 100644 --- a/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php @@ -5,10 +5,10 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\StringType; use PHPStan\Type\Type; @@ -24,15 +24,15 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_rand'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { $argsCount = count($functionCall->getArgs()); if ($argsCount < 1) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); - $isInteger = (new IntegerType())->isSuperTypeOf($firstArgType->getIterableKeyType()); + $isInteger = $firstArgType->getIterableKeyType()->isInteger(); $isString = $firstArgType->getIterableKeyType()->isString(); if ($isInteger->yes()) { @@ -49,14 +49,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $secondArgType = $scope->getType($functionCall->getArgs()[1]->value); - if ($secondArgType instanceof ConstantIntegerType) { - if ($secondArgType->getValue() === 1) { - return $valueType; - } + $one = new ConstantIntegerType(1); + if ($one->isSuperTypeOf($secondArgType)->yes()) { + return $valueType; + } - if ($secondArgType->getValue() >= 2) { - return new ArrayType(new IntegerType(), $valueType); - } + $bigger2 = IntegerRangeType::fromInterval(2, null); + if ($bigger2->isSuperTypeOf($secondArgType)->yes()) { + return new ArrayType(new IntegerType(), $valueType); } return TypeCombinator::union($valueType, new ArrayType(new IntegerType(), $valueType)); diff --git a/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php b/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php index 0f9487203f..80bd5866ec 100644 --- a/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php @@ -11,7 +11,6 @@ use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; class ArrayReduceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -22,15 +21,15 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_reduce'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[1])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $callbackType = $scope->getType($functionCall->getArgs()[1]->value); if ($callbackType->isCallable()->no()) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $callbackReturnType = ParametersAcceptorSelector::selectFromArgs( @@ -46,7 +45,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $arraysType = $scope->getType($functionCall->getArgs()[0]->value); - $constantArrays = TypeUtils::getOldConstantArrays($arraysType); + $constantArrays = $arraysType->getConstantArrays(); if (count($constantArrays) > 0) { $onlyEmpty = TrinaryLogic::createYes(); $onlyNonEmpty = TrinaryLogic::createYes(); diff --git a/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php b/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php index 049fc77497..ef16c66fbf 100644 --- a/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php @@ -5,14 +5,11 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntersectionType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; -use PHPStan\Type\TypeTraverser; -use PHPStan\Type\UnionType; +use PHPStan\Type\TypeCombinator; +use function count; class ArrayReverseFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -30,21 +27,23 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $type = $scope->getType($functionCall->getArgs()[0]->value); $preserveKeysType = isset($functionCall->getArgs()[1]) ? $scope->getType($functionCall->getArgs()[1]->value) : new NeverType(); - $preserveKeys = $preserveKeysType instanceof ConstantBooleanType ? $preserveKeysType->getValue() : false; + $preserveKeys = $preserveKeysType->isTrue()->yes(); - if (!$type->isIterable()->yes()) { + if (!$type->isArray()->yes()) { return null; } - return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($preserveKeys): Type { - if ($type instanceof UnionType || $type instanceof IntersectionType) { - return $traverse($type); + $constantArrays = $type->getConstantArrays(); + if (count($constantArrays) > 0) { + $results = []; + foreach ($constantArrays as $constantArray) { + $results[] = $constantArray->reverse($preserveKeys); } - if ($type instanceof ConstantArrayType) { - return $type->reverse($preserveKeys); - } - return $type; - }); + + return TypeCombinator::union(...$results); + } + + return $type; } } diff --git a/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php index 7190cb4d39..8560faf5a9 100644 --- a/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php @@ -4,38 +4,38 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\ConstantScalarType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\NeverType; use PHPStan\Type\NullType; -use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; final class ArraySearchFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_search'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { $argsCount = count($functionCall->getArgs()); if ($argsCount < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $haystackArgType = $scope->getType($functionCall->getArgs()[1]->value); - $haystackIsArray = $haystackArgType->isArray(); - if ($haystackIsArray->no()) { - return new NullType(); + if ($haystackArgType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } if ($argsCount < 3) { @@ -52,81 +52,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new ConstantBooleanType(false); } - $typesFromConstantArrays = []; - if ($haystackIsArray->maybe()) { - $typesFromConstantArrays[] = new NullType(); - } - - $haystackArrays = TypeUtils::getAnyArrays($haystackArgType); - if (count($haystackArrays) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - } - - $arrays = []; - $typesFromConstantArraysCount = 0; - foreach ($haystackArrays as $haystackArray) { - if (!$haystackArray instanceof ConstantArrayType) { - $arrays[] = $haystackArray; - continue; - } - - $typesFromConstantArrays[] = $this->resolveTypeFromConstantHaystackAndNeedle($needleArgType, $haystackArray); - $typesFromConstantArraysCount++; - } - - if ( - $typesFromConstantArraysCount > 0 - && count($haystackArrays) === $typesFromConstantArraysCount - ) { - return TypeCombinator::union(...$typesFromConstantArrays); - } - - $iterableKeyType = TypeCombinator::union(...$arrays)->getIterableKeyType(); - - return TypeCombinator::union( - $iterableKeyType, - new ConstantBooleanType(false), - ...$typesFromConstantArrays, - ); - } - - private function resolveTypeFromConstantHaystackAndNeedle(Type $needle, ConstantArrayType $haystack): Type - { - $matchesByType = []; - - foreach ($haystack->getValueTypes() as $index => $valueType) { - $isNeedleSuperType = $valueType->isSuperTypeOf($needle); - if ($isNeedleSuperType->no()) { - $matchesByType[] = new ConstantBooleanType(false); - continue; - } - - if ($needle instanceof ConstantScalarType && $valueType instanceof ConstantScalarType - && $needle->getValue() === $valueType->getValue() - ) { - return $haystack->getKeyTypes()[$index]; - } - - $matchesByType[] = $haystack->getKeyTypes()[$index]; - if (!$isNeedleSuperType->maybe()) { - continue; - } - - $matchesByType[] = new ConstantBooleanType(false); - } - - if (count($matchesByType) > 0) { - if ( - $haystack->getIterableValueType()->accepts($needle, true)->yes() - && $needle->isSuperTypeOf(new ObjectWithoutClassType())->no() - ) { - return TypeCombinator::union(...$matchesByType); - } - - return TypeCombinator::union(new ConstantBooleanType(false), ...$matchesByType); - } - - return new ConstantBooleanType(false); + return $haystackArgType->searchArray($needleArgType); } } diff --git a/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php b/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php index f569e852fc..47552e875e 100644 --- a/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php @@ -5,13 +5,10 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use function count; class ArrayShiftFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -21,10 +18,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_shift'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); @@ -33,25 +30,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new NullType(); } - $constantArrays = TypeUtils::getOldConstantArrays($argType); - if (count($constantArrays) > 0) { - $valueTypes = []; - foreach ($constantArrays as $constantArray) { - $iterableAtLeastOnce = $constantArray->isIterableAtLeastOnce(); - if (!$iterableAtLeastOnce->yes()) { - $valueTypes[] = new NullType(); - } - if ($iterableAtLeastOnce->no()) { - continue; - } - - $valueTypes[] = $constantArray->getFirstValueType(); - } - - return TypeCombinator::union(...$valueTypes); - } - - $itemType = $argType->getIterableValueType(); + $itemType = $argType->getFirstIterableValueType(); if ($iterableAtLeastOnce->yes()) { return $itemType; } diff --git a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php index 3438d79f9e..282c65d3b2 100644 --- a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php @@ -6,13 +6,10 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntersectionType; use PHPStan\Type\Type; -use PHPStan\Type\TypeTraverser; -use PHPStan\Type\UnionType; +use PHPStan\Type\TypeCombinator; use function count; class ArraySliceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -30,31 +27,34 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $valueType = $scope->getType($functionCall->getArgs()[0]->value); - if (!$valueType->isIterable()->yes()) { + if (!$valueType->isArray()->yes()) { return null; } $offsetType = isset($functionCall->getArgs()[1]) ? $scope->getType($functionCall->getArgs()[1]->value) : null; $offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : 0; - $limitType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null; - $limit = $limitType instanceof ConstantIntegerType ? $limitType->getValue() : null; + $constantArrays = $valueType->getConstantArrays(); + if (count($constantArrays) > 0) { + $limitType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null; + $limit = $limitType instanceof ConstantIntegerType ? $limitType->getValue() : null; - $preserveKeysType = isset($functionCall->getArgs()[3]) ? $scope->getType($functionCall->getArgs()[3]->value) : null; - $preserveKeys = $preserveKeysType instanceof ConstantBooleanType ? $preserveKeysType->getValue() : false; + $preserveKeysType = isset($functionCall->getArgs()[3]) ? $scope->getType($functionCall->getArgs()[3]->value) : null; + $preserveKeys = $preserveKeysType !== null && $preserveKeysType->isTrue()->yes(); - return TypeTraverser::map($valueType, static function (Type $type, callable $traverse) use ($offset, $limit, $preserveKeys): Type { - if ($type instanceof UnionType || $type instanceof IntersectionType) { - return $traverse($type); + $results = []; + foreach ($constantArrays as $constantArray) { + $results[] = $constantArray->slice($offset, $limit, $preserveKeys); } - if ($type instanceof ConstantArrayType) { - return $type->slice($offset, $limit, $preserveKeys); - } - if ($type->isIterableAtLeastOnce()->yes()) { - return $type->toArray(); - } - return $type; - }); + + return TypeCombinator::union(...$results); + } + + if ($valueType->isIterableAtLeastOnce()->yes()) { + return TypeCombinator::union($valueType, new ConstantArrayType([], [])); + } + + return $valueType; } } diff --git a/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php index 27e763c0d9..45cff582a2 100644 --- a/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; @@ -22,10 +21,10 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectFromArgs($scope, $functionCall->getArgs(), $functionReflection->getVariants())->getReturnType(); + return null; } $arrayArg = $scope->getType($functionCall->getArgs()[0]->value); diff --git a/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php index 03a5d15910..b60730e828 100644 --- a/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php @@ -2,17 +2,19 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr\BinaryOp\Mul; +use PhpParser\Node\Expr\BinaryOp\Plus; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Scalar\LNumber; use PHPStan\Analyser\Scope; +use PHPStan\Node\Expr\TypeExpr; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\FloatType; -use PHPStan\Type\IntegerType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\UnionType; +use function count; final class ArraySumFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -22,34 +24,42 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'array_sum'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } - $arrayType = $scope->getType($functionCall->getArgs()[0]->value); - $itemType = $arrayType->getIterableValueType(); + $argType = $scope->getType($functionCall->getArgs()[0]->value); + $resultTypes = []; - if ($arrayType->isIterableAtLeastOnce()->no()) { - return new ConstantIntegerType(0); - } + if (count($argType->getConstantArrays()) > 0) { + foreach ($argType->getConstantArrays() as $constantArray) { + $node = new LNumber(0); - $intUnionFloat = new UnionType([new IntegerType(), new FloatType()]); + foreach ($constantArray->getValueTypes() as $i => $type) { + if ($constantArray->isOptionalKey($i)) { + $node = new Plus($node, new TypeExpr(TypeCombinator::union($type, new ConstantIntegerType(0)))); + } else { + $node = new Plus($node, new TypeExpr($type)); + } + } - if ($arrayType->isIterableAtLeastOnce()->yes()) { - if ($intUnionFloat->isSuperTypeOf($itemType)->yes()) { - return $itemType; + $resultTypes[] = $scope->getType($node); } + } else { + $itemType = $argType->getIterableValueType(); + + $mulNode = new Mul(new TypeExpr($itemType), new TypeExpr(IntegerRangeType::fromInterval(0, null))); - return $intUnionFloat; + $resultTypes[] = $scope->getType(new Plus(new TypeExpr($itemType), $mulNode)); } - if ($intUnionFloat->isSuperTypeOf($itemType)->yes()) { - return TypeCombinator::union(new ConstantIntegerType(0), $itemType); + if (!$argType->isIterableAtLeastOnce()->yes()) { + $resultTypes[] = new ConstantIntegerType(0); } - return TypeCombinator::union(new ConstantIntegerType(0), $intUnionFloat); + return TypeCombinator::union(...$resultTypes)->toNumber(); } } diff --git a/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php index e3d5e0815b..4573e7d89d 100644 --- a/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php @@ -4,47 +4,39 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\Accessory\NonEmptyArrayType; -use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntegerType; -use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; +use function count; use function strtolower; class ArrayValuesFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return strtolower($functionReflection->getName()) === 'array_values'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $arrayArg = $functionCall->getArgs()[0]->value ?? null; - if ($arrayArg !== null) { - $valueType = $scope->getType($arrayArg); - if ($valueType->isArray()->yes()) { - if ($valueType instanceof ConstantArrayType) { - return $valueType->getValuesArray(); - } - - $array = new ArrayType(new IntegerType(), $valueType->getIterableValueType()); - if ($valueType->isIterableAtLeastOnce()->yes()) { - $array = TypeCombinator::intersect($array, new NonEmptyArrayType()); - } - return $array; - } + if (count($functionCall->getArgs()) !== 1) { + return null; + } + + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + if ($arrayType->isArray()->no()) { + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } - return new ArrayType( - new IntegerType(), - new MixedType(), - ); + return $arrayType->getValuesArray(); } } diff --git a/src/Type/Php/AssertThrowTypeExtension.php b/src/Type/Php/AssertThrowTypeExtension.php new file mode 100644 index 0000000000..22bbbc2047 --- /dev/null +++ b/src/Type/Php/AssertThrowTypeExtension.php @@ -0,0 +1,36 @@ +getName() === 'assert'; + } + + public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type + { + if (count($funcCall->getArgs()) < 2) { + return $functionReflection->getThrowType(); + } + + $customThrow = $scope->getType($funcCall->getArgs()[1]->value); + if ((new ObjectType(Throwable::class))->isSuperTypeOf($customThrow)->yes()) { + return $customThrow; + } + + return $functionReflection->getThrowType(); + } + +} diff --git a/src/Type/Php/BackedEnumFromMethodDynamicReturnTypeExtension.php b/src/Type/Php/BackedEnumFromMethodDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..8f4103018d --- /dev/null +++ b/src/Type/Php/BackedEnumFromMethodDynamicReturnTypeExtension.php @@ -0,0 +1,100 @@ +getName(), ['from', 'tryFrom'], true); + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + if (!$methodReflection->getDeclaringClass()->isBackedEnum()) { + return null; + } + + $arguments = $methodCall->getArgs(); + if (count($arguments) < 1) { + return null; + } + + $valueType = $scope->getType($arguments[0]->value); + + $enumCases = $methodReflection->getDeclaringClass()->getEnumCases(); + if (count($enumCases) === 0) { + if ($methodReflection->getName() === 'tryFrom') { + return new NullType(); + } + + return null; + } + + if (count($valueType->getConstantScalarValues()) === 0) { + return null; + } + + $resultEnumCases = []; + $addNull = false; + foreach ($valueType->getConstantScalarValues() as $value) { + $hasMatching = false; + foreach ($enumCases as $enumCase) { + if ($enumCase->getBackingValueType() === null) { + continue; + } + + $enumCaseValues = $enumCase->getBackingValueType()->getConstantScalarValues(); + if (count($enumCaseValues) !== 1) { + continue; + } + + if ($value === $enumCaseValues[0]) { + $resultEnumCases[] = new EnumCaseObjectType($enumCase->getDeclaringEnum()->getName(), $enumCase->getName(), $enumCase->getDeclaringEnum()); + $hasMatching = true; + break; + } + } + + if ($hasMatching) { + continue; + } + + $addNull = true; + } + + if (count($resultEnumCases) === 0) { + if ($methodReflection->getName() === 'tryFrom') { + return new NullType(); + } + + return null; + } + + $result = TypeCombinator::union(...$resultEnumCases); + if ($addNull && $methodReflection->getName() === 'tryFrom') { + return TypeCombinator::addNull($result); + } + + return $result; + } + +} diff --git a/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php b/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php index 158f1da5b1..bb1ef07430 100644 --- a/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php @@ -37,8 +37,8 @@ public function getTypeFromFunctionCall( return new BenevolentUnionType([new StringType(), new ConstantBooleanType(false)]); } - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); + $isTrueType = $argType->isTrue(); + $isFalseType = $argType->isFalse(); $compareTypes = $isTrueType->compareTo($isFalseType); if ($compareTypes === $isTrueType) { return new UnionType([new StringType(), new ConstantBooleanType(false)]); diff --git a/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php b/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php index efc57ed1cb..c10cbe2e58 100644 --- a/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php +++ b/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php @@ -12,7 +12,6 @@ use PHPStan\Type\ConstantScalarType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; -use PHPStan\Type\IntegerType; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; use PHPStan\Type\StringType; @@ -61,7 +60,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $secondArgument = $scope->getType($functionCall->getArgs()[1]->value); - $secondArgumentIsNumeric = ($secondArgument instanceof ConstantScalarType && is_numeric($secondArgument->getValue())) || $secondArgument instanceof IntegerType; + $secondArgumentIsNumeric = ($secondArgument instanceof ConstantScalarType && is_numeric($secondArgument->getValue())) || $secondArgument->isInteger()->yes(); if ($secondArgument instanceof ConstantScalarType && ($this->isZero($secondArgument->getValue()) || !$secondArgumentIsNumeric)) { if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { @@ -85,7 +84,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($thirdArgument instanceof ConstantScalarType && is_numeric($thirdArgument->getValue())) { $thirdArgumentIsNumeric = true; $thirdArgumentIsNegative = ($thirdArgument->getValue() < 0); - } elseif ((new IntegerType())->isSuperTypeOf($thirdArgument)->yes()) { + } elseif ($thirdArgument->isInteger()->yes()) { $thirdArgumentIsNumeric = true; if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($thirdArgument)->yes()) { $thirdArgumentIsNegative = true; diff --git a/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php index 5c10a3ad7d..9367e20b1b 100644 --- a/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php @@ -16,6 +16,8 @@ use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; +use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\ObjectType; use function in_array; use function ltrim; @@ -35,13 +37,12 @@ public function isFunctionSupported( 'interface_exists', 'trait_exists', 'enum_exists', - ], true) && isset($node->getArgs()[0]) && $context->truthy(); + ], true) && isset($node->getArgs()[0]) && $context->true(); } public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { $argType = $scope->getType($node->getArgs()[0]->value); - $classStringType = new ClassStringType(); if ($argType instanceof ConstantStringType) { return $this->typeSpecifier->create( new FuncCall(new FullyQualified('class_exists'), [ @@ -54,9 +55,14 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n ); } + $narrowedType = new ClassStringType(); + if ($functionReflection->getName() === 'enum_exists') { + $narrowedType = new GenericClassStringType(new ObjectType('UnitEnum')); + } + return $this->typeSpecifier->create( $node->getArgs()[0]->value, - $classStringType, + $narrowedType, $context, false, $scope, diff --git a/src/Type/Php/ClassImplementsFunctionReturnTypeExtension.php b/src/Type/Php/ClassImplementsFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..754ac737c8 --- /dev/null +++ b/src/Type/Php/ClassImplementsFunctionReturnTypeExtension.php @@ -0,0 +1,67 @@ +getName(), + ['class_implements', 'class_uses', 'class_parents'], + true, + ); + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) < 1) { + return null; + } + + $firstArgType = $scope->getType($args[0]->value); + $autoload = TrinaryLogic::createYes(); + if (isset($args[1])) { + $autoload = $scope->getType($args[1]->value)->isTrue(); + } + + $isObject = $firstArgType->isObject(); + $variant = ParametersAcceptorSelector::selectFromArgs($scope, $args, $functionReflection->getVariants()); + if ($isObject->yes()) { + return TypeCombinator::remove($variant->getReturnType(), new ConstantBooleanType(false)); + } + $isClassStringOrObject = (new UnionType([new ObjectWithoutClassType(), new ClassStringType()]))->isSuperTypeOf($firstArgType); + if ($isClassStringOrObject->yes()) { + if ($autoload->yes()) { + return TypeUtils::toBenevolentUnion($variant->getReturnType()); + } + + return $variant->getReturnType(); + } + + if ($firstArgType->isClassStringType()->no()) { + return new ConstantBooleanType(false); + } + + return null; + } + +} diff --git a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php index 312a3cd42c..1d0e07a500 100644 --- a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ClosureType; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\Type; @@ -24,11 +23,11 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo return $methodReflection->getName() === 'bind'; } - public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type { $closureType = $scope->getType($methodCall->getArgs()[0]->value); if (!($closureType instanceof ClosureType)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } return $closureType; diff --git a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php index d2d74ff813..73c34fa9ed 100644 --- a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ClosureType; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Type; @@ -24,11 +23,11 @@ public function isMethodSupported(MethodReflection $methodReflection): bool return $methodReflection->getName() === 'bindTo'; } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { $closureType = $scope->getType($methodCall->var); if (!($closureType instanceof ClosureType)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } return $closureType; diff --git a/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php b/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php index f5f8de9f1b..ed2bd20acd 100644 --- a/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php @@ -6,10 +6,11 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Type\ClosureType; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\ErrorType; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -26,14 +27,10 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo return $methodReflection->getName() === 'fromCallable'; } - public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type { if (!isset($methodCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectFromArgs( - $scope, - $methodCall->getArgs(), - $methodReflection->getVariants(), - )->getReturnType(); + return null; } $callableType = $scope->getType($methodCall->getArgs()[0]->value); @@ -50,6 +47,7 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, $variant->isVariadic(), $variant->getTemplateTypeMap(), $variant->getResolvedTemplateTypeMap(), + $variant instanceof ParametersAcceptorWithPhpDocs ? $variant->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), ); } diff --git a/src/Type/Php/CompactFunctionReturnTypeExtension.php b/src/Type/Php/CompactFunctionReturnTypeExtension.php index e17ddfb6a5..4c634ad0d1 100644 --- a/src/Type/Php/CompactFunctionReturnTypeExtension.php +++ b/src/Type/Php/CompactFunctionReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantStringType; @@ -30,15 +29,14 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); if (count($functionCall->getArgs()) === 0) { - return $defaultReturnType; + return null; } if ($scope->canAnyVariableExist() && !$this->checkMaybeUndefinedVariables) { - return $defaultReturnType; + return null; } $array = ConstantArrayTypeBuilder::createEmpty(); @@ -46,7 +44,7 @@ public function getTypeFromFunctionCall( $type = $scope->getType($arg->value); $constantStrings = $this->findConstantStrings($type); if ($constantStrings === null) { - return $defaultReturnType; + return null; } foreach ($constantStrings as $constantString) { $has = $scope->hasVariableType($constantString->getValue()); diff --git a/src/Type/Php/ConstantFunctionReturnTypeExtension.php b/src/Type/Php/ConstantFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..3c32b6c360 --- /dev/null +++ b/src/Type/Php/ConstantFunctionReturnTypeExtension.php @@ -0,0 +1,49 @@ +getName() === 'constant'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + if (count($functionCall->getArgs()) < 1) { + return null; + } + + $nameType = $scope->getType($functionCall->getArgs()[0]->value); + + $results = []; + foreach ($nameType->getConstantStrings() as $constantName) { + $results[] = $scope->getType($this->constantHelper->createExprFromConstantName($constantName->getValue())); + } + + if (count($results) > 0) { + return TypeCombinator::union(...$results); + } + + return null; + } + +} diff --git a/src/Type/Php/ConstantHelper.php b/src/Type/Php/ConstantHelper.php new file mode 100644 index 0000000000..790f169ed9 --- /dev/null +++ b/src/Type/Php/ConstantHelper.php @@ -0,0 +1,33 @@ += 2) { + $classConstName = new FullyQualified(ltrim($classConstParts[0], '\\')); + if ($classConstName->isSpecialClassName()) { + $classConstName = new Name($classConstName->toString()); + } + + return new ClassConstFetch($classConstName, new Identifier($classConstParts[1])); + } + + return new ConstFetch(new FullyQualified($constantName)); + } + +} diff --git a/src/Type/Php/CountFunctionReturnTypeExtension.php b/src/Type/Php/CountFunctionReturnTypeExtension.php index b037a8422d..d4c995f933 100644 --- a/src/Type/Php/CountFunctionReturnTypeExtension.php +++ b/src/Type/Php/CountFunctionReturnTypeExtension.php @@ -5,13 +5,9 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntegerRangeType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; use function in_array; use const COUNT_RECURSIVE; @@ -28,34 +24,20 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { if (count($functionCall->getArgs()) < 1) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } if (count($functionCall->getArgs()) > 1) { $mode = $scope->getType($functionCall->getArgs()[1]->value); if ($mode->isSuperTypeOf(new ConstantIntegerType(COUNT_RECURSIVE))->yes()) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } } - $argType = $scope->getType($functionCall->getArgs()[0]->value); - $constantArrays = TypeUtils::getOldConstantArrays($scope->getType($functionCall->getArgs()[0]->value)); - if (count($constantArrays) === 0) { - if ($argType->isIterableAtLeastOnce()->yes()) { - return IntegerRangeType::fromInterval(1, null); - } - - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - } - $countTypes = []; - foreach ($constantArrays as $array) { - $countTypes[] = $array->count(); - } - - return TypeCombinator::union(...$countTypes); + return $scope->getType($functionCall->getArgs()[0]->value)->getArraySize(); } } diff --git a/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php b/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php index ab1bd7aebd..112ed30292 100644 --- a/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr\Cast; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; @@ -11,11 +12,13 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; use function strtolower; class CtypeDigitFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension @@ -38,7 +41,8 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n throw new ShouldNotHappenException(); } - if ($context->true() && $scope->getType($node->getArgs()[0]->value)->isNumericString()->yes()) { + $exprArg = $node->getArgs()[0]->value; + if ($context->true() && $scope->getType($exprArg)->isNumericString()->yes()) { return new SpecifiedTypes(); } @@ -54,7 +58,24 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n ]); } - return $this->typeSpecifier->create($node->getArgs()[0]->value, TypeCombinator::union(...$types), $context, false, $scope); + $unionType = TypeCombinator::union(...$types); + $specifiedTypes = $this->typeSpecifier->create($exprArg, $unionType, $context, false, $scope); + + if ($exprArg instanceof Cast\String_) { + $castedType = new UnionType([ + IntegerRangeType::fromInterval(0, null), + new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]), + new ConstantBooleanType(true), + ]); + $specifiedTypes = $specifiedTypes->unionWith( + $this->typeSpecifier->create($exprArg->expr, $castedType, $context, false, $scope), + ); + } + + return $specifiedTypes; } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/CurlGetinfoFunctionDynamicReturnTypeExtension.php b/src/Type/Php/CurlGetinfoFunctionDynamicReturnTypeExtension.php index 75776426d6..b5de9de5f1 100644 --- a/src/Type/Php/CurlGetinfoFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/CurlGetinfoFunctionDynamicReturnTypeExtension.php @@ -6,14 +6,12 @@ use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\ConstantType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; @@ -38,12 +36,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'curl_getinfo'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) < 1) { - return ParametersAcceptorSelector::selectSingle( - $functionReflection->getVariants(), - )->getReturnType(); + return null; } if (count($functionCall->getArgs()) <= 1) { @@ -51,7 +47,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $componentType = $scope->getType($functionCall->getArgs()[1]->value); - if ($componentType instanceof ConstantType === false || $componentType->equals(new NullType())) { + if (!$componentType->isNull()->no()) { return $this->createAllComponentsReturnType(); } diff --git a/src/Type/Php/DateFormatFunctionReturnTypeExtension.php b/src/Type/Php/DateFormatFunctionReturnTypeExtension.php index b926ed181d..bae299abaa 100644 --- a/src/Type/Php/DateFormatFunctionReturnTypeExtension.php +++ b/src/Type/Php/DateFormatFunctionReturnTypeExtension.php @@ -3,7 +3,6 @@ namespace PHPStan\Type\Php; use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Name\FullyQualified; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -14,6 +13,10 @@ class DateFormatFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private DateFunctionReturnTypeHelper $dateFunctionReturnTypeHelper) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'date_format'; @@ -23,16 +26,15 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { if (count($functionCall->getArgs()) < 2) { return new StringType(); } - return $scope->getType( - new FuncCall(new FullyQualified('date'), [ - $functionCall->getArgs()[1], - ]), + return $this->dateFunctionReturnTypeHelper->getTypeFromFormatType( + $scope->getType($functionCall->getArgs()[1]->value), + true, ); } diff --git a/src/Type/Php/DateFormatMethodReturnTypeExtension.php b/src/Type/Php/DateFormatMethodReturnTypeExtension.php index f6b71786bc..f028fbea7b 100644 --- a/src/Type/Php/DateFormatMethodReturnTypeExtension.php +++ b/src/Type/Php/DateFormatMethodReturnTypeExtension.php @@ -3,9 +3,7 @@ namespace PHPStan\Type\Php; use DateTimeInterface; -use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\MethodCall; -use PhpParser\Node\Name\FullyQualified; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicMethodReturnTypeExtension; @@ -16,6 +14,10 @@ class DateFormatMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension { + public function __construct(private DateFunctionReturnTypeHelper $dateFunctionReturnTypeHelper) + { + } + public function getClass(): string { return DateTimeInterface::class; @@ -26,16 +28,15 @@ public function isMethodSupported(MethodReflection $methodReflection): bool return $methodReflection->getName() === 'format'; } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { if (count($methodCall->getArgs()) === 0) { return new StringType(); } - return $scope->getType( - new FuncCall(new FullyQualified('date'), [ - $methodCall->getArgs()[0], - ]), + return $this->dateFunctionReturnTypeHelper->getTypeFromFormatType( + $scope->getType($methodCall->getArgs()[0]->value), + true, ); } diff --git a/src/Type/Php/DateFunctionReturnTypeExtension.php b/src/Type/Php/DateFunctionReturnTypeExtension.php index a10f66b6e5..41432d1c3a 100644 --- a/src/Type/Php/DateFunctionReturnTypeExtension.php +++ b/src/Type/Php/DateFunctionReturnTypeExtension.php @@ -5,24 +5,17 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; -use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; -use PHPStan\Type\Accessory\AccessoryNumericStringType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntersectionType; -use PHPStan\Type\StringType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\UnionType; use function count; -use function date; -use function sprintf; class DateFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private DateFunctionReturnTypeHelper $dateFunctionReturnTypeHelper) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'date'; @@ -32,101 +25,16 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { if (count($functionCall->getArgs()) === 0) { - return new StringType(); - } - $argType = $scope->getType($functionCall->getArgs()[0]->value); - $constantStrings = TypeUtils::getConstantStrings($argType); - - if (count($constantStrings) === 0) { - return new StringType(); - } - - if (count($constantStrings) === 1) { - $constantString = $constantStrings[0]->getValue(); - - // see see https://www.php.net/manual/en/datetime.format.php - switch ($constantString) { - case 'd': - return $this->buildNumericRangeType(1, 31, true); - case 'j': - return $this->buildNumericRangeType(1, 31, false); - case 'N': - return $this->buildNumericRangeType(1, 7, false); - case 'w': - return $this->buildNumericRangeType(0, 6, false); - case 'm': - return $this->buildNumericRangeType(1, 12, true); - case 'n': - return $this->buildNumericRangeType(1, 12, false); - case 't': - return $this->buildNumericRangeType(28, 31, false); - case 'L': - return $this->buildNumericRangeType(0, 1, false); - case 'g': - return $this->buildNumericRangeType(1, 12, false); - case 'G': - return $this->buildNumericRangeType(0, 23, false); - case 'h': - return $this->buildNumericRangeType(1, 12, true); - case 'H': - return $this->buildNumericRangeType(0, 23, true); - case 'I': - return $this->buildNumericRangeType(0, 1, false); - } - } - - $types = []; - foreach ($constantStrings as $constantString) { - $types[] = new ConstantStringType(date($constantString->getValue())); - } - - $type = TypeCombinator::union(...$types); - if ($type->isNumericString()->yes()) { - return new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]); - } - - if ($type->isNonFalsyString()->yes()) { - return new IntersectionType([ - new StringType(), - new AccessoryNonFalsyStringType(), - ]); - } - - if ($type->isNonEmptyString()->yes()) { - return new IntersectionType([ - new StringType(), - new AccessoryNonEmptyStringType(), - ]); - } - - if ($type->isNonEmptyString()->no()) { - return new ConstantStringType(''); - } - - return new StringType(); - } - - private function buildNumericRangeType(int $min, int $max, bool $zeroPad): Type - { - $types = []; - - for ($i = $min; $i <= $max; $i++) { - $string = (string) $i; - - if ($zeroPad) { - $string = sprintf('%02s', $string); - } - - $types[] = new ConstantStringType($string); + return null; } - return new UnionType($types); + return $this->dateFunctionReturnTypeHelper->getTypeFromFormatType( + $scope->getType($functionCall->getArgs()[0]->value), + false, + ); } } diff --git a/src/Type/Php/DateFunctionReturnTypeHelper.php b/src/Type/Php/DateFunctionReturnTypeHelper.php new file mode 100644 index 0000000000..d6cadad384 --- /dev/null +++ b/src/Type/Php/DateFunctionReturnTypeHelper.php @@ -0,0 +1,114 @@ +getConstantStrings() as $formatString) { + $types[] = $this->buildReturnTypeFromFormat($formatString->getValue(), $useMicrosec); + } + + if (count($types) === 0) { + $types[] = $formatType->isNonEmptyString()->yes() + ? new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]) + : new StringType(); + } + + $type = TypeCombinator::union(...$types); + + if ($type->isNumericString()->no() && $formatType->isNonEmptyString()->yes()) { + $type = TypeCombinator::union($type, new IntersectionType([ + new StringType(), new AccessoryNonEmptyStringType(), + ])); + } + + return $type; + } + + public function buildReturnTypeFromFormat(string $formatString, bool $useMicrosec): Type + { + // see see https://www.php.net/manual/en/datetime.format.php + switch ($formatString) { + case 'd': + return $this->buildNumericRangeType(1, 31, true); + case 'j': + return $this->buildNumericRangeType(1, 31, false); + case 'N': + return $this->buildNumericRangeType(1, 7, false); + case 'w': + return $this->buildNumericRangeType(0, 6, false); + case 'm': + return $this->buildNumericRangeType(1, 12, true); + case 'n': + return $this->buildNumericRangeType(1, 12, false); + case 't': + return $this->buildNumericRangeType(28, 31, false); + case 'L': + return $this->buildNumericRangeType(0, 1, false); + case 'g': + return $this->buildNumericRangeType(1, 12, false); + case 'G': + return $this->buildNumericRangeType(0, 23, false); + case 'h': + return $this->buildNumericRangeType(1, 12, true); + case 'H': + return $this->buildNumericRangeType(0, 23, true); + case 'I': + return $this->buildNumericRangeType(0, 1, false); + case 'u': + return $useMicrosec + ? new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]) + : new ConstantStringType('000000'); + } + + $date = date($formatString); + + // If parameter string is not included, returned as ConstantStringType + if ($date === $formatString) { + return new ConstantStringType($date); + } + + if (is_numeric($date)) { + return new IntersectionType([new StringType(), new AccessoryNumericStringType()]); + } + + return new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]); + } + + private function buildNumericRangeType(int $min, int $max, bool $zeroPad): Type + { + $types = []; + + for ($i = $min; $i <= $max; $i++) { + $string = (string) $i; + + if ($zeroPad) { + $string = str_pad($string, 2, '0', STR_PAD_LEFT); + } + + $types[] = new ConstantStringType($string); + } + + return new UnionType($types); + } + +} diff --git a/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php index e7fda0fa2c..746fa0f2f5 100644 --- a/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php @@ -10,7 +10,6 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; class DateIntervalConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension @@ -28,7 +27,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } $valueType = $scope->getType($methodCall->getArgs()[0]->value); - $constantStrings = TypeUtils::getConstantStrings($valueType); + $constantStrings = $valueType->getConstantStrings(); foreach ($constantStrings as $constantString) { try { diff --git a/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..1fdcbf4937 --- /dev/null +++ b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php @@ -0,0 +1,68 @@ +getName() === 'createFromDateString'; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + $arguments = $methodCall->getArgs(); + + if (!isset($arguments[0])) { + return null; + } + + $strings = $scope->getType($arguments[0]->value)->getConstantStrings(); + + $possibleReturnTypes = []; + foreach ($strings as $string) { + try { + $result = @DateInterval::createFromDateString($string->getValue()); + } catch (Throwable) { + $possibleReturnTypes[] = false; + continue; + } + $possibleReturnTypes[] = $result instanceof DateInterval ? DateInterval::class : false; + } + + // the error case, when wrong types are passed + if (count($possibleReturnTypes) === 0) { + return null; + } + + if (in_array(false, $possibleReturnTypes, true) && in_array(DateInterval::class, $possibleReturnTypes, true)) { + return null; + } + + if (in_array(false, $possibleReturnTypes, true)) { + return new ConstantBooleanType(false); + } + + return new ObjectType(DateInterval::class); + } + +} diff --git a/src/Type/Php/DatePeriodConstructorReturnTypeExtension.php b/src/Type/Php/DatePeriodConstructorReturnTypeExtension.php index 457ad8ccce..a492e79388 100644 --- a/src/Type/Php/DatePeriodConstructorReturnTypeExtension.php +++ b/src/Type/Php/DatePeriodConstructorReturnTypeExtension.php @@ -70,7 +70,7 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, ]); } - if ((new IntegerType())->isSuperTypeOf($thirdArgType)->yes()) { + if ($thirdArgType->isInteger()->yes()) { return new GenericObjectType(DatePeriod::class, [ $firstArgType, new NullType(), diff --git a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php index c84512c05a..d69fad1ef3 100644 --- a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php @@ -11,7 +11,6 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; use function in_array; @@ -30,7 +29,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } $valueType = $scope->getType($methodCall->getArgs()[0]->value); - $constantStrings = TypeUtils::getConstantStrings($valueType); + $constantStrings = $valueType->getConstantStrings(); foreach ($constantStrings as $constantString) { try { diff --git a/src/Type/Php/DateTimeCreateDynamicReturnTypeExtension.php b/src/Type/Php/DateTimeCreateDynamicReturnTypeExtension.php index 26f726c971..bd00c0f12a 100644 --- a/src/Type/Php/DateTimeCreateDynamicReturnTypeExtension.php +++ b/src/Type/Php/DateTimeCreateDynamicReturnTypeExtension.php @@ -8,10 +8,10 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function count; use function date_create; use function in_array; @@ -30,16 +30,20 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return null; } - $datetime = $scope->getType($functionCall->getArgs()[0]->value); + $datetimes = $scope->getType($functionCall->getArgs()[0]->value)->getConstantStrings(); - if (!$datetime instanceof ConstantStringType) { + if (count($datetimes) === 0) { return null; } - $isValid = date_create($datetime->getValue()) !== false; - + $types = []; $className = $functionReflection->getName() === 'date_create' ? DateTime::class : DateTimeImmutable::class; - return $isValid ? new ObjectType($className) : new ConstantBooleanType(false); + foreach ($datetimes as $constantString) { + $isValid = date_create($constantString->getValue()) !== false; + $types[] = $isValid ? new ObjectType($className) : new ConstantBooleanType(false); + } + + return TypeCombinator::union(...$types); } } diff --git a/src/Type/Php/DateTimeDynamicReturnTypeExtension.php b/src/Type/Php/DateTimeDynamicReturnTypeExtension.php index 22945f9966..b18c8534df 100644 --- a/src/Type/Php/DateTimeDynamicReturnTypeExtension.php +++ b/src/Type/Php/DateTimeDynamicReturnTypeExtension.php @@ -7,12 +7,11 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function count; use function in_array; @@ -24,25 +23,29 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return in_array($functionReflection->getName(), ['date_create_from_format', 'date_create_immutable_from_format'], true); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (count($functionCall->getArgs()) < 2) { - return $defaultReturnType; + return null; } - $format = $scope->getType($functionCall->getArgs()[0]->value); - $datetime = $scope->getType($functionCall->getArgs()[1]->value); + $formats = $scope->getType($functionCall->getArgs()[0]->value)->getConstantStrings(); + $datetimes = $scope->getType($functionCall->getArgs()[1]->value)->getConstantStrings(); - if (!$format instanceof ConstantStringType || !$datetime instanceof ConstantStringType) { - return $defaultReturnType; + if (count($formats) === 0 || count($datetimes) === 0) { + return null; } - $isValid = (DateTime::createFromFormat($format->getValue(), $datetime->getValue()) !== false); - + $types = []; $className = $functionReflection->getName() === 'date_create_from_format' ? DateTime::class : DateTimeImmutable::class; - return $isValid ? new ObjectType($className) : new ConstantBooleanType(false); + foreach ($formats as $formatConstantString) { + foreach ($datetimes as $datetimeConstantString) { + $isValid = (DateTime::createFromFormat($formatConstantString->getValue(), $datetimeConstantString->getValue()) !== false); + $types[] = $isValid ? new ObjectType($className) : new ConstantBooleanType(false); + } + } + + return TypeCombinator::union(...$types); } } diff --git a/src/Type/Php/DateTimeModifyReturnTypeExtension.php b/src/Type/Php/DateTimeModifyReturnTypeExtension.php index 937ce2fd27..17359ba45c 100644 --- a/src/Type/Php/DateTimeModifyReturnTypeExtension.php +++ b/src/Type/Php/DateTimeModifyReturnTypeExtension.php @@ -12,7 +12,7 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; +use Throwable; use function count; class DateTimeModifyReturnTypeExtension implements DynamicMethodReturnTypeExtension @@ -44,13 +44,21 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } $valueType = $scope->getType($methodCall->getArgs()[0]->value); - $constantStrings = TypeUtils::getConstantStrings($valueType); + $constantStrings = $valueType->getConstantStrings(); $hasFalse = false; $hasDateTime = false; foreach ($constantStrings as $constantString) { - if (@(new DateTime())->modify($constantString->getValue()) === false) { + try { + $result = @(new DateTime())->modify($constantString->getValue()); + } catch (Throwable) { + $hasFalse = true; + $valueType = TypeCombinator::remove($valueType, $constantString); + continue; + } + + if ($result === false) { $hasFalse = true; } else { $hasDateTime = true; diff --git a/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php b/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php new file mode 100644 index 0000000000..3715cc4329 --- /dev/null +++ b/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php @@ -0,0 +1,49 @@ +getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === DateTimeZone::class; + } + + public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->getArgs()) === 0) { + return null; + } + + $valueType = $scope->getType($methodCall->getArgs()[0]->value); + $constantStrings = $valueType->getConstantStrings(); + + foreach ($constantStrings as $constantString) { + try { + new DateTimeZone($constantString->getValue()); + } catch (\Exception $e) { // phpcs:ignore + return $methodReflection->getThrowType(); + } + + $valueType = TypeCombinator::remove($valueType, $constantString); + } + + if (!$valueType instanceof NeverType) { + return $methodReflection->getThrowType(); + } + + return null; + } + +} diff --git a/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php b/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php index 0e90075013..43ab4c48db 100644 --- a/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php +++ b/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php @@ -2,7 +2,6 @@ namespace PHPStan\Type\Php; -use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; @@ -20,6 +19,10 @@ class DefinedConstantTypeSpecifyingExtension implements FunctionTypeSpecifyingEx private TypeSpecifier $typeSpecifier; + public function __construct(private ConstantHelper $constantHelper) + { + } + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void { $this->typeSpecifier = $typeSpecifier; @@ -33,7 +36,7 @@ public function isFunctionSupported( { return $functionReflection->getName() === 'defined' && count($node->getArgs()) >= 1 - && !$context->null(); + && $context->true(); } public function specifyTypes( @@ -52,9 +55,7 @@ public function specifyTypes( } return $this->typeSpecifier->create( - new Node\Expr\ConstFetch( - new Node\Name\FullyQualified($constantName->getValue()), - ), + $this->constantHelper->createExprFromConstantName($constantName->getValue()), new MixedType(), $context, false, diff --git a/src/Type/Php/DsMapDynamicMethodThrowTypeExtension.php b/src/Type/Php/DsMapDynamicMethodThrowTypeExtension.php new file mode 100644 index 0000000000..3075827a96 --- /dev/null +++ b/src/Type/Php/DsMapDynamicMethodThrowTypeExtension.php @@ -0,0 +1,31 @@ +getDeclaringClass()->getName() === 'Ds\Map' + && ($methodReflection->getName() === 'get' || $methodReflection->getName() === 'remove'); + } + + public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->args) < 2) { + return $methodReflection->getThrowType(); + } + + return new VoidType(); + } + +} diff --git a/src/Type/Php/DsMapDynamicReturnTypeExtension.php b/src/Type/Php/DsMapDynamicReturnTypeExtension.php index 6441028275..50ecba6226 100644 --- a/src/Type/Php/DsMapDynamicReturnTypeExtension.php +++ b/src/Type/Php/DsMapDynamicReturnTypeExtension.php @@ -5,11 +5,11 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\TypeWithClassName; use function count; +use function in_array; final class DsMapDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -21,44 +21,38 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection): bool { - return $methodReflection->getName() === 'get' || $methodReflection->getName() === 'remove'; + return in_array($methodReflection->getName(), ['get', 'remove'], true); } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { - $returnType = ParametersAcceptorSelector::selectFromArgs( - $scope, - $methodCall->getArgs(), - $methodReflection->getVariants(), - )->getReturnType(); - $argsCount = count($methodCall->getArgs()); if ($argsCount > 1) { - return $returnType; + return null; } if ($argsCount === 0) { - return $returnType; + return null; } $mapType = $scope->getType($methodCall->var); if (!$mapType instanceof TypeWithClassName) { - return $returnType; + return null; } $mapAncestor = $mapType->getAncestorWithClassName('Ds\Map'); if ($mapAncestor === null) { - return $returnType; + return null; } $mapAncestorClass = $mapAncestor->getClassReflection(); if ($mapAncestorClass === null) { - return $returnType; + return null; } $valueType = $mapAncestorClass->getActiveTemplateTypeMap()->getType('TValue'); if ($valueType === null) { - return $returnType; + return null; } return $valueType; diff --git a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php index 8440f8f2dd..498904c528 100644 --- a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php @@ -7,6 +7,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -38,10 +39,10 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $delimiterType = $scope->getType($functionCall->getArgs()[0]->value); @@ -52,7 +53,7 @@ public function getTypeFromFunctionCall( } return new ConstantBooleanType(false); } elseif ($isSuperset->no()) { - $arrayType = new ArrayType(new IntegerType(), new StringType()); + $arrayType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new StringType())); if ( !isset($functionCall->getArgs()[2]) || IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($functionCall->getArgs()[2]->value))->yes() diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php new file mode 100644 index 0000000000..a2ed6c792b --- /dev/null +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -0,0 +1,448 @@ +|null */ + private ?array $filterTypeMap = null; + + /** @var array>|null */ + private ?array $filterTypeOptions = null; + + private ?Type $supportedFilterInputTypes = null; + + public function __construct(private ReflectionProvider $reflectionProvider, private PhpVersion $phpVersion) + { + $this->flagsString = new ConstantStringType('flags'); + } + + public function getOffsetValueType(Type $inputType, Type $offsetType, ?Type $filterType, ?Type $flagsType): Type + { + $inexistentOffsetType = $this->hasFlag($this->getConstant('FILTER_NULL_ON_FAILURE'), $flagsType) + ? new ConstantBooleanType(false) + : new NullType(); + + $hasOffsetValueType = $inputType->hasOffsetValueType($offsetType); + if ($hasOffsetValueType->no()) { + return $inexistentOffsetType; + } + + $filteredType = $this->getType($inputType->getOffsetValueType($offsetType), $filterType, $flagsType); + + return $hasOffsetValueType->maybe() + ? TypeCombinator::union($filteredType, $inexistentOffsetType) + : $filteredType; + } + + public function getInputType(Type $typeType, Type $varNameType, ?Type $filterType, ?Type $flagsType): Type + { + $this->supportedFilterInputTypes ??= TypeCombinator::union( + $this->reflectionProvider->getConstant(new Node\Name('INPUT_GET'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_POST'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_COOKIE'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_SERVER'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_ENV'), null)->getValueType(), + ); + + if (!$typeType->isInteger()->yes() || $this->supportedFilterInputTypes->isSuperTypeOf($typeType)->no()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + + // Using a null as input mimics pre PHP 8 behaviour where filter_input + // would return the same as if the offset does not exist + $inputType = new NullType(); + } else { + // Pragmatical solution since global expressions are not passed through the scope for performance reasons + // See https://github.com/phpstan/phpstan-src/pull/2012 for details + $inputType = new ArrayType(new StringType(), new MixedType()); + } + + return $this->getOffsetValueType($inputType, $varNameType, $filterType, $flagsType); + } + + public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): Type + { + $mixedType = new MixedType(); + + if ($filterType === null) { + $filterValue = $this->getConstant('FILTER_DEFAULT'); + } else { + if (!$filterType instanceof ConstantIntegerType) { + return $mixedType; + } + $filterValue = $filterType->getValue(); + } + + if ($flagsType === null) { + $flagsType = new ConstantIntegerType(0); + } + + $hasOptions = $this->hasOptions($flagsType); + $options = $hasOptions->yes() ? $this->getOptions($flagsType, $filterValue) : []; + + $defaultType = $options['default'] ?? ($this->hasFlag($this->getConstant('FILTER_NULL_ON_FAILURE'), $flagsType) + ? new NullType() + : new ConstantBooleanType(false)); + + $inputIsArray = $inputType->isArray(); + $hasRequireArrayFlag = $this->hasFlag($this->getConstant('FILTER_REQUIRE_ARRAY'), $flagsType); + if ($inputIsArray->no() && $hasRequireArrayFlag) { + return $defaultType; + } + + $hasForceArrayFlag = $this->hasFlag($this->getConstant('FILTER_FORCE_ARRAY'), $flagsType); + if ($inputIsArray->yes() && ($hasRequireArrayFlag || $hasForceArrayFlag)) { + $inputArrayKeyType = $inputType->getIterableKeyType(); + $inputType = $inputType->getIterableValueType(); + } + + if ($inputType->isScalar()->no() && $inputType->isNull()->no()) { + return $defaultType; + } + + $exactType = $this->determineExactType($inputType, $filterValue, $defaultType, $flagsType); + $type = $exactType ?? $this->getFilterTypeMap()[$filterValue] ?? $mixedType; + $type = $this->applyRangeOptions($type, $options, $defaultType); + + if ($inputType->isNonEmptyString()->yes() + && $type->isString()->yes() + && !$this->canStringBeSanitized($filterValue, $flagsType)) { + $accessory = new AccessoryNonEmptyStringType(); + if ($inputType->isNonFalsyString()->yes()) { + $accessory = new AccessoryNonFalsyStringType(); + } + $type = TypeCombinator::intersect($type, $accessory); + } + + if ($hasRequireArrayFlag) { + $type = new ArrayType($inputArrayKeyType ?? $mixedType, $type); + } + + if ($exactType === null || $hasOptions->maybe() || (!$inputType->equals($type) && $inputType->isSuperTypeOf($type)->yes())) { + if ($defaultType->isSuperTypeOf($type)->no()) { + $type = TypeCombinator::union($type, $defaultType); + } + } + + if (!$hasRequireArrayFlag && $hasForceArrayFlag) { + return new ArrayType($inputArrayKeyType ?? $mixedType, $type); + } + + return $type; + } + + /** + * @return array + */ + private function getFilterTypeMap(): array + { + if ($this->filterTypeMap !== null) { + return $this->filterTypeMap; + } + + $booleanType = new BooleanType(); + $floatType = new FloatType(); + $intType = new IntegerType(); + $stringType = new StringType(); + $nonFalsyStringType = TypeCombinator::intersect($stringType, new AccessoryNonFalsyStringType()); + + $this->filterTypeMap = [ + $this->getConstant('FILTER_UNSAFE_RAW') => $stringType, + $this->getConstant('FILTER_SANITIZE_EMAIL') => $stringType, + $this->getConstant('FILTER_SANITIZE_ENCODED') => $stringType, + $this->getConstant('FILTER_SANITIZE_NUMBER_FLOAT') => $stringType, + $this->getConstant('FILTER_SANITIZE_NUMBER_INT') => $stringType, + $this->getConstant('FILTER_SANITIZE_SPECIAL_CHARS') => $stringType, + $this->getConstant('FILTER_SANITIZE_STRING') => $stringType, + $this->getConstant('FILTER_SANITIZE_URL') => $stringType, + $this->getConstant('FILTER_VALIDATE_BOOLEAN') => $booleanType, + $this->getConstant('FILTER_VALIDATE_DOMAIN') => $stringType, + $this->getConstant('FILTER_VALIDATE_EMAIL') => $nonFalsyStringType, + $this->getConstant('FILTER_VALIDATE_FLOAT') => $floatType, + $this->getConstant('FILTER_VALIDATE_INT') => $intType, + $this->getConstant('FILTER_VALIDATE_IP') => $nonFalsyStringType, + $this->getConstant('FILTER_VALIDATE_MAC') => $nonFalsyStringType, + $this->getConstant('FILTER_VALIDATE_REGEXP') => $stringType, + $this->getConstant('FILTER_VALIDATE_URL') => $nonFalsyStringType, + ]; + + if ($this->reflectionProvider->hasConstant(new Node\Name('FILTER_SANITIZE_MAGIC_QUOTES'), null)) { + $this->filterTypeMap[$this->getConstant('FILTER_SANITIZE_MAGIC_QUOTES')] = $stringType; + } + + if ($this->reflectionProvider->hasConstant(new Node\Name('FILTER_SANITIZE_ADD_SLASHES'), null)) { + $this->filterTypeMap[$this->getConstant('FILTER_SANITIZE_ADD_SLASHES')] = $stringType; + } + + return $this->filterTypeMap; + } + + /** + * @return array> + */ + private function getFilterTypeOptions(): array + { + if ($this->filterTypeOptions !== null) { + return $this->filterTypeOptions; + } + + $this->filterTypeOptions = [ + $this->getConstant('FILTER_VALIDATE_INT') => ['min_range', 'max_range'], + // PHPStan does not yet support FloatRangeType + // $this->getConstant('FILTER_VALIDATE_FLOAT') => ['min_range', 'max_range'], + ]; + + return $this->filterTypeOptions; + } + + private function getConstant(string $constantName): int + { + $constant = $this->reflectionProvider->getConstant(new Node\Name($constantName), null); + $valueType = $constant->getValueType(); + if (!$valueType instanceof ConstantIntegerType) { + throw new ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); + } + + return $valueType->getValue(); + } + + private function determineExactType(Type $in, int $filterValue, Type $defaultType, ?Type $flagsType): ?Type + { + if ($filterValue === $this->getConstant('FILTER_VALIDATE_BOOLEAN')) { + if ($in->isBoolean()->yes()) { + return $in; + } + + if ($in->isNull()->yes()) { + return $defaultType; + } + } + + if ($filterValue === $this->getConstant('FILTER_VALIDATE_FLOAT')) { + if ($in->isFloat()->yes()) { + return $in; + } + + if ($in->isInteger()->yes()) { + return $in->toFloat(); + } + + if ($in->isTrue()->yes()) { + return new ConstantFloatType(1); + } + + if ($in->isFalse()->yes() || $in->isNull()->yes()) { + return $defaultType; + } + } + + if ($filterValue === $this->getConstant('FILTER_VALIDATE_INT')) { + if ($in->isInteger()->yes()) { + return $in; + } + + if ($in->isTrue()->yes()) { + return new ConstantIntegerType(1); + } + + if ($in->isFalse()->yes() || $in->isNull()->yes()) { + return $defaultType; + } + + if ($in instanceof ConstantFloatType) { + return $in->getValue() - (int) $in->getValue() <= PHP_FLOAT_EPSILON + ? $in->toInteger() + : $defaultType; + } + + if ($in instanceof ConstantStringType) { + $value = $in->getValue(); + $allowOctal = $this->hasFlag($this->getConstant('FILTER_FLAG_ALLOW_OCTAL'), $flagsType); + $allowHex = $this->hasFlag($this->getConstant('FILTER_FLAG_ALLOW_HEX'), $flagsType); + + if ($allowOctal && preg_match('/\A0[oO][0-7]+\z/', $value) === 1) { + $octalValue = octdec($value); + return is_int($octalValue) ? new ConstantIntegerType($octalValue) : $defaultType; + } + + if ($allowHex && preg_match('/\A0[xX][0-9A-Fa-f]+\z/', $value) === 1) { + $hexValue = hexdec($value); + return is_int($hexValue) ? new ConstantIntegerType($hexValue) : $defaultType; + } + + return preg_match('/\A[+-]?(?:0|[1-9][0-9]*)\z/', $value) === 1 ? $in->toInteger() : $defaultType; + } + } + + if ($filterValue === $this->getConstant('FILTER_DEFAULT')) { + if (!$this->canStringBeSanitized($filterValue, $flagsType) && $in->isString()->yes()) { + return $in; + } + + if ($in->isBoolean()->yes() || $in->isFloat()->yes() || $in->isInteger()->yes() || $in->isNull()->yes()) { + return $in->toString(); + } + } + + return null; + } + + /** @param array $typeOptions */ + private function applyRangeOptions(Type $type, array $typeOptions, Type $defaultType): Type + { + if (!$type->isInteger()->yes()) { + return $type; + } + + $range = []; + if (isset($typeOptions['min_range'])) { + if ($typeOptions['min_range'] instanceof ConstantScalarType) { + $range['min'] = (int) $typeOptions['min_range']->getValue(); + } elseif ($typeOptions['min_range'] instanceof IntegerRangeType) { + $range['min'] = $typeOptions['min_range']->getMin(); + } else { + $range['min'] = null; + } + } + if (isset($typeOptions['max_range'])) { + if ($typeOptions['max_range'] instanceof ConstantScalarType) { + $range['max'] = (int) $typeOptions['max_range']->getValue(); + } elseif ($typeOptions['max_range'] instanceof IntegerRangeType) { + $range['max'] = $typeOptions['max_range']->getMax(); + } else { + $range['max'] = null; + } + } + + if (array_key_exists('min', $range) || array_key_exists('max', $range)) { + $min = $range['min'] ?? null; + $max = $range['max'] ?? null; + $rangeType = IntegerRangeType::fromInterval($min, $max); + $rangeTypeIsSuperType = $rangeType->isSuperTypeOf($type); + + if ($rangeTypeIsSuperType->no()) { + // e.g. if 9 is filtered with a range of int<17, 19> + return $defaultType; + } + + if ($rangeTypeIsSuperType->yes() && !$rangeType->equals($type)) { + // e.g. if 18 or int<18, 19> are filtered with a range of int<17, 19> + return $type; + } + + // Open ranges on either side means that the input is potentially not part of the range + return $min === null || $max === null ? TypeCombinator::union($rangeType, $defaultType) : $rangeType; + } + + return $type; + } + + private function hasOptions(Type $flagsType): TrinaryLogic + { + return $flagsType->isArray() + ->and($flagsType->hasOffsetValueType(new ConstantStringType('options'))); + } + + /** @return array */ + private function getOptions(Type $flagsType, int $filterValue): array + { + $options = []; + + $optionsType = $flagsType->getOffsetValueType(new ConstantStringType('options')); + if (!$optionsType->isConstantArray()->yes()) { + return $options; + } + + $optionNames = array_merge(['default'], $this->getFilterTypeOptions()[$filterValue] ?? []); + foreach ($optionNames as $optionName) { + $optionaNameType = new ConstantStringType($optionName); + if (!$optionsType->hasOffsetValueType($optionaNameType)->yes()) { + $options[$optionName] = null; + continue; + } + + $options[$optionName] = $optionsType->getOffsetValueType($optionaNameType); + } + + return $options; + } + + private function hasFlag(int $flag, ?Type $flagsType): bool + { + if ($flagsType === null) { + return false; + } + + $type = $this->getFlagsValue($flagsType); + + return $type instanceof ConstantIntegerType && ($type->getValue() & $flag) === $flag; + } + + private function getFlagsValue(Type $exprType): Type + { + if (!$exprType->isConstantArray()->yes()) { + return $exprType; + } + + return $exprType->getOffsetValueType($this->flagsString); + } + + private function canStringBeSanitized(int $filterValue, ?Type $flagsType): bool + { + // If it is a validation filter, the string will not be changed + if (($filterValue & self::VALIDATION_FILTER_BITMASK) !== 0) { + return false; + } + + // FILTER_DEFAULT will not sanitize, unless it has FILTER_FLAG_STRIP_LOW, + // FILTER_FLAG_STRIP_HIGH, or FILTER_FLAG_STRIP_BACKTICK + if ($filterValue === $this->getConstant('FILTER_DEFAULT')) { + return $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_LOW'), $flagsType) + || $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_HIGH'), $flagsType) + || $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_BACKTICK'), $flagsType); + } + + return true; + } + +} diff --git a/src/Type/Php/FilterInputDynamicReturnTypeExtension.php b/src/Type/Php/FilterInputDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..0dd934cd32 --- /dev/null +++ b/src/Type/Php/FilterInputDynamicReturnTypeExtension.php @@ -0,0 +1,38 @@ +getName() === 'filter_input'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 2) { + return null; + } + + return $this->filterFunctionReturnTypeHelper->getInputType( + $scope->getType($functionCall->getArgs()[0]->value), + $scope->getType($functionCall->getArgs()[1]->value), + isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null, + isset($functionCall->getArgs()[3]) ? $scope->getType($functionCall->getArgs()[3]->value) : null, + ); + } + +} diff --git a/src/Type/Php/FilterVarArrayDynamicReturnTypeExtension.php b/src/Type/Php/FilterVarArrayDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..caaca73501 --- /dev/null +++ b/src/Type/Php/FilterVarArrayDynamicReturnTypeExtension.php @@ -0,0 +1,198 @@ +getName()), ['filter_var_array', 'filter_input_array'], true); + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 2) { + return null; + } + + $functionName = strtolower($functionReflection->getName()); + $inputArgType = $scope->getType($functionCall->getArgs()[0]->value); + $inputConstantArrayType = null; + if ($functionName === 'filter_var_array') { + if ($inputArgType->isArray()->no()) { + return new NeverType(); + } + + $inputConstantArrayType = $inputArgType->getConstantArrays()[0] ?? null; + } elseif ($functionName === 'filter_input_array') { + $supportedTypes = TypeCombinator::union( + $this->reflectionProvider->getConstant(new Node\Name('INPUT_GET'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_POST'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_COOKIE'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_SERVER'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_ENV'), null)->getValueType(), + ); + + if (!$inputArgType->isInteger()->yes() || $supportedTypes->isSuperTypeOf($inputArgType)->no()) { + return null; + } + + // Pragmatical solution since global expressions are not passed through the scope for performance reasons + // See https://github.com/phpstan/phpstan-src/pull/2012 for details + $inputArgType = new ArrayType(new StringType(), new MixedType()); + } + + $filterArgType = $scope->getType($functionCall->getArgs()[1]->value); + $filterConstantArrayType = $filterArgType->getConstantArrays()[0] ?? null; + $addEmptyType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null; + $addEmpty = $addEmptyType === null || $addEmptyType->isTrue()->yes(); + + $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); + + if ($filterArgType instanceof ConstantIntegerType) { + if ($inputConstantArrayType === null) { + $isList = $inputArgType->isList()->yes(); + $valueType = $this->filterFunctionReturnTypeHelper->getType( + $inputArgType->getIterableValueType(), + $filterArgType, + null, + ); + $arrayType = new ArrayType($inputArgType->getIterableKeyType(), $valueType); + + return $isList ? AccessoryArrayListType::intersectWith($arrayType) : $arrayType; + } + + // Override $add_empty option + $addEmpty = false; + + $keysType = $inputConstantArrayType; + $inputKeysList = array_map(static fn ($type) => $type->getValue(), $inputConstantArrayType->getKeyTypes()); + $filterTypesMap = array_fill_keys($inputKeysList, $filterArgType); + $inputTypesMap = array_combine($inputKeysList, $inputConstantArrayType->getValueTypes()); + $optionalKeys = []; + foreach ($inputConstantArrayType->getOptionalKeys() as $index) { + if (!isset($inputKeysList[$index])) { + continue; + } + + $optionalKeys[] = $inputKeysList[$index]; + } + } elseif ($filterConstantArrayType === null) { + if ($inputConstantArrayType === null) { + $isList = $inputArgType->isList()->yes(); + $valueType = $this->filterFunctionReturnTypeHelper->getType($inputArgType, $filterArgType, null); + + $arrayType = new ArrayType( + $inputArgType->getIterableKeyType(), + $addEmpty ? TypeCombinator::addNull($valueType) : $valueType, + ); + + return $isList ? AccessoryArrayListType::intersectWith($arrayType) : $arrayType; + } + + return null; + } else { + $keysType = $filterConstantArrayType; + $filterKeyTypes = $filterConstantArrayType->getKeyTypes(); + $filterKeysList = array_map(static fn ($type) => $type->getValue(), $filterKeyTypes); + $filterTypesMap = array_combine($filterKeysList, $keysType->getValueTypes()); + + if ($inputConstantArrayType !== null) { + $inputKeysList = array_map(static fn ($type) => $type->getValue(), $inputConstantArrayType->getKeyTypes()); + $inputTypesMap = array_combine($inputKeysList, $inputConstantArrayType->getValueTypes()); + + $optionalKeys = []; + foreach ($inputConstantArrayType->getOptionalKeys() as $index) { + if (!isset($inputKeysList[$index])) { + continue; + } + + $optionalKeys[] = $inputKeysList[$index]; + } + } else { + $optionalKeys = $filterKeysList; + $inputTypesMap = array_fill_keys($optionalKeys, $inputArgType->getIterableValueType()); + } + } + + foreach ($keysType->getKeyTypes() as $keyType) { + $optional = false; + $key = $keyType->getValue(); + $inputType = $inputTypesMap[$key] ?? null; + if ($inputType === null) { + if ($addEmpty) { + $valueTypesBuilder->setOffsetValueType($keyType, new NullType()); + } + + continue; + } + + [$filterType, $flagsType] = $this->fetchFilter($filterTypesMap[$key] ?? new MixedType()); + $valueType = $this->filterFunctionReturnTypeHelper->getType($inputType, $filterType, $flagsType); + + if (in_array($key, $optionalKeys, true)) { + if ($addEmpty) { + $valueType = TypeCombinator::addNull($valueType); + } else { + $optional = true; + } + } + + $valueTypesBuilder->setOffsetValueType($keyType, $valueType, $optional); + } + + return $valueTypesBuilder->getArray(); + } + + /** @return array{?Type, ?Type} */ + public function fetchFilter(Type $type): array + { + if (!$type->isArray()->yes()) { + return [$type, null]; + } + + $filterKey = new ConstantStringType('filter'); + if (!$type->hasOffsetValueType($filterKey)->yes()) { + return [$type, null]; + } + + $filterOffsetType = $type->getOffsetValueType($filterKey); + $filterType = null; + + if (count($filterOffsetType->getConstantScalarTypes()) > 0) { + $filterType = TypeCombinator::union(...$filterOffsetType->getConstantScalarTypes()); + } + + return [$filterType, $type]; + } + +} diff --git a/src/Type/Php/FilterVarDynamicReturnTypeExtension.php b/src/Type/Php/FilterVarDynamicReturnTypeExtension.php index c85073f6f8..438d6440e9 100644 --- a/src/Type/Php/FilterVarDynamicReturnTypeExtension.php +++ b/src/Type/Php/FilterVarDynamicReturnTypeExtension.php @@ -2,132 +2,19 @@ namespace PHPStan\Type\Php; -use PhpParser\Node; -use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\ShouldNotHappenException; -use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; -use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; -use PHPStan\Type\ArrayType; -use PHPStan\Type\BooleanType; -use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantIntegerType; -use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\ConstantScalarType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\ErrorType; -use PHPStan\Type\FloatType; -use PHPStan\Type\IntegerRangeType; -use PHPStan\Type\IntegerType; -use PHPStan\Type\MixedType; -use PHPStan\Type\NullType; -use PHPStan\Type\StringType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use function hexdec; -use function is_int; -use function octdec; -use function preg_match; -use function sprintf; +use function count; use function strtolower; class FilterVarDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - /** - * All validation filters match 0x100. - */ - private const VALIDATION_FILTER_BITMASK = 0x100; - - private ConstantStringType $flagsString; - - /** @var array|null */ - private ?array $filterTypeMap = null; - - /** @var array>|null */ - private ?array $filterTypeOptions = null; - - public function __construct(private ReflectionProvider $reflectionProvider) + public function __construct(private FilterFunctionReturnTypeHelper $filterFunctionReturnTypeHelper) { - $this->flagsString = new ConstantStringType('flags'); - } - - /** - * @return array - */ - private function getFilterTypeMap(): array - { - if ($this->filterTypeMap !== null) { - return $this->filterTypeMap; - } - - $booleanType = new BooleanType(); - $floatType = new FloatType(); - $intType = new IntegerType(); - $stringType = new StringType(); - - $this->filterTypeMap = [ - $this->getConstant('FILTER_UNSAFE_RAW') => $stringType, - $this->getConstant('FILTER_SANITIZE_EMAIL') => $stringType, - $this->getConstant('FILTER_SANITIZE_ENCODED') => $stringType, - $this->getConstant('FILTER_SANITIZE_NUMBER_FLOAT') => $stringType, - $this->getConstant('FILTER_SANITIZE_NUMBER_INT') => $stringType, - $this->getConstant('FILTER_SANITIZE_SPECIAL_CHARS') => $stringType, - $this->getConstant('FILTER_SANITIZE_STRING') => $stringType, - $this->getConstant('FILTER_SANITIZE_URL') => $stringType, - $this->getConstant('FILTER_VALIDATE_BOOLEAN') => $booleanType, - $this->getConstant('FILTER_VALIDATE_DOMAIN') => $stringType, - $this->getConstant('FILTER_VALIDATE_EMAIL') => $stringType, - $this->getConstant('FILTER_VALIDATE_FLOAT') => $floatType, - $this->getConstant('FILTER_VALIDATE_INT') => $intType, - $this->getConstant('FILTER_VALIDATE_IP') => $stringType, - $this->getConstant('FILTER_VALIDATE_MAC') => $stringType, - $this->getConstant('FILTER_VALIDATE_REGEXP') => $stringType, - $this->getConstant('FILTER_VALIDATE_URL') => $stringType, - ]; - - if ($this->reflectionProvider->hasConstant(new Node\Name('FILTER_SANITIZE_MAGIC_QUOTES'), null)) { - $this->filterTypeMap[$this->getConstant('FILTER_SANITIZE_MAGIC_QUOTES')] = $stringType; - } - - if ($this->reflectionProvider->hasConstant(new Node\Name('FILTER_SANITIZE_ADD_SLASHES'), null)) { - $this->filterTypeMap[$this->getConstant('FILTER_SANITIZE_ADD_SLASHES')] = $stringType; - } - - return $this->filterTypeMap; - } - - /** - * @return array> - */ - private function getFilterTypeOptions(): array - { - if ($this->filterTypeOptions !== null) { - return $this->filterTypeOptions; - } - - $this->filterTypeOptions = [ - $this->getConstant('FILTER_VALIDATE_INT') => ['min_range', 'max_range'], - // PHPStan does not yet support FloatRangeType - // $this->getConstant('FILTER_VALIDATE_FLOAT') => ['min_range', 'max_range'], - ]; - - return $this->filterTypeOptions; - } - - private function getConstant(string $constantName): int - { - $constant = $this->reflectionProvider->getConstant(new Node\Name($constantName), null); - $valueType = $constant->getValueType(); - if (!$valueType instanceof ConstantIntegerType) { - throw new ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); - } - - return $valueType->getValue(); } public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -135,207 +22,17 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return strtolower($functionReflection->getName()) === 'filter_var'; } - public function getTypeFromFunctionCall( - FunctionReflection $functionReflection, - FuncCall $functionCall, - Scope $scope, - ): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $mixedType = new MixedType(); - - $filterArg = $functionCall->getArgs()[1] ?? null; - if ($filterArg === null) { - $filterValue = $this->getConstant('FILTER_DEFAULT'); - } else { - $filterType = $scope->getType($filterArg->value); - if (!$filterType instanceof ConstantIntegerType) { - return $mixedType; - } - $filterValue = $filterType->getValue(); + if (count($functionCall->getArgs()) < 1) { + return null; } - $flagsArg = $functionCall->getArgs()[2] ?? null; $inputType = $scope->getType($functionCall->getArgs()[0]->value); + $filterType = isset($functionCall->getArgs()[1]) ? $scope->getType($functionCall->getArgs()[1]->value) : null; + $flagsType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null; - $defaultType = $this->hasFlag($this->getConstant('FILTER_NULL_ON_FAILURE'), $flagsArg, $scope) - ? new NullType() - : new ConstantBooleanType(false); - $exactType = $this->determineExactType($inputType, $filterValue, $defaultType, $flagsArg, $scope); - $type = $exactType ?? $this->getFilterTypeMap()[$filterValue] ?? $mixedType; - - $typeOptionNames = $this->getFilterTypeOptions()[$filterValue] ?? []; - $otherTypes = $this->getOtherTypes($flagsArg, $scope, $typeOptionNames, $defaultType); - - if ($inputType->isNonEmptyString()->yes() - && $type->isString()->yes() - && !$this->canStringBeSanitized($filterValue, $flagsArg, $scope)) { - $accessory = new AccessoryNonEmptyStringType(); - if ($inputType->isNonFalsyString()->yes()) { - $accessory = new AccessoryNonFalsyStringType(); - } - $type = TypeCombinator::intersect($type, $accessory); - } - - if (isset($otherTypes['range'])) { - if ($type instanceof ConstantScalarType) { - if ($otherTypes['range']->isSuperTypeOf($type)->no()) { - $type = $otherTypes['default']; - } - - unset($otherTypes['default']); - } else { - $type = $otherTypes['range']; - } - } - - if ($exactType !== null) { - unset($otherTypes['default']); - } - - if (isset($otherTypes['default']) && $otherTypes['default']->isSuperTypeOf($type)->no()) { - $type = TypeCombinator::union($type, $otherTypes['default']); - } - - if ($this->hasFlag($this->getConstant('FILTER_FORCE_ARRAY'), $flagsArg, $scope)) { - return new ArrayType(new MixedType(), $type); - } - - return $type; - } - - private function determineExactType(Type $in, int $filterValue, Type $defaultType, ?Arg $flagsArg, Scope $scope): ?Type - { - if (($filterValue === $this->getConstant('FILTER_VALIDATE_BOOLEAN') && $in instanceof BooleanType) - || ($filterValue === $this->getConstant('FILTER_VALIDATE_INT') && $in instanceof IntegerType) - || ($filterValue === $this->getConstant('FILTER_VALIDATE_FLOAT') && $in instanceof FloatType)) { - return $in; - } - - if ($filterValue === $this->getConstant('FILTER_VALIDATE_INT') && $in instanceof ConstantStringType) { - $value = $in->getValue(); - $allowOctal = $this->hasFlag($this->getConstant('FILTER_FLAG_ALLOW_OCTAL'), $flagsArg, $scope); - $allowHex = $this->hasFlag($this->getConstant('FILTER_FLAG_ALLOW_HEX'), $flagsArg, $scope); - - if ($allowOctal && preg_match('/\A0[oO][0-7]+\z/', $value) === 1) { - $octalValue = octdec($value); - return is_int($octalValue) ? new ConstantIntegerType($octalValue) : $defaultType; - } - - if ($allowHex && preg_match('/\A0[xX][0-9A-Fa-f]+\z/', $value) === 1) { - $hexValue = hexdec($value); - return is_int($hexValue) ? new ConstantIntegerType($hexValue) : $defaultType; - } - - return preg_match('/\A[+-]?(?:0|[1-9][0-9]*)\z/', $value) === 1 ? $in->toInteger() : $defaultType; - } - - if ($filterValue === $this->getConstant('FILTER_VALIDATE_FLOAT') && $in instanceof IntegerType) { - return $in->toFloat(); - } - - return null; - } - - /** - * @param list $typeOptionNames - * @return array{default: Type, range?: Type} - */ - private function getOtherTypes(?Node\Arg $flagsArg, Scope $scope, array $typeOptionNames, Type $defaultType): array - { - $falseType = new ConstantBooleanType(false); - if ($flagsArg === null) { - return ['default' => $falseType]; - } - - $typeOptions = $this->getOptions($flagsArg, $scope, 'default', ...$typeOptionNames); - $defaultType = $typeOptions['default'] ?? $defaultType; - $otherTypes = ['default' => $defaultType]; - $range = []; - if (isset($typeOptions['min_range'])) { - if ($typeOptions['min_range'] instanceof ConstantScalarType) { - $range['min'] = $typeOptions['min_range']->getValue(); - } elseif ($typeOptions['min_range'] instanceof IntegerRangeType) { - $range['min'] = $typeOptions['min_range']->getMin(); - } - } - if (isset($typeOptions['max_range'])) { - if ($typeOptions['max_range'] instanceof ConstantScalarType) { - $range['max'] = $typeOptions['max_range']->getValue(); - } elseif ($typeOptions['max_range'] instanceof IntegerRangeType) { - $range['max'] = $typeOptions['max_range']->getMax(); - } - } - - if (isset($range['min']) || isset($range['max'])) { - $min = isset($range['min']) && is_int($range['min']) ? $range['min'] : null; - $max = isset($range['max']) && is_int($range['max']) ? $range['max'] : null; - $otherTypes['range'] = IntegerRangeType::fromInterval($min, $max); - } - - return $otherTypes; - } - - /** - * @return array - */ - private function getOptions(Node\Arg $expression, Scope $scope, string ...$optionNames): array - { - $options = []; - - $exprType = $scope->getType($expression->value); - if (!$exprType instanceof ConstantArrayType) { - return $options; - } - - $optionsType = $exprType->getOffsetValueType(new ConstantStringType('options')); - if (!$optionsType instanceof ConstantArrayType) { - return $options; - } - - foreach ($optionNames as $optionName) { - $type = $optionsType->getOffsetValueType(new ConstantStringType($optionName)); - $options[$optionName] = $type instanceof ErrorType ? null : $type; - } - - return $options; - } - - private function hasFlag(int $flag, ?Node\Arg $expression, Scope $scope): bool - { - if ($expression === null) { - return false; - } - - $type = $this->getFlagsValue($scope->getType($expression->value)); - - return $type instanceof ConstantIntegerType && ($type->getValue() & $flag) === $flag; - } - - private function getFlagsValue(Type $exprType): Type - { - if (!$exprType instanceof ConstantArrayType) { - return $exprType; - } - - return $exprType->getOffsetValueType($this->flagsString); - } - - private function canStringBeSanitized(int $filterValue, ?Node\Arg $flagsArg, Scope $scope): bool - { - // If it is a validation filter, the string will not be changed - if (($filterValue & self::VALIDATION_FILTER_BITMASK) !== 0) { - return false; - } - - // FILTER_DEFAULT will not sanitize, unless it has FILTER_FLAG_STRIP_LOW, - // FILTER_FLAG_STRIP_HIGH, or FILTER_FLAG_STRIP_BACKTICK - if ($filterValue === $this->getConstant('FILTER_DEFAULT')) { - return $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_LOW'), $flagsArg, $scope) - || $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_HIGH'), $flagsArg, $scope) - || $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_BACKTICK'), $flagsArg, $scope); - } - - return true; + return $this->filterFunctionReturnTypeHelper->getType($inputType, $filterType, $flagsType); } } diff --git a/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php index 12df64cf17..de7e8dfe40 100644 --- a/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php @@ -29,7 +29,7 @@ public function isFunctionSupported( TypeSpecifierContext $context, ): bool { - return $functionReflection->getName() === 'function_exists' && isset($node->getArgs()[0]) && $context->truthy(); + return $functionReflection->getName() === 'function_exists' && isset($node->getArgs()[0]) && $context->true(); } public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes diff --git a/src/Type/Php/GetCalledClassDynamicReturnTypeExtension.php b/src/Type/Php/GetCalledClassDynamicReturnTypeExtension.php index 77381c798c..c570e8ec86 100644 --- a/src/Type/Php/GetCalledClassDynamicReturnTypeExtension.php +++ b/src/Type/Php/GetCalledClassDynamicReturnTypeExtension.php @@ -2,11 +2,12 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; @@ -20,9 +21,8 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $classContext = $scope->getClassReflection(); - if ($classContext !== null) { - return new ConstantStringType($classContext->getName(), true); + if ($scope->isInClass()) { + return $scope->getType(new ClassConstFetch(new Name('static'), 'class')); } return new ConstantBooleanType(false); } diff --git a/src/Type/Php/GetClassDynamicReturnTypeExtension.php b/src/Type/Php/GetClassDynamicReturnTypeExtension.php index 863871f75b..a6c2fdfdb5 100644 --- a/src/Type/Php/GetClassDynamicReturnTypeExtension.php +++ b/src/Type/Php/GetClassDynamicReturnTypeExtension.php @@ -19,7 +19,7 @@ use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; -use PHPStan\Type\TypeWithClassName; +use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use function count; @@ -34,7 +34,12 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { $args = $functionCall->getArgs(); + if (count($args) === 0) { + if ($scope->isInTrait()) { + return new ClassStringType(); + } + if ($scope->isInClass()) { return new ConstantStringType($scope->getClassReflection()->getName(), true); } @@ -44,6 +49,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $argType = $scope->getType($args[0]->value); + if ($scope->isInTrait() && TypeUtils::findThisType($argType) !== null) { + return new ClassStringType(); + } + return TypeTraverser::map( $argType, static function (Type $type, callable $traverse): Type { @@ -55,7 +64,8 @@ static function (Type $type, callable $traverse): Type { return new GenericClassStringType(new ObjectType($type->getClassName())); } - if ($type instanceof TemplateType && !$type instanceof TypeWithClassName) { + $objectClassNames = $type->getObjectClassNames(); + if ($type instanceof TemplateType && $objectClassNames === []) { if ($type instanceof ObjectWithoutClassType) { return new GenericClassStringType($type); } @@ -71,7 +81,7 @@ static function (Type $type, callable $traverse): Type { ]); } elseif ($type instanceof StaticType) { return new GenericClassStringType($type->getStaticObjectType()); - } elseif ($type instanceof TypeWithClassName) { + } elseif ($objectClassNames !== []) { return new GenericClassStringType($type); } elseif ($type instanceof ObjectWithoutClassType) { return new ClassStringType(); diff --git a/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php b/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php index 2ba8086d47..3dc2dbba5b 100644 --- a/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -37,14 +36,11 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle( - $functionReflection->getVariants(), - )->getReturnType(); if (count($functionCall->getArgs()) === 0) { if ($scope->isInTrait()) { - return $defaultReturnType; + return null; } if ($scope->isInClass()) { return $this->findParentClassType( @@ -57,20 +53,20 @@ public function getTypeFromFunctionCall( $argType = $scope->getType($functionCall->getArgs()[0]->value); if ($scope->isInTrait() && TypeUtils::findThisType($argType) !== null) { - return $defaultReturnType; + return null; } - $constantStrings = TypeUtils::getConstantStrings($argType); + $constantStrings = $argType->getConstantStrings(); if (count($constantStrings) > 0) { return TypeCombinator::union(...array_map(fn (ConstantStringType $stringType): Type => $this->findParentClassNameType($stringType->getValue()), $constantStrings)); } - $classNames = TypeUtils::getDirectClassNames($argType); + $classNames = $argType->getObjectClassNames(); if (count($classNames) > 0) { return TypeCombinator::union(...array_map(fn (string $classNames): Type => $this->findParentClassNameType($classNames), $classNames)); } - return $defaultReturnType; + return null; } private function findParentClassNameType(string $className): Type @@ -82,7 +78,15 @@ private function findParentClassNameType(string $className): Type ]); } - return $this->findParentClassType($this->reflectionProvider->getClass($className)); + $classReflection = $this->reflectionProvider->getClass($className); + if ($classReflection->isInterface()) { + return new UnionType([ + new ClassStringType(), + new ConstantBooleanType(false), + ]); + } + + return $this->findParentClassType($classReflection); } private function findParentClassType( diff --git a/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php b/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php index 92ff653718..eb396ad797 100644 --- a/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php @@ -7,7 +7,6 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\FloatType; @@ -44,8 +43,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $argType = $scope->getType($functionCall->getArgs()[0]->value); - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); + $isTrueType = $argType->isTrue(); + $isFalseType = $argType->isFalse(); $compareTypes = $isTrueType->compareTo($isFalseType); if ($compareTypes === $isTrueType) { return $floatType; diff --git a/src/Type/Php/GettypeFunctionReturnTypeExtension.php b/src/Type/Php/GettypeFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..566faa3243 --- /dev/null +++ b/src/Type/Php/GettypeFunctionReturnTypeExtension.php @@ -0,0 +1,90 @@ +getName() === 'gettype'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 1) { + return null; + } + + $valueType = $scope->getType($functionCall->getArgs()[0]->value); + + return TypeTraverser::map($valueType, static function (Type $valueType, callable $traverse): Type { + if ($valueType instanceof UnionType || $valueType instanceof IntersectionType) { + return $traverse($valueType); + } + + if ($valueType->isString()->yes()) { + return new ConstantStringType('string'); + } + if ($valueType->isArray()->yes()) { + return new ConstantStringType('array'); + } + + if ($valueType->isBoolean()->yes()) { + return new ConstantStringType('boolean'); + } + + $resource = new ResourceType(); + if ($resource->isSuperTypeOf($valueType)->yes()) { + return new UnionType([ + new ConstantStringType('resource'), + new ConstantStringType('resource (closed)'), + ]); + } + + if ($valueType->isInteger()->yes()) { + return new ConstantStringType('integer'); + } + + if ($valueType->isFloat()->yes()) { + // for historical reasons "double" is returned in case of a float, and not simply "float" + return new ConstantStringType('double'); + } + + if ($valueType->isNull()->yes()) { + return new ConstantStringType('NULL'); + } + + if ($valueType->isObject()->yes()) { + return new ConstantStringType('object'); + } + + return TypeCombinator::union( + new ConstantStringType('string'), + new ConstantStringType('array'), + new ConstantStringType('boolean'), + new ConstantStringType('resource'), + new ConstantStringType('resource (closed)'), + new ConstantStringType('integer'), + new ConstantStringType('double'), + new ConstantStringType('NULL'), + new ConstantStringType('object'), + new ConstantStringType('unknown type'), + ); + }); + } + +} diff --git a/src/Type/Php/HashFunctionsReturnTypeExtension.php b/src/Type/Php/HashFunctionsReturnTypeExtension.php index a1c6d2e06e..72b26d3dd5 100644 --- a/src/Type/Php/HashFunctionsReturnTypeExtension.php +++ b/src/Type/Php/HashFunctionsReturnTypeExtension.php @@ -99,7 +99,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return TypeUtils::toBenevolentUnion($defaultReturnType); } - $constantAlgorithmTypes = TypeUtils::getConstantStrings($algorithmType); + $constantAlgorithmTypes = $algorithmType->getConstantStrings(); if ($constantAlgorithmTypes === []) { return TypeUtils::toBenevolentUnion($defaultReturnType); diff --git a/src/Type/Php/HrtimeFunctionReturnTypeExtension.php b/src/Type/Php/HrtimeFunctionReturnTypeExtension.php index 2192fc8745..47dc50fee9 100644 --- a/src/Type/Php/HrtimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/HrtimeFunctionReturnTypeExtension.php @@ -5,8 +5,8 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; +use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\FloatType; @@ -26,7 +26,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $arrayType = new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new IntegerType(), new IntegerType()], [2]); + $arrayType = new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new IntegerType(), new IntegerType()], [2], [], TrinaryLogic::createYes()); $numberType = TypeUtils::toBenevolentUnion(TypeCombinator::union(new IntegerType(), new FloatType())); if (count($functionCall->getArgs()) < 1) { @@ -34,8 +34,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $argType = $scope->getType($functionCall->getArgs()[0]->value); - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); + $isTrueType = $argType->isTrue(); + $isFalseType = $argType->isFalse(); $compareTypes = $isTrueType->compareTo($isFalseType); if ($compareTypes === $isTrueType) { return $numberType; diff --git a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php index f66b7f6324..0daed61c52 100644 --- a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php +++ b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php @@ -57,10 +57,23 @@ public function getTypeFromFunctionCall( private function implode(Type $arrayType, Type $separatorType): Type { - if ($arrayType instanceof ConstantArrayType && $separatorType instanceof ConstantStringType) { - $constantType = $this->inferConstantType($arrayType, $separatorType); - if ($constantType !== null) { - return $constantType; + if (count($arrayType->getConstantArrays()) > 0 && count($separatorType->getConstantStrings()) > 0) { + $result = []; + foreach ($separatorType->getConstantStrings() as $separator) { + foreach ($arrayType->getConstantArrays() as $constantArray) { + $constantType = $this->inferConstantType($constantArray, $separator); + if ($constantType !== null) { + $result[] = $constantType; + continue; + } + + $result = []; + break 2; + } + } + + if (count($result) > 0) { + return TypeCombinator::union(...$result); } } diff --git a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php index d485cd20d2..b23663a994 100644 --- a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php @@ -2,16 +2,18 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr\Array_; +use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Node\Expr\AlwaysRememberedExpr; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\MixedType; use PHPStan\Type\TypeCombinator; @@ -37,40 +39,97 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - if (count($node->getArgs()) < 3) { + $argsCount = count($node->getArgs()); + if ($argsCount < 2) { return new SpecifiedTypes(); } - $strictNodeType = $scope->getType($node->getArgs()[2]->value); - if (!(new ConstantBooleanType(true))->isSuperTypeOf($strictNodeType)->yes()) { - return new SpecifiedTypes(); + + $isStrictComparison = false; + if ($argsCount >= 3) { + $strictNodeType = $scope->getType($node->getArgs()[2]->value); + $isStrictComparison = $strictNodeType->isTrue()->yes(); + } + + $needleExpr = $node->getArgs()[0]->value; + $arrayExpr = $node->getArgs()[1]->value; + if ($arrayExpr instanceof Array_ && $isStrictComparison) { + $types = null; + foreach ($arrayExpr->items as $item) { + if ($item === null) { + continue; + } + if ($item->unpack) { + $types = null; + break; + } + $itemTypes = $this->typeSpecifier->resolveIdentical(new Identical($needleExpr, $item->value), $scope, $context, null); + + if ($types === null) { + $types = $itemTypes; + continue; + } + + $types = $context->true() ? $types->normalize($scope)->intersectWith($itemTypes->normalize($scope)) : $types->unionWith($itemTypes); + } + + if ($types !== null) { + return $types; + } } - $needleType = $scope->getType($node->getArgs()[0]->value); - $arrayType = $scope->getType($node->getArgs()[1]->value); + $needleType = $scope->getType($needleExpr); + $arrayType = $scope->getType($arrayExpr); $arrayValueType = $arrayType->getIterableValueType(); + $isStrictComparison = $isStrictComparison + || $needleType->isEnum()->yes() + || $arrayValueType->isEnum()->yes(); + + if (!$isStrictComparison) { + return new SpecifiedTypes(); + } + $specifiedTypes = new SpecifiedTypes(); if ( - $context->truthy() - || count(TypeUtils::getConstantScalars($arrayValueType)) > 0 - || count(TypeUtils::getEnumCaseObjects($arrayValueType)) > 0 + $context->true() + || ( + $context->false() + && ( + count(TypeUtils::getConstantScalars($arrayValueType)) > 0 + || count(TypeUtils::getEnumCaseObjects($arrayValueType)) > 0 + ) + ) ) { $specifiedTypes = $this->typeSpecifier->create( - $node->getArgs()[0]->value, + $needleExpr, $arrayValueType, $context, false, $scope, ); + if ($needleExpr instanceof AlwaysRememberedExpr) { + $specifiedTypes = $specifiedTypes->unionWith($this->typeSpecifier->create( + $needleExpr->getExpr(), + $arrayValueType, + $context, + false, + $scope, + )); + } } if ( - $context->truthy() - || count(TypeUtils::getConstantScalars($needleType)) > 0 - || count(TypeUtils::getEnumCaseObjects($needleType)) > 0 + $context->true() + || ( + $context->false() + && ( + count(TypeUtils::getConstantScalars($needleType)) === 1 + || count(TypeUtils::getEnumCaseObjects($needleType)) === 1 + ) + ) ) { - if ($context->truthy()) { + if ($context->true()) { $arrayValueType = TypeCombinator::union($arrayValueType, $needleType); } else { $arrayValueType = TypeCombinator::remove($arrayValueType, $needleType); @@ -85,7 +144,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n )); } - if ($context->truthy() && $arrayType->isArray()->yes()) { + if ($context->true() && $arrayType->isArray()->yes()) { $specifiedTypes = $specifiedTypes->unionWith($this->typeSpecifier->create( $node->getArgs()[1]->value, TypeCombinator::intersect($arrayType, new NonEmptyArrayType()), diff --git a/src/Type/Php/IniGetReturnTypeExtension.php b/src/Type/Php/IniGetReturnTypeExtension.php new file mode 100644 index 0000000000..4e4de99bc4 --- /dev/null +++ b/src/Type/Php/IniGetReturnTypeExtension.php @@ -0,0 +1,64 @@ +getName() === 'ini_get'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) < 1) { + return null; + } + + $numericString = TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType(), + ); + $types = [ + 'date.timezone' => new StringType(), + 'memory_limit' => new StringType(), + 'max_execution_time' => $numericString, + 'max_input_time' => $numericString, + 'default_socket_timeout' => $numericString, + 'precision' => $numericString, + ]; + + $argType = $scope->getType($args[0]->value); + $results = []; + foreach ($argType->getConstantStrings() as $constantString) { + if (!array_key_exists($constantString->getValue(), $types)) { + return null; + } + $results[] = $types[$constantString->getValue()]; + } + + if (count($results) > 0) { + return TypeCombinator::union(...$results); + } + + return null; + } + +} diff --git a/src/Type/Php/IntdivThrowTypeExtension.php b/src/Type/Php/IntdivThrowTypeExtension.php index 8b97f9265a..aab1448733 100644 --- a/src/Type/Php/IntdivThrowTypeExtension.php +++ b/src/Type/Php/IntdivThrowTypeExtension.php @@ -12,7 +12,6 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; use const PHP_INT_MIN; @@ -32,7 +31,7 @@ public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflect $containsMin = false; $valueType = $scope->getType($funcCall->getArgs()[0]->value); - foreach (TypeUtils::getConstantScalars($valueType) as $constantScalarType) { + foreach ($valueType->getConstantScalarTypes() as $constantScalarType) { if ($constantScalarType->getValue() === PHP_INT_MIN) { $containsMin = true; } @@ -46,7 +45,7 @@ public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflect $divisionByZero = false; $divisorType = $scope->getType($funcCall->getArgs()[1]->value); - foreach (TypeUtils::getConstantScalars($divisorType) as $constantScalarType) { + foreach ($divisorType->getConstantScalarTypes() as $constantScalarType) { if ($containsMin && $constantScalarType->getValue() === -1) { return new ObjectType(ArithmeticError::class); } diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php index 2e03074b37..0eb26199df 100644 --- a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php @@ -37,15 +37,16 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n if (count($node->getArgs()) < 2) { return new SpecifiedTypes(); } - $objectOrClassType = $scope->getType($node->getArgs()[0]->value); $classType = $scope->getType($node->getArgs()[1]->value); - $allowStringType = isset($node->getArgs()[2]) ? $scope->getType($node->getArgs()[2]->value) : new ConstantBooleanType(false); - $allowString = !$allowStringType->equals(new ConstantBooleanType(false)); - if (!$classType instanceof ConstantStringType && !$context->truthy()) { + if (!$classType instanceof ConstantStringType && !$context->true()) { return new SpecifiedTypes([], []); } + $objectOrClassType = $scope->getType($node->getArgs()[0]->value); + $allowStringType = isset($node->getArgs()[2]) ? $scope->getType($node->getArgs()[2]->value) : new ConstantBooleanType(false); + $allowString = !$allowStringType->equals(new ConstantBooleanType(false)); + return $this->typeSpecifier->create( $node->getArgs()[0]->value, $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, true), diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php index c608156f84..d1f4a1bd82 100644 --- a/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php +++ b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php @@ -12,8 +12,9 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; +use function array_unique; +use function array_values; final class IsAFunctionTypeSpecifyingHelper { @@ -25,16 +26,22 @@ public function determineType( bool $allowSameClass, ): Type { - $objectOrClassTypeClassName = $this->determineClassNameFromObjectOrClassType($objectOrClassType, $allowString); + $objectOrClassTypeClassNames = $objectOrClassType->getObjectClassNames(); + if ($allowString) { + foreach ($objectOrClassType->getConstantStrings() as $constantString) { + $objectOrClassTypeClassNames[] = $constantString->getValue(); + } + $objectOrClassTypeClassNames = array_values(array_unique($objectOrClassTypeClassNames)); + } return TypeTraverser::map( $classType, - static function (Type $type, callable $traverse) use ($objectOrClassTypeClassName, $allowString, $allowSameClass): Type { + static function (Type $type, callable $traverse) use ($objectOrClassTypeClassNames, $allowString, $allowSameClass): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } if ($type instanceof ConstantStringType) { - if (!$allowSameClass && $type->getValue() === $objectOrClassTypeClassName) { + if (!$allowSameClass && $objectOrClassTypeClassNames === [$type->getValue()]) { return new NeverType(); } if ($allowString) { @@ -68,17 +75,4 @@ static function (Type $type, callable $traverse) use ($objectOrClassTypeClassNam ); } - private function determineClassNameFromObjectOrClassType(Type $type, bool $allowString): ?string - { - if ($type instanceof TypeWithClassName) { - return $type->getClassName(); - } - - if ($allowString && $type instanceof ConstantStringType) { - return $type->getValue(); - } - - return null; - } - } diff --git a/src/Type/Php/IsBoolFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsBoolFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 44a9b8b088..0000000000 --- a/src/Type/Php/IsBoolFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,45 +0,0 @@ -getName()) === 'is_bool' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new BooleanType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php index fc832c4737..38bb1f3044 100644 --- a/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php @@ -14,7 +14,6 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\ShouldNotHappenException; use PHPStan\Type\CallableType; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use function count; use function strtolower; @@ -49,7 +48,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n if ( $value instanceof Array_ && count($value->items) === 2 - && $valueType instanceof ConstantArrayType + && $valueType->isConstantArray()->yes() && !$valueType->isCallable()->no() ) { if ($value->items[0] === null || $value->items[1] === null) { diff --git a/src/Type/Php/IsCountableFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsCountableFunctionTypeSpecifyingExtension.php deleted file mode 100644 index c05579be5a..0000000000 --- a/src/Type/Php/IsCountableFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,59 +0,0 @@ -getName()) === 'is_countable' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - - return $this->typeSpecifier->create( - $node->getArgs()[0]->value, - new UnionType([ - new ArrayType(new MixedType(), new MixedType()), - new ObjectType(Countable::class), - ]), - $context, - false, - $scope, - ); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsFloatFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsFloatFunctionTypeSpecifyingExtension.php deleted file mode 100644 index de8c3f6cbd..0000000000 --- a/src/Type/Php/IsFloatFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,50 +0,0 @@ -getName()), [ - 'is_float', - 'is_double', - 'is_real', - ], true) - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new FloatType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsIntFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsIntFunctionTypeSpecifyingExtension.php deleted file mode 100644 index a5504cf663..0000000000 --- a/src/Type/Php/IsIntFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,50 +0,0 @@ -getName()), [ - 'is_int', - 'is_integer', - 'is_long', - ], true) - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new IntegerType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsNullFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsNullFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 403ee6c337..0000000000 --- a/src/Type/Php/IsNullFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,46 +0,0 @@ -getName()) === 'is_null' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new NullType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsNumericFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsNumericFunctionTypeSpecifyingExtension.php deleted file mode 100644 index ccc1691e54..0000000000 --- a/src/Type/Php/IsNumericFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,58 +0,0 @@ -getName() === 'is_numeric' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - $numericTypes = [ - new IntegerType(), - new FloatType(), - new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]), - ]; - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new UnionType($numericTypes), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsObjectFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsObjectFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 33bd282b65..0000000000 --- a/src/Type/Php/IsObjectFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,46 +0,0 @@ -getName()) === 'is_object' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new ObjectWithoutClassType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsResourceFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsResourceFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 392fb1159b..0000000000 --- a/src/Type/Php/IsResourceFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,46 +0,0 @@ -getName()) === 'is_resource' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new ResourceType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsScalarFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsScalarFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 33ce803d01..0000000000 --- a/src/Type/Php/IsScalarFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,54 +0,0 @@ -getName() === 'is_scalar' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new UnionType([ - new StringType(), - new IntegerType(), - new FloatType(), - new BooleanType(), - ]), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsStringFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsStringFunctionTypeSpecifyingExtension.php deleted file mode 100644 index 7e307545b6..0000000000 --- a/src/Type/Php/IsStringFunctionTypeSpecifyingExtension.php +++ /dev/null @@ -1,46 +0,0 @@ -getName()) === 'is_string' - && !$context->null(); - } - - public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - throw new ShouldNotHappenException(); - } - - if (!isset($node->getArgs()[0])) { - return new SpecifiedTypes(); - } - - return $this->typeSpecifier->create($node->getArgs()[0]->value, new StringType(), $context, false, $scope); - } - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - -} diff --git a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php index b7f2bb0636..3864db74e5 100644 --- a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php @@ -34,7 +34,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - if (!$context->truthy() || count($node->getArgs()) < 2) { + if (!$context->true() || count($node->getArgs()) < 2) { return new SpecifiedTypes(); } diff --git a/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php index fe8be93631..3eba789175 100644 --- a/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php +++ b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php @@ -5,9 +5,8 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerType; use PHPStan\Type\Type; @@ -21,29 +20,37 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return strtolower($functionReflection->getName()) === 'iterator_to_array'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { $arguments = $functionCall->getArgs(); if ($arguments === []) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $traversableType = $scope->getType($arguments[0]->value); $arrayKeyType = $traversableType->getIterableKeyType(); + $isList = false; if (isset($arguments[1])) { $preserveKeysType = $scope->getType($arguments[1]->value); - if ($preserveKeysType instanceof ConstantBooleanType && !$preserveKeysType->getValue()) { + if ($preserveKeysType->isFalse()->yes()) { $arrayKeyType = new IntegerType(); + $isList = true; } } - return new ArrayType( + $arrayType = new ArrayType( $arrayKeyType, $traversableType->getIterableValueType(), ); + + if ($isList) { + $arrayType = AccessoryArrayListType::intersectWith($arrayType); + } + + return $arrayType; } } diff --git a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php index cf653e182e..be7c236364 100644 --- a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php @@ -45,7 +45,7 @@ public function isFunctionSupported( return true; } - return $this->reflectionProvider->hasConstant(new FullyQualified('JSON_THROW_ON_ERROR'), null) && $functionReflection->getName() === 'json_encode'; + return $functionReflection->getName() === 'json_encode' && $this->reflectionProvider->hasConstant(new FullyQualified('JSON_THROW_ON_ERROR'), null); } public function getTypeFromFunctionCall( diff --git a/src/Type/Php/JsonThrowTypeExtension.php b/src/Type/Php/JsonThrowTypeExtension.php index 7b1cb81289..29eaf4f290 100644 --- a/src/Type/Php/JsonThrowTypeExtension.php +++ b/src/Type/Php/JsonThrowTypeExtension.php @@ -16,8 +16,7 @@ class JsonThrowTypeExtension implements DynamicFunctionThrowTypeExtension { - /** @var array */ - private array $argumentPositions = [ + private const ARGUMENTS_POSITIONS = [ 'json_encode' => 1, 'json_decode' => 3, ]; @@ -33,14 +32,14 @@ public function isFunctionSupported( FunctionReflection $functionReflection, ): bool { - return $this->reflectionProvider->hasConstant(new Name\FullyQualified('JSON_THROW_ON_ERROR'), null) && in_array( + return in_array( $functionReflection->getName(), [ 'json_encode', 'json_decode', ], true, - ); + ) && $this->reflectionProvider->hasConstant(new Name\FullyQualified('JSON_THROW_ON_ERROR'), null); } public function getThrowTypeFromFunctionCall( @@ -49,13 +48,13 @@ public function getThrowTypeFromFunctionCall( Scope $scope, ): ?Type { - $argumentPosition = $this->argumentPositions[$functionReflection->getName()]; + $argumentPosition = self::ARGUMENTS_POSITIONS[$functionReflection->getName()]; if (!isset($functionCall->getArgs()[$argumentPosition])) { return null; } $optionsExpr = $functionCall->getArgs()[$argumentPosition]->value; - if ($this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($optionsExpr, $scope, 'JSON_THROW_ON_ERROR')->yes()) { + if (!$this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($optionsExpr, $scope, 'JSON_THROW_ON_ERROR')->no()) { return new ObjectType('JsonException'); } diff --git a/src/Type/Php/LtrimFunctionReturnTypeExtension.php b/src/Type/Php/LtrimFunctionReturnTypeExtension.php index a70f736661..964d605b27 100644 --- a/src/Type/Php/LtrimFunctionReturnTypeExtension.php +++ b/src/Type/Php/LtrimFunctionReturnTypeExtension.php @@ -29,14 +29,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $string = $scope->getType($functionCall->getArgs()[0]->value); $trimChars = $scope->getType($functionCall->getArgs()[1]->value); - if ($trimChars instanceof ConstantStringType && $trimChars->getValue() === '\\') { - if ($string instanceof ConstantStringType && $string->isClassString()) { + if ($trimChars instanceof ConstantStringType && $trimChars->getValue() === '\\' && $string->isClassStringType()->yes()) { + if ($string instanceof ConstantStringType) { return new ConstantStringType(ltrim($string->getValue(), $trimChars->getValue()), true); } - if ($string instanceof ClassStringType) { - return new ClassStringType(); - } + return new ClassStringType(); } return null; diff --git a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php index 7a2b24f9e7..ae3aa752f9 100644 --- a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php +++ b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerType; @@ -24,11 +23,10 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); if (!isset($functionCall->getArgs()[0])) { - return $defaultReturnType; + return null; } $argType = $scope->getType($functionCall->getArgs()[0]->value); @@ -41,7 +39,7 @@ public function getTypeFromFunctionCall( return new ArrayType(new IntegerType(), new StringType()); } - return $defaultReturnType; + return null; } } diff --git a/src/Type/Php/MbFunctionsReturnTypeExtension.php b/src/Type/Php/MbFunctionsReturnTypeExtension.php index f0bf08dff0..13f8d238b0 100644 --- a/src/Type/Php/MbFunctionsReturnTypeExtension.php +++ b/src/Type/Php/MbFunctionsReturnTypeExtension.php @@ -15,7 +15,6 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use function array_key_exists; use function array_map; @@ -55,7 +54,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return TypeCombinator::remove($returnType, new BooleanType()); } - $strings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[$positionEncodingParam - 1]->value)); + $strings = $scope->getType($functionCall->getArgs()[$positionEncodingParam - 1]->value)->getConstantStrings(); $results = array_unique(array_map(fn (ConstantStringType $encoding): bool => $this->isSupportedEncoding($encoding->getValue()), $strings)); if ($returnType->equals(new UnionType([new StringType(), new BooleanType()]))) { diff --git a/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php b/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php index 5373ae1544..cb246f7e3d 100644 --- a/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php +++ b/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php @@ -19,7 +19,6 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function array_map; use function array_merge; use function array_unique; @@ -54,12 +53,11 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { $args = $functionCall->getArgs(); - $returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); if (count($args) === 0) { - return $returnType; + return null; } $encodings = []; @@ -70,7 +68,7 @@ public function getTypeFromFunctionCall( } elseif (count($functionCall->getArgs()) === 2) { // custom encoding is specified $encodings = array_map( static fn (ConstantStringType $t) => $t->getValue(), - TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[1]->value)), + $scope->getType($functionCall->getArgs()[1]->value)->getConstantStrings(), ); } @@ -97,13 +95,13 @@ public function getTypeFromFunctionCall( $argType = $scope->getType($args[0]->value); if ($argType->isSuperTypeOf(new BooleanType())->yes()) { - $constantScalars = TypeUtils::getConstantScalars(TypeCombinator::remove($argType, new BooleanType())); + $constantScalars = TypeCombinator::remove($argType, new BooleanType())->getConstantScalarTypes(); if (count($constantScalars) > 0) { $constantScalars[] = new ConstantBooleanType(true); $constantScalars[] = new ConstantBooleanType(false); } } else { - $constantScalars = TypeUtils::getConstantScalars($argType); + $constantScalars = $argType->getConstantScalarTypes(); } $lengths = []; @@ -127,7 +125,6 @@ public function getTypeFromFunctionCall( } } - $range = null; $isNonEmpty = $argType->isNonEmptyString(); $numeric = TypeCombinator::union(new IntegerType(), new FloatType()); if (count($lengths) > 0) { @@ -138,7 +135,7 @@ public function getTypeFromFunctionCall( } else { $range = TypeCombinator::union(...array_map(static fn ($l) => new ConstantIntegerType($l), $lengths)); } - } elseif ((new BooleanType())->isSuperTypeOf($argType)->yes()) { + } elseif ($argType->isBoolean()->yes()) { $range = IntegerRangeType::fromInterval(0, 1); } elseif ( $isNonEmpty->yes() diff --git a/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php b/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php index a1d30c2a13..f5d0055d9b 100644 --- a/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php +++ b/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php @@ -12,9 +12,7 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; -use PHPStan\Type\IntegerType; use PHPStan\Type\NeverType; -use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function in_array; @@ -57,8 +55,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $argType = $scope->getType($functionCall->getArgs()[0]->value); $isString = $argType->isString(); - $isNull = (new NullType())->isSuperTypeOf($argType); - $isInteger = (new IntegerType())->isSuperTypeOf($argType); + $isNull = $argType->isNull(); + $isInteger = $argType->isInteger(); if ($isString->no() && $isNull->no() && $isInteger->no()) { if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { @@ -105,7 +103,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($argType instanceof ConstantStringType) { $value = strtolower($argType->getValue()); - if ($value === 'none' || $value === 'long' || $value === 'entity') { + if (in_array($value, ['none', 'long', 'entity'], true)) { return new ConstantBooleanType(true); } diff --git a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php index 1498960929..08a366a59b 100644 --- a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php @@ -14,7 +14,6 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\IntersectionType; -use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\UnionType; use function count; @@ -36,7 +35,7 @@ public function isFunctionSupported( ): bool { return $functionReflection->getName() === 'method_exists' - && $context->truthy() + && $context->true() && count($node->getArgs()) >= 2; } @@ -47,15 +46,26 @@ public function specifyTypes( TypeSpecifierContext $context, ): SpecifiedTypes { + $methodNameType = $scope->getType($node->getArgs()[1]->value); + if (!$methodNameType instanceof ConstantStringType) { + return new SpecifiedTypes([], []); + } + $objectType = $scope->getType($node->getArgs()[0]->value); - if (!$objectType instanceof ObjectType) { - if ($objectType->isString()->yes()) { - return new SpecifiedTypes([], []); + if ($objectType->isString()->yes()) { + if ($objectType->isClassStringType()->yes()) { + return $this->typeSpecifier->create( + $node->getArgs()[0]->value, + new IntersectionType([ + $objectType, + new HasMethodType($methodNameType->getValue()), + ]), + $context, + false, + $scope, + ); } - } - $methodNameType = $scope->getType($node->getArgs()[1]->value); - if (!$methodNameType instanceof ConstantStringType) { return new SpecifiedTypes([], []); } diff --git a/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php b/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php index de55a206d8..422e1355c6 100644 --- a/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\BenevolentUnionType; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\FloatType; use PHPStan\Type\MixedType; @@ -30,8 +29,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $argType = $scope->getType($functionCall->getArgs()[0]->value); - $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); - $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); + $isTrueType = $argType->isTrue(); + $isFalseType = $argType->isFalse(); $compareTypes = $isTrueType->compareTo($isFalseType); if ($compareTypes === $isTrueType) { return new FloatType(); diff --git a/src/Type/Php/MinMaxFunctionReturnTypeExtension.php b/src/Type/Php/MinMaxFunctionReturnTypeExtension.php index 4c346d3459..e1633c0b64 100644 --- a/src/Type/Php/MinMaxFunctionReturnTypeExtension.php +++ b/src/Type/Php/MinMaxFunctionReturnTypeExtension.php @@ -6,8 +6,8 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Ternary; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\ConstantScalarType; @@ -18,50 +18,34 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use function count; +use function in_array; class MinMaxFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - /** @var string[] */ - private array $functionNames = [ - 'min' => '', - 'max' => '', - ]; + public function __construct( + private PhpVersion $phpVersion, + ) + { + } public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return isset($this->functionNames[$functionReflection->getName()]); + return in_array($functionReflection->getName(), ['min', 'max'], true); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (!isset($functionCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } if (count($functionCall->getArgs()) === 1) { $argType = $scope->getType($functionCall->getArgs()[0]->value); if ($argType->isArray()->yes()) { - $isIterable = $argType->isIterableAtLeastOnce(); - if ($isIterable->no()) { - return new ConstantBooleanType(false); - } - $iterableValueType = $argType->getIterableValueType(); - $argumentTypes = []; - if (!$isIterable->yes()) { - $argumentTypes[] = new ConstantBooleanType(false); - } - if ($iterableValueType instanceof UnionType) { - foreach ($iterableValueType->getTypes() as $innerType) { - $argumentTypes[] = $innerType; - } - } else { - $argumentTypes[] = $iterableValueType; - } - - return $this->processType( + return $this->processArrayType( $functionReflection->getName(), - $argumentTypes, + $argType, ); } @@ -117,6 +101,47 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ); } + private function processArrayType(string $functionName, Type $argType): Type + { + $constArrayTypes = $argType->getConstantArrays(); + if (count($constArrayTypes) > 0) { + $resultTypes = []; + foreach ($constArrayTypes as $constArrayType) { + $isIterable = $constArrayType->isIterableAtLeastOnce(); + if ($isIterable->no() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + $resultTypes[] = new ConstantBooleanType(false); + continue; + } + $argumentTypes = []; + if (!$isIterable->yes() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + $argumentTypes[] = new ConstantBooleanType(false); + } + + foreach ($constArrayType->getValueTypes() as $innerType) { + $argumentTypes[] = $innerType; + } + + $resultTypes[] = $this->processType($functionName, $argumentTypes); + } + + return TypeCombinator::union(...$resultTypes); + } + + $isIterable = $argType->isIterableAtLeastOnce(); + if ($isIterable->no() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + return new ConstantBooleanType(false); + } + $iterableValueType = $argType->getIterableValueType(); + $argumentTypes = []; + if (!$isIterable->yes() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + $argumentTypes[] = new ConstantBooleanType(false); + } + + $argumentTypes[] = $iterableValueType; + + return $this->processType($functionName, $argumentTypes); + } + /** * @param Type[] $types */ @@ -161,7 +186,7 @@ private function compareTypes( ): ?Type { if ( - $firstType instanceof ConstantArrayType + $firstType->isConstantArray()->yes() && $secondType instanceof ConstantScalarType ) { return $secondType; @@ -169,7 +194,7 @@ private function compareTypes( if ( $firstType instanceof ConstantScalarType - && $secondType instanceof ConstantArrayType + && $secondType->isConstantArray()->yes() ) { return $firstType; } @@ -178,9 +203,9 @@ private function compareTypes( $firstType instanceof ConstantArrayType && $secondType instanceof ConstantArrayType ) { - if ($secondType->count() < $firstType->count()) { + if ($secondType->getArraySize() < $firstType->getArraySize()) { return $secondType; - } elseif ($firstType->count() < $secondType->count()) { + } elseif ($firstType->getArraySize() < $secondType->getArraySize()) { return $firstType; } diff --git a/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php b/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php index ca095acc77..cb7cf0eb07 100644 --- a/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php +++ b/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -39,11 +38,11 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { $args = $functionCall->getArgs(); if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($args[0]->value); diff --git a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php index 0bb98aaa1e..a3e2ec18b8 100644 --- a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php @@ -5,13 +5,11 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\ConstantType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\NullType; @@ -46,41 +44,45 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'parse_url'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) < 1) { - return ParametersAcceptorSelector::selectSingle( - $functionReflection->getVariants(), - )->getReturnType(); + return null; } $this->cacheReturnTypes(); - $urlType = $scope->getType($functionCall->getArgs()[0]->value); if (count($functionCall->getArgs()) > 1) { $componentType = $scope->getType($functionCall->getArgs()[1]->value); - if (!$componentType instanceof ConstantType) { + if (!$componentType->isConstantValue()->yes()) { return $this->createAllComponentsReturnType(); } $componentType = $componentType->toInteger(); if (!$componentType instanceof ConstantIntegerType) { - throw new ShouldNotHappenException(); + return $this->createAllComponentsReturnType(); } } else { $componentType = new ConstantIntegerType(-1); } - if ($urlType instanceof ConstantStringType) { - try { - $result = @parse_url($urlType->getValue(), $componentType->getValue()); - } catch (ValueError) { - return new ConstantBooleanType(false); + $urlType = $scope->getType($functionCall->getArgs()[0]->value); + if (count($urlType->getConstantStrings()) > 0) { + $types = []; + foreach ($urlType->getConstantStrings() as $constantString) { + try { + $result = @parse_url($constantString->getValue(), $componentType->getValue()); + } catch (ValueError) { + $types[] = new ConstantBooleanType(false); + continue; + } + + $types[] = $scope->getTypeFromValue($result); } - return $scope->getTypeFromValue($result); + return TypeCombinator::union(...$types); } if ($componentType->getValue() === -1) { diff --git a/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php b/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php index 1376ed4ff8..2139200e0b 100644 --- a/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php @@ -5,17 +5,25 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function count; +use function sprintf; class PathinfoFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'pathinfo'; @@ -25,24 +33,51 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, Node\Expr\FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { $argsCount = count($functionCall->getArgs()); if ($argsCount === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - } elseif ($argsCount === 1) { - $pathType = $scope->getType($functionCall->getArgs()[0]->value); + return null; + } + + $pathType = $scope->getType($functionCall->getArgs()[0]->value); + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantStringType('dirname'), new StringType(), !$pathType->isNonEmptyString()->yes()); + $builder->setOffsetValueType(new ConstantStringType('basename'), new StringType()); + $builder->setOffsetValueType(new ConstantStringType('extension'), new StringType(), true); + $builder->setOffsetValueType(new ConstantStringType('filename'), new StringType()); + $arrayType = $builder->getArray(); + + if ($argsCount === 1) { + return $arrayType; + } - $builder = ConstantArrayTypeBuilder::createEmpty(); - $builder->setOffsetValueType(new ConstantStringType('dirname'), new StringType(), !$pathType->isNonEmptyString()->yes()); - $builder->setOffsetValueType(new ConstantStringType('basename'), new StringType()); - $builder->setOffsetValueType(new ConstantStringType('extension'), new StringType(), true); - $builder->setOffsetValueType(new ConstantStringType('filename'), new StringType()); + $flagsType = $scope->getType($functionCall->getArgs()[1]->value); + if ($flagsType instanceof ConstantIntegerType) { + if ($flagsType->getValue() === $this->getConstant('PATHINFO_ALL')) { + return $arrayType; + } + + return new StringType(); + } + + return TypeCombinator::union($arrayType, new StringType()); + } + + private function getConstant(string $constantName): ?int + { + if (!$this->reflectionProvider->hasConstant(new Node\Name($constantName), null)) { + return null; + } - return $builder->getArray(); + $constant = $this->reflectionProvider->getConstant(new Node\Name($constantName), null); + $valueType = $constant->getValueType(); + if (!$valueType instanceof ConstantIntegerType) { + throw new ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); } - return new StringType(); + return $valueType->getValue(); } } diff --git a/src/Type/Php/PowFunctionReturnTypeExtension.php b/src/Type/Php/PowFunctionReturnTypeExtension.php index bbae101311..ea15dae2e8 100644 --- a/src/Type/Php/PowFunctionReturnTypeExtension.php +++ b/src/Type/Php/PowFunctionReturnTypeExtension.php @@ -2,17 +2,12 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr\BinaryOp\Pow; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\FloatType; -use PHPStan\Type\IntegerType; -use PHPStan\Type\MixedType; -use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use function count; class PowFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -23,31 +18,13 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'pow'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $defaultReturnType = new BenevolentUnionType([ - new FloatType(), - new IntegerType(), - ]); if (count($functionCall->getArgs()) < 2) { - return $defaultReturnType; + return null; } - $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); - $secondArgType = $scope->getType($functionCall->getArgs()[1]->value); - if ($firstArgType instanceof MixedType || $secondArgType instanceof MixedType) { - return $defaultReturnType; - } - - $object = new ObjectWithoutClassType(); - if ( - !$object->isSuperTypeOf($firstArgType)->no() - || !$object->isSuperTypeOf($secondArgType)->no() - ) { - return TypeCombinator::union($firstArgType, $secondArgType); - } - - return $defaultReturnType; + return $scope->getType(new Pow($functionCall->getArgs()[0]->value, $functionCall->getArgs()[1]->value)); } } diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 9aa8248c7c..6300d6887d 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -5,7 +5,8 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; use PHPStan\Type\BitwiseFlagHelper; use PHPStan\Type\Constant\ConstantArrayType; @@ -33,19 +34,19 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return strtolower($functionReflection->getName()) === 'preg_split'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { $flagsArg = $functionCall->getArgs()[3] ?? null; if ($flagsArg !== null && $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagsArg->value, $scope, 'PREG_SPLIT_OFFSET_CAPTURE')->yes()) { $type = new ArrayType( new IntegerType(), - new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new StringType(), IntegerRangeType::fromInterval(0, null)], [2]), + new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new StringType(), IntegerRangeType::fromInterval(0, null)], [2], [], TrinaryLogic::createYes()), ); - return TypeCombinator::union($type, new ConstantBooleanType(false)); + return TypeCombinator::union(AccessoryArrayListType::intersectWith($type), new ConstantBooleanType(false)); } - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } } diff --git a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php index dbee387fc5..8907e48a66 100644 --- a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php @@ -40,7 +40,7 @@ public function isFunctionSupported( ): bool { return $functionReflection->getName() === 'property_exists' - && $context->truthy() + && $context->true() && count($node->getArgs()) >= 2; } @@ -59,7 +59,7 @@ public function specifyTypes( $objectType = $scope->getType($node->getArgs()[0]->value); if ($objectType instanceof ConstantStringType) { return new SpecifiedTypes([], []); - } elseif ((new ObjectWithoutClassType())->isSuperTypeOf($objectType)->yes()) { + } elseif ($objectType->isObject()->yes()) { $propertyNode = new PropertyFetch( $node->getArgs()[0]->value, new Identifier($propertyNameType->getValue()), diff --git a/src/Type/Php/RandomIntFunctionReturnTypeExtension.php b/src/Type/Php/RandomIntFunctionReturnTypeExtension.php index 5b71dbf433..e7e17b90f6 100644 --- a/src/Type/Php/RandomIntFunctionReturnTypeExtension.php +++ b/src/Type/Php/RandomIntFunctionReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; @@ -23,17 +22,17 @@ class RandomIntFunctionReturnTypeExtension implements DynamicFunctionReturnTypeE public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return in_array($functionReflection->getName(), ['random_int', 'rand'], true); + return in_array($functionReflection->getName(), ['random_int', 'rand', 'mt_rand'], true); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - if ($functionReflection->getName() === 'rand' && count($functionCall->getArgs()) === 0) { + if (in_array($functionReflection->getName(), ['rand', 'mt_rand'], true) && count($functionCall->getArgs()) === 0) { return IntegerRangeType::fromInterval(0, null); } if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectFromArgs($scope, $functionCall->getArgs(), $functionReflection->getVariants())->getReturnType(); + return null; } $minType = $scope->getType($functionCall->getArgs()[0]->value)->toInteger(); diff --git a/src/Type/Php/RangeFunctionReturnTypeExtension.php b/src/Type/Php/RangeFunctionReturnTypeExtension.php index 028c6c9bda..9d48e74c01 100644 --- a/src/Type/Php/RangeFunctionReturnTypeExtension.php +++ b/src/Type/Php/RangeFunctionReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; @@ -18,11 +18,9 @@ use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; -use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use function count; use function range; @@ -37,10 +35,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'range'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $startType = $scope->getType($functionCall->getArgs()[0]->value); @@ -49,19 +47,19 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $constantReturnTypes = []; - $startConstants = TypeUtils::getConstantScalars($startType); + $startConstants = $startType->getConstantScalarTypes(); foreach ($startConstants as $startConstant) { if (!$startConstant instanceof ConstantIntegerType && !$startConstant instanceof ConstantFloatType && !$startConstant instanceof ConstantStringType) { continue; } - $endConstants = TypeUtils::getConstantScalars($endType); + $endConstants = $endType->getConstantScalarTypes(); foreach ($endConstants as $endConstant) { if (!$endConstant instanceof ConstantIntegerType && !$endConstant instanceof ConstantFloatType && !$endConstant instanceof ConstantStringType) { continue; } - $stepConstants = TypeUtils::getConstantScalars($stepType); + $stepConstants = $stepType->getConstantScalarTypes(); foreach ($stepConstants as $stepConstant) { if (!$stepConstant instanceof ConstantIntegerType && !$stepConstant instanceof ConstantFloatType) { continue; @@ -75,16 +73,16 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $startConstant = $endConstant; $endConstant = $tmp; } - return new IntersectionType([ + return AccessoryArrayListType::intersectWith(TypeCombinator::intersect( new ArrayType( new IntegerType(), IntegerRangeType::fromInterval($startConstant->getValue(), $endConstant->getValue()), ), new NonEmptyArrayType(), - ]); + )); } - return new IntersectionType([ + return AccessoryArrayListType::intersectWith(TypeCombinator::intersect( new ArrayType( new IntegerType(), TypeCombinator::union( @@ -93,7 +91,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ), ), new NonEmptyArrayType(), - ]); + )); } $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach ($rangeValues as $value) { @@ -110,31 +108,35 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $argType = TypeCombinator::union($startType, $endType); - $isInteger = (new IntegerType())->isSuperTypeOf($argType)->yes(); - $isStepInteger = (new IntegerType())->isSuperTypeOf($stepType)->yes(); + $isInteger = $argType->isInteger()->yes(); + $isStepInteger = $stepType->isInteger()->yes(); if ($isInteger && $isStepInteger) { - return new ArrayType(new IntegerType(), new IntegerType()); + if ($argType instanceof IntegerRangeType) { + return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $argType)); + } + return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new IntegerType())); } - $isFloat = (new FloatType())->isSuperTypeOf($argType)->yes(); - if ($isFloat) { - return new ArrayType(new IntegerType(), new FloatType()); + if ($argType->isFloat()->yes()) { + return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new FloatType())); } $numberType = new UnionType([new IntegerType(), new FloatType()]); $isNumber = $numberType->isSuperTypeOf($argType)->yes(); $isNumericString = $argType->isNumericString()->yes(); if ($isNumber || $isNumericString) { - return new ArrayType(new IntegerType(), $numberType); + return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $numberType)); } - $isString = $argType->isString()->yes(); - if ($isString) { - return new ArrayType(new IntegerType(), new StringType()); + if ($argType->isString()->yes()) { + return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new StringType())); } - return new ArrayType(new IntegerType(), new BenevolentUnionType([new IntegerType(), new FloatType(), new StringType()])); + return AccessoryArrayListType::intersectWith(new ArrayType( + new IntegerType(), + new BenevolentUnionType([new IntegerType(), new FloatType(), new StringType()]), + )); } } diff --git a/src/Type/Php/ReflectionClassConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionClassConstructorThrowTypeExtension.php index 3efa672a99..bbe3c9de62 100644 --- a/src/Type/Php/ReflectionClassConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionClassConstructorThrowTypeExtension.php @@ -5,24 +5,17 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\ClassStringType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; -use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; +use PHPStan\Type\UnionType; use ReflectionClass; use function count; class ReflectionClassConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { - public function __construct(private ReflectionProvider $reflectionProvider) - { - } - public function isStaticMethodSupported(MethodReflection $methodReflection): bool { return $methodReflection->getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === ReflectionClass::class; @@ -35,22 +28,15 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } $valueType = $scope->getType($methodCall->getArgs()[0]->value); - foreach (TypeUtils::flattenTypes($valueType) as $type) { - if ($type instanceof ClassStringType || $type instanceof ObjectWithoutClassType || $type instanceof ObjectType) { - continue; - } - - if ( - $type instanceof ConstantStringType - && $this->reflectionProvider->hasClass($type->getValue()) - ) { - continue; - } - - return $methodReflection->getThrowType(); + $classOrString = new UnionType([ + new ClassStringType(), + new ObjectWithoutClassType(), + ]); + if ($classOrString->isSuperTypeOf($valueType)->yes()) { + return null; } - return null; + return $methodReflection->getThrowType(); } } diff --git a/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php index 7dd3ba3e22..078301bb12 100644 --- a/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php @@ -11,7 +11,6 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use ReflectionFunction; use function count; @@ -34,7 +33,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } $valueType = $scope->getType($methodCall->getArgs()[0]->value); - foreach (TypeUtils::getConstantStrings($valueType) as $constantString) { + foreach ($valueType->getConstantStrings() as $constantString) { if (!$this->reflectionProvider->hasFunction(new Name($constantString->getValue()), $scope)) { return $methodReflection->getThrowType(); } diff --git a/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php b/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php index ac4b12a9aa..bb11536c1b 100644 --- a/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php +++ b/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php @@ -6,12 +6,9 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicMethodReturnTypeExtension; -use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\MixedType; -use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use ReflectionAttribute; use function count; @@ -43,14 +40,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return null; } $argType = $scope->getType($methodCall->getArgs()[0]->value); - - if ($argType instanceof ConstantStringType) { - $classType = new ObjectType($argType->getValue()); - } elseif ($argType instanceof GenericClassStringType) { - $classType = $argType->getGenericType(); - } else { - return null; - } + $classType = $argType->getClassStringObjectType(); return new ArrayType(new MixedType(), new GenericObjectType(ReflectionAttribute::class, [$classType])); } diff --git a/src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php index 18890c4236..e6aa5823f5 100644 --- a/src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php @@ -38,7 +38,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect $propertyType = $scope->getType($methodCall->getArgs()[1]->value); foreach (TypeUtils::flattenTypes($valueType) as $type) { if ($type instanceof GenericClassStringType) { - $classes = $type->getReferencedClasses(); + $classes = $type->getGenericType()->getObjectClassNames(); } elseif ( $type instanceof ConstantStringType && $this->reflectionProvider->hasClass($type->getValue()) @@ -50,7 +50,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect foreach ($classes as $class) { $classReflection = $this->reflectionProvider->getClass($class); - foreach (TypeUtils::getConstantStrings($propertyType) as $constantPropertyString) { + foreach ($propertyType->getConstantStrings() as $constantPropertyString) { if (!$classReflection->hasMethod($constantPropertyString->getValue())) { return $methodReflection->getThrowType(); } @@ -65,7 +65,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } // Look for non constantStrings value. - foreach (TypeUtils::getConstantStrings($propertyType) as $constantPropertyString) { + foreach ($propertyType->getConstantStrings() as $constantPropertyString) { $propertyType = TypeCombinator::remove($propertyType, $constantPropertyString); } diff --git a/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php index eed7c7d115..098b3f1567 100644 --- a/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php @@ -10,7 +10,6 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use ReflectionProperty; use function count; @@ -34,13 +33,13 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect $valueType = $scope->getType($methodCall->getArgs()[0]->value); $propertyType = $scope->getType($methodCall->getArgs()[1]->value); - foreach (TypeUtils::getConstantStrings($valueType) as $constantString) { + foreach ($valueType->getConstantStrings() as $constantString) { if (!$this->reflectionProvider->hasClass($constantString->getValue())) { return $methodReflection->getThrowType(); } $classReflection = $this->reflectionProvider->getClass($constantString->getValue()); - foreach (TypeUtils::getConstantStrings($propertyType) as $constantPropertyString) { + foreach ($propertyType->getConstantStrings() as $constantPropertyString) { if (!$classReflection->hasProperty($constantPropertyString->getValue())) { return $methodReflection->getThrowType(); } @@ -54,7 +53,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } // Look for non constantStrings value. - foreach (TypeUtils::getConstantStrings($propertyType) as $constantPropertyString) { + foreach ($propertyType->getConstantStrings() as $constantPropertyString) { $propertyType = TypeCombinator::remove($propertyType, $constantPropertyString); } diff --git a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php index 6451584ca6..1da17b48d8 100644 --- a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php @@ -8,7 +8,6 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; -use PHPStan\Type\ArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; @@ -22,27 +21,27 @@ class ReplaceFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - /** @var array */ - private array $functionsSubjectPosition = [ + private const FUNCTIONS_SUBJECT_POSITION = [ 'preg_replace' => 2, 'preg_replace_callback' => 2, 'preg_replace_callback_array' => 1, 'str_replace' => 2, 'str_ireplace' => 2, 'substr_replace' => 0, + 'strtr' => 0, ]; - /** @var array */ - private array $functionsReplacePosition = [ + private const FUNCTIONS_REPLACE_POSITION = [ 'preg_replace' => 1, 'str_replace' => 1, 'str_ireplace' => 1, 'substr_replace' => 1, + 'strtr' => 2, ]; public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return array_key_exists($functionReflection->getName(), $this->functionsSubjectPosition); + return array_key_exists($functionReflection->getName(), self::FUNCTIONS_SUBJECT_POSITION); } public function getTypeFromFunctionCall( @@ -53,7 +52,13 @@ public function getTypeFromFunctionCall( { $type = $this->getPreliminarilyResolvedTypeFromFunctionCall($functionReflection, $functionCall, $scope); - $possibleTypes = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $possibleTypes = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(); + // resolve conditional return types + $possibleTypes = TypeUtils::resolveLateResolvableTypes($possibleTypes); if (TypeCombinator::containsNull($possibleTypes)) { $type = TypeCombinator::addNull($type); @@ -68,19 +73,24 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( Scope $scope, ): Type { - $argumentPosition = $this->functionsSubjectPosition[$functionReflection->getName()]; + $argumentPosition = self::FUNCTIONS_SUBJECT_POSITION[$functionReflection->getName()]; + $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants(), + )->getReturnType(); + if (count($functionCall->getArgs()) <= $argumentPosition) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return $defaultReturnType; } $subjectArgumentType = $scope->getType($functionCall->getArgs()[$argumentPosition]->value); - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); if ($subjectArgumentType instanceof MixedType) { return TypeUtils::toBenevolentUnion($defaultReturnType); } - if ($subjectArgumentType->isNonEmptyString()->yes() && array_key_exists($functionReflection->getName(), $this->functionsReplacePosition)) { - $replaceArgumentPosition = $this->functionsReplacePosition[$functionReflection->getName()]; + if ($subjectArgumentType->isNonEmptyString()->yes() && array_key_exists($functionReflection->getName(), self::FUNCTIONS_REPLACE_POSITION)) { + $replaceArgumentPosition = self::FUNCTIONS_REPLACE_POSITION[$functionReflection->getName()]; if (count($functionCall->getArgs()) > $replaceArgumentPosition) { $replaceArgumentType = $scope->getType($functionCall->getArgs()[$replaceArgumentPosition]->value); @@ -100,8 +110,13 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( if ($compareSuperTypes === $isStringSuperType) { return new StringType(); } elseif ($compareSuperTypes === $isArraySuperType) { - if ($subjectArgumentType instanceof ArrayType) { - return $subjectArgumentType->generalizeValues(); + if (count($subjectArgumentType->getArrays()) > 0) { + $result = []; + foreach ($subjectArgumentType->getArrays() as $arrayType) { + $result[] = $arrayType->generalizeValues(); + } + + return TypeCombinator::union(...$result); } return $subjectArgumentType; } diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 839c5c1587..b68471d04c 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\FloatType; @@ -39,21 +38,18 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo ); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { + // PHP 7 can return either a float or false. + // PHP 8 can either return a float or fatal. + $defaultReturnType = null; + if ($this->phpVersion->hasStricterRoundFunctions()) { // PHP 8 fatals with a missing parameter. $noArgsReturnType = new NeverType(true); - // PHP 8 can either return a float or fatal. - $defaultReturnType = new FloatType(); } else { // PHP 7 returns null with a missing parameter. $noArgsReturnType = new NullType(); - // PHP 7 can return either a float or false. - $defaultReturnType = new BenevolentUnionType([ - new FloatType(), - new ConstantBooleanType(false), - ]); } if (count($functionCall->getArgs()) < 1) { diff --git a/src/Type/Php/SetTypeFunctionTypeSpecifyingExtension.php b/src/Type/Php/SetTypeFunctionTypeSpecifyingExtension.php new file mode 100644 index 0000000000..70526414bb --- /dev/null +++ b/src/Type/Php/SetTypeFunctionTypeSpecifyingExtension.php @@ -0,0 +1,91 @@ +getName()) === 'settype' + && count($node->getArgs()) > 1 + && $context->null(); + } + + public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + $value = $node->getArgs()[0]->value; + $valueType = $scope->getType($value); + $castType = $scope->getType($node->getArgs()[1]->value); + + $constantStrings = $castType->getConstantStrings(); + if (count($constantStrings) < 1) { + return new SpecifiedTypes(); + } + + $types = []; + + foreach ($constantStrings as $constantString) { + switch ($constantString->getValue()) { + case 'bool': + case 'boolean': + $types[] = $valueType->toBoolean(); + break; + case 'int': + case 'integer': + $types[] = $valueType->toInteger(); + break; + case 'float': + case 'double': + $types[] = $valueType->toFloat(); + break; + case 'string': + $types[] = $valueType->toString(); + break; + case 'array': + $types[] = $valueType->toArray(); + break; + case 'object': + $types[] = new ObjectType(stdClass::class); + break; + case 'null': + $types[] = new NullType(); + break; + default: + $types[] = new ErrorType(); + } + } + + return $this->typeSpecifier->create( + $value, + TypeCombinator::union(...$types), + TypeSpecifierContext::createTruthy(), + true, + $scope, + ); + } + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + +} diff --git a/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php b/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php index e7b8247870..ea5da83872 100644 --- a/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php +++ b/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php @@ -9,16 +9,19 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use SimpleXMLElement; use function count; +use function extension_loaded; +use function libxml_use_internal_errors; class SimpleXMLElementConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { public function isStaticMethodSupported(MethodReflection $methodReflection): bool { - return $methodReflection->getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === SimpleXMLElement::class; + return extension_loaded('simplexml') + && $methodReflection->getName() === '__construct' + && $methodReflection->getDeclaringClass()->getName() === SimpleXMLElement::class; } public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type @@ -28,16 +31,22 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } $valueType = $scope->getType($methodCall->getArgs()[0]->value); - $constantStrings = TypeUtils::getConstantStrings($valueType); + $constantStrings = $valueType->getConstantStrings(); - foreach ($constantStrings as $constantString) { - try { - new SimpleXMLElement($constantString->getValue()); - } catch (\Exception $e) { // phpcs:ignore - return $methodReflection->getThrowType(); - } + $internalErrorsOld = libxml_use_internal_errors(true); + + try { + foreach ($constantStrings as $constantString) { + try { + new SimpleXMLElement($constantString->getValue()); + } catch (\Exception $e) { // phpcs:ignore + return $methodReflection->getThrowType(); + } - $valueType = TypeCombinator::remove($valueType, $constantString); + $valueType = TypeCombinator::remove($valueType, $constantString); + } + } finally { + libxml_use_internal_errors($internalErrorsOld); } if (!$valueType instanceof NeverType) { diff --git a/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php b/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php index 175cd703ed..5ce6d57598 100644 --- a/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php +++ b/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php @@ -5,15 +5,14 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ArrayType; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use SimpleXMLElement; +use function extension_loaded; class SimpleXMLElementXpathMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -25,31 +24,31 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection): bool { - return $methodReflection->getName() === 'xpath'; + return extension_loaded('simplexml') && $methodReflection->getName() === 'xpath'; } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { if (!isset($methodCall->getArgs()[0])) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($methodCall->getArgs()[0]->value); $xmlElement = new SimpleXMLElement(''); - foreach (TypeUtils::getConstantStrings($argType) as $constantString) { + foreach ($argType->getConstantStrings() as $constantString) { $result = @$xmlElement->xpath($constantString->getValue()); if ($result === false) { // We can't be sure since it's maybe a namespaced xpath - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } $argType = TypeCombinator::remove($argType, $constantString); } if (!$argType instanceof NeverType) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + return null; } return new ArrayType(new MixedType(), $scope->getType($methodCall->var)); diff --git a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php index 16663c3073..ef70f072b9 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -4,17 +4,17 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Internal\CombinationsHelper; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; -use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\ConstantScalarType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use Throwable; use function array_key_exists; use function array_shift; @@ -41,18 +41,28 @@ public function getTypeFromFunctionCall( { $args = $functionCall->getArgs(); if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $formatType = $scope->getType($args[0]->value); - if ($formatType instanceof ConstantStringType) { - // The printf format is %[argnum$][flags][width][.precision] - if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*[bdeEfFgGhHouxX]$/', $formatType->getValue(), $matches) === 1) { - // invalid positional argument - if (array_key_exists(1, $matches) && $matches[1] === '0$') { - return null; + if (count($formatType->getConstantStrings()) > 0) { + $skip = false; + foreach ($formatType->getConstantStrings() as $constantString) { + // The printf format is %[argnum$][flags][width][.precision] + if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*[bdeEfFgGhHouxX]$/', $constantString->getValue(), $matches) === 1) { + // invalid positional argument + if (array_key_exists(1, $matches) && $matches[1] === '0$') { + return null; + } + + continue; } + $skip = true; + break; + } + + if (!$skip) { return new IntersectionType([ new StringType(), new AccessoryNumericStringType(), @@ -75,31 +85,46 @@ public function getTypeFromFunctionCall( } $values = []; + $combinationsCount = 1; foreach ($args as $arg) { $argType = $scope->getType($arg->value); - if (!$argType instanceof ConstantScalarType) { + if (count($argType->getConstantScalarValues()) === 0) { return $returnType; } - $values[] = $argType->getValue(); + $constantScalarValues = $argType->getConstantScalarValues(); + $values[] = $constantScalarValues; + $combinationsCount *= count($constantScalarValues); } - $format = array_shift($values); - if (!is_string($format)) { + if ($combinationsCount > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { return $returnType; } - try { - if ($functionReflection->getName() === 'sprintf') { - $value = @sprintf($format, ...$values); - } else { - $value = @vsprintf($format, $values); + $combinations = CombinationsHelper::combinations($values); + $returnTypes = []; + foreach ($combinations as $combination) { + $format = array_shift($combination); + if (!is_string($format)) { + return $returnType; + } + + try { + if ($functionReflection->getName() === 'sprintf') { + $returnTypes[] = $scope->getTypeFromValue(@sprintf($format, ...$combination)); + } else { + $returnTypes[] = $scope->getTypeFromValue(@vsprintf($format, $combination)); + } + } catch (Throwable) { + return $returnType; } - } catch (Throwable) { + } + + if (count($returnTypes) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { return $returnType; } - return $scope->getTypeFromValue($value); + return TypeCombinator::union(...$returnTypes); } } diff --git a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php index 970985d8e6..df1dfc88e9 100644 --- a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php +++ b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php @@ -5,8 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; @@ -17,53 +15,87 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function array_map; use function count; use function in_array; use function is_callable; +use function mb_check_encoding; class StrCaseFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + /** + * [function name => minimum arity] + */ + private const FUNCTIONS = [ + 'strtoupper' => 1, + 'strtolower' => 1, + 'mb_strtoupper' => 1, + 'mb_strtolower' => 1, + 'lcfirst' => 1, + 'ucfirst' => 1, + 'ucwords' => 1, + 'mb_convert_case' => 2, + 'mb_convert_kana' => 1, + ]; + public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return in_array($functionReflection->getName(), [ - 'strtoupper', - 'strtolower', - 'mb_strtoupper', - 'mb_strtolower', - 'lcfirst', - 'ucfirst', - 'ucwords', - ], true); + return isset(self::FUNCTIONS[$functionReflection->getName()]); } public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { + $fnName = $functionReflection->getName(); $args = $functionCall->getArgs(); - if (count($args) < 1) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + + if (count($args) < self::FUNCTIONS[$fnName]) { + return null; } $argType = $scope->getType($args[0]->value); - $fnName = $functionReflection->getName(); if (!is_callable($fnName)) { - throw new ShouldNotHappenException(); + return null; + } + + $modes = []; + if ($fnName === 'mb_convert_case') { + $modeType = $scope->getType($args[1]->value); + $modes = array_map(static fn ($mode) => $mode->getValue(), TypeUtils::getConstantIntegers($modeType)); + } elseif (in_array($fnName, ['ucwords', 'mb_convert_kana'], true)) { + if (count($args) >= 2) { + $modeType = $scope->getType($args[1]->value); + $modes = array_map(static fn ($mode) => $mode->getValue(), $modeType->getConstantStrings()); + } else { + $modes = $fnName === 'mb_convert_kana' ? ['KV'] : [" \t\r\n\f\v"]; + } } - if (count($args) === 1) { - $constantStrings = TypeUtils::getConstantStrings($argType); - if (count($constantStrings) > 0) { - $strings = []; + $constantStrings = array_map(static fn ($type) => $type->getValue(), $argType->getConstantStrings()); + if (count($constantStrings) > 0 && mb_check_encoding($constantStrings, 'UTF-8')) { + $strings = []; - foreach ($constantStrings as $constantString) { - $strings[] = new ConstantStringType($fnName($constantString->getValue())); + $parameters = []; + if (in_array($fnName, ['ucwords', 'mb_convert_case', 'mb_convert_kana'], true)) { + foreach ($modes as $mode) { + foreach ($constantStrings as $constantString) { + $parameters[] = [$constantString, $mode]; + } } + } else { + $parameters = array_map(static fn ($s) => [$s], $constantStrings); + } + + foreach ($parameters as $parameter) { + $strings[] = $fnName(...$parameter); + } - return TypeCombinator::union(...$strings); + if (count($strings) !== 0 && mb_check_encoding($strings, 'UTF-8')) { + return TypeCombinator::union(...array_map(static fn ($s) => new ConstantStringType($s), $strings)); } } diff --git a/src/Type/Php/StrContainingTypeSpecifyingExtension.php b/src/Type/Php/StrContainingTypeSpecifyingExtension.php index 596a1b3dc2..f7745f2128 100644 --- a/src/Type/Php/StrContainingTypeSpecifyingExtension.php +++ b/src/Type/Php/StrContainingTypeSpecifyingExtension.php @@ -28,8 +28,7 @@ final class StrContainingTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - /** @var array */ - private array $strContainingFunctions = [ + private const STR_CONTAINING_FUNCTIONS = [ 'fnmatch' => [1, 0], 'str_contains' => [0, 1], 'str_starts_with' => [0, 1], @@ -50,8 +49,8 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { - return array_key_exists(strtolower($functionReflection->getName()), $this->strContainingFunctions) - && $context->truthy(); + return array_key_exists(strtolower($functionReflection->getName()), self::STR_CONTAINING_FUNCTIONS) + && $context->true(); } public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes @@ -59,7 +58,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $args = $node->getArgs(); if (count($args) >= 2) { - [$hackstackArg, $needleArg] = $this->strContainingFunctions[strtolower($functionReflection->getName())]; + [$hackstackArg, $needleArg] = self::STR_CONTAINING_FUNCTIONS[strtolower($functionReflection->getName())]; $haystackType = $scope->getType($args[$hackstackArg]->value); $needleType = $scope->getType($args[$needleArg]->value); diff --git a/src/Type/Php/StrIncrementDecrementFunctionReturnTypeExtension.php b/src/Type/Php/StrIncrementDecrementFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..b739263e64 --- /dev/null +++ b/src/Type/Php/StrIncrementDecrementFunctionReturnTypeExtension.php @@ -0,0 +1,143 @@ +getName(), ['str_increment', 'str_decrement'], true); + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $fnName = $functionReflection->getName(); + $args = $functionCall->getArgs(); + + if (count($args) !== 1) { + return null; + } + + $argType = $scope->getType($args[0]->value); + if (count($argType->getConstantScalarValues()) === 0) { + return null; + } + + $types = []; + foreach ($argType->getConstantScalarValues() as $value) { + if (!(is_string($value) || is_int($value) || is_float($value))) { + continue; + } + $string = (string) $value; + + if (preg_match('/\A(?:0|[1-9A-Za-z][0-9A-Za-z]*)+\z/', $string) < 1) { + continue; + } + + $result = null; + if ($fnName === 'str_increment') { + $result = $this->increment($string); + } elseif ($fnName === 'str_decrement') { + $result = $this->decrement($string); + } + + if ($result === null) { + continue; + } + + $types[] = new ConstantStringType($result); + } + + return count($types) === 0 + ? new ErrorType() + : TypeCombinator::union(...$types); + } + + private function increment(string $s): string + { + if (is_numeric($s)) { + $offset = stripos($s, 'e'); + if ($offset !== false) { + // Using increment operator would cast the string to float + // Therefore we manually increment it to convert it to an "f"/"F" that doesn't get affected + $c = $s[$offset]; + $c++; + $s[$offset] = $c; + $s++; + $s[$offset] = [ + 'f' => 'e', + 'F' => 'E', + 'g' => 'f', + 'G' => 'F', + ][$s[$offset]]; + + return $s; + } + } + + return (string) ++$s; + } + + private function decrement(string $s): ?string + { + if (in_array($s, ['a', 'A', '0'], true)) { + return null; + } + + $decremented = str_split($s, 1); + $position = count($decremented) - 1; + $carry = false; + $map = [ + '0' => '9', + 'A' => 'Z', + 'a' => 'z', + ]; + do { + $c = $decremented[$position]; + if (!in_array($c, ['a', 'A', '0'], true)) { + $carry = false; + $decremented[$position] = chr(ord($c) - 1); + } else { + $carry = true; + $decremented[$position] = $map[$c]; + } + } while ($carry && $position-- > 0); + + if ($carry || count($decremented) > 1 && $decremented[0] === '0') { + if (count($decremented) === 1) { + return null; + } + + unset($decremented[0]); + } + + return implode($decremented); + } + +} diff --git a/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php b/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php index 36af3ec9c0..6bf696ac11 100644 --- a/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php @@ -50,10 +50,11 @@ public function getTypeFromFunctionCall( return new NeverType(); } - if ($inputType instanceof ConstantStringType && - $multiplierType instanceof ConstantIntegerType && + if ( + $inputType instanceof ConstantStringType + && $multiplierType instanceof ConstantIntegerType // don't generate type too big to avoid hitting memory limit - strlen($inputType->getValue()) * $multiplierType->getValue() < 100 + && strlen($inputType->getValue()) * $multiplierType->getValue() < 100 ) { return new ConstantStringType(str_repeat($inputType->getValue(), $multiplierType->getValue())); } diff --git a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php index a61c897864..34d58152dc 100644 --- a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php @@ -6,8 +6,9 @@ use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; @@ -16,13 +17,10 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerType; -use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeTraverser; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\UnionType; +use function array_is_list; use function array_map; use function array_unique; use function count; @@ -45,12 +43,10 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return in_array($functionReflection->getName(), ['str_split', 'mb_str_split'], true); } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (count($functionCall->getArgs()) < 1) { - return $defaultReturnType; + return null; } if (count($functionCall->getArgs()) >= 2) { @@ -68,11 +64,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $encoding = null; if ($functionReflection->getName() === 'mb_str_split') { if (count($functionCall->getArgs()) >= 3) { - $strings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[2]->value)); + $strings = $scope->getType($functionCall->getArgs()[2]->value)->getConstantStrings(); $values = array_unique(array_map(static fn (ConstantStringType $encoding): string => $encoding->getValue(), $strings)); if (count($values) !== 1) { - return $defaultReturnType; + return null; } $encoding = $values[0]; @@ -85,35 +81,33 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if (!isset($splitLength)) { - return $defaultReturnType; + return null; } $stringType = $scope->getType($functionCall->getArgs()[0]->value); - return TypeTraverser::map($stringType, function (Type $type, callable $traverse) use ($encoding, $splitLength, $scope): Type { - if ($type instanceof UnionType || $type instanceof IntersectionType) { - return $traverse($type); - } - - if (!$type instanceof ConstantStringType) { - $returnType = new ArrayType(new IntegerType(), new StringType()); + $constantStrings = $stringType->getConstantStrings(); + if (count($constantStrings) > 0) { + $results = []; + foreach ($constantStrings as $constantString) { + $items = $encoding === null + ? str_split($constantString->getValue(), $splitLength) + : @mb_str_split($constantString->getValue(), $splitLength, $encoding); + if ($items === false) { + throw new ShouldNotHappenException(); + } - return $encoding === null && !$this->phpVersion->strSplitReturnsEmptyArray() - ? TypeCombinator::intersect($returnType, new NonEmptyArrayType()) - : $returnType; + $results[] = self::createConstantArrayFrom($items, $scope); } - $stringValue = $type->getValue(); + return TypeCombinator::union(...$results); + } - $items = $encoding === null - ? str_split($stringValue, $splitLength) - : @mb_str_split($stringValue, $splitLength, $encoding); - if ($items === false) { - throw new ShouldNotHappenException(); - } + $returnType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new StringType())); - return self::createConstantArrayFrom($items, $scope); - }); + return $encoding === null && !$this->phpVersion->strSplitReturnsEmptyArray() + ? TypeCombinator::intersect($returnType, new NonEmptyArrayType()) + : $returnType; } /** @@ -139,7 +133,7 @@ private static function createConstantArrayFrom(array $constantArray, Scope $sco $i++; } - return new ConstantArrayType($keyTypes, $valueTypes, $isList ? [$i] : [0]); + return new ConstantArrayType($keyTypes, $valueTypes, $isList ? [$i] : [0], [], TrinaryLogic::createFromBoolean(array_is_list($constantArray))); } } diff --git a/src/Type/Php/StrTokFunctionReturnTypeExtension.php b/src/Type/Php/StrTokFunctionReturnTypeExtension.php index 0d82a17a37..1af80eafe6 100644 --- a/src/Type/Php/StrTokFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrTokFunctionReturnTypeExtension.php @@ -5,10 +5,11 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use function count; @@ -21,11 +22,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo return $functionReflection->getName() === 'strtok'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { $args = $functionCall->getArgs(); if (count($args) !== 2) { - return ParametersAcceptorSelector::selectFromArgs($scope, $args, $functionReflection->getVariants())->getReturnType(); + return null; } $delimiterType = $scope->getType($functionCall->getArgs()[0]->value); @@ -35,10 +36,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($isEmptyString->no()) { - return new StringType(); + return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); } - return ParametersAcceptorSelector::selectFromArgs($scope, $args, $functionReflection->getVariants())->getReturnType(); + return null; } } diff --git a/src/Type/Php/StrlenFunctionReturnTypeExtension.php b/src/Type/Php/StrlenFunctionReturnTypeExtension.php index b636739db7..e4a51cb833 100644 --- a/src/Type/Php/StrlenFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrlenFunctionReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -16,8 +15,13 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; +use function array_map; +use function array_unique; use function count; +use function max; +use function min; +use function range; +use function sort; use function strlen; class StrlenFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -32,75 +36,60 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { $args = $functionCall->getArgs(); if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($args[0]->value); if ($argType->isSuperTypeOf(new BooleanType())->yes()) { - $constantScalars = TypeUtils::getConstantScalars(TypeCombinator::remove($argType, new BooleanType())); + $constantScalars = TypeCombinator::remove($argType, new BooleanType())->getConstantScalarTypes(); if (count($constantScalars) > 0) { $constantScalars[] = new ConstantBooleanType(true); $constantScalars[] = new ConstantBooleanType(false); } } else { - $constantScalars = TypeUtils::getConstantScalars($argType); + $constantScalars = $argType->getConstantScalarTypes(); } - $min = null; - $max = null; + $lengths = []; foreach ($constantScalars as $constantScalar) { $stringScalar = $constantScalar->toString(); if (!($stringScalar instanceof ConstantStringType)) { - $min = $max = null; + $lengths = []; break; } - $len = strlen($stringScalar->getValue()); - - if ($min === null) { - $min = $len; - $max = $len; - } - - if ($len < $min) { - $min = $len; - } - if ($len <= $max) { - continue; - } - - $max = $len; - } - - // $max is always != null, when $min is != null - if ($min !== null) { - return IntegerRangeType::fromInterval($min, $max); - } - - $bool = new BooleanType(); - if ($bool->isSuperTypeOf($argType)->yes()) { - return IntegerRangeType::fromInterval(0, 1); + $length = strlen($stringScalar->getValue()); + $lengths[] = $length; } $isNonEmpty = $argType->isNonEmptyString(); $numeric = TypeCombinator::union(new IntegerType(), new FloatType()); - if ( + $range = null; + if (count($lengths) > 0) { + $lengths = array_unique($lengths); + sort($lengths); + if ($lengths === range(min($lengths), max($lengths))) { + $range = IntegerRangeType::fromInterval(min($lengths), max($lengths)); + } else { + $range = TypeCombinator::union(...array_map(static fn ($l) => new ConstantIntegerType($l), $lengths)); + } + } elseif ($argType->isBoolean()->yes()) { + $range = IntegerRangeType::fromInterval(0, 1); + } elseif ( $isNonEmpty->yes() || $numeric->isSuperTypeOf($argType)->yes() || TypeCombinator::remove($argType, $numeric)->isNonEmptyString()->yes() ) { - return IntegerRangeType::fromInterval(1, null); - } - - if ($argType->isString()->yes() && $isNonEmpty->no()) { - return new ConstantIntegerType(0); + $range = IntegerRangeType::fromInterval(1, null); + } elseif ($argType->isString()->yes() && $isNonEmpty->no()) { + $range = new ConstantIntegerType(0); } - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return $range; } } diff --git a/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php b/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php index 304b5401d3..ef5e1af787 100644 --- a/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php @@ -39,7 +39,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($argType instanceof MixedType) { return TypeUtils::toBenevolentUnion($defaultReturnType); } - $results = array_unique(array_map(static fn (ConstantStringType $string): int|bool => strtotime($string->getValue()), TypeUtils::getConstantStrings($argType))); + $results = array_unique(array_map(static fn (ConstantStringType $string): int|bool => strtotime($string->getValue()), $argType->getConstantStrings())); $resultTypes = array_unique(array_map(static fn (int|bool $value): string => gettype($value), $results)); if (count($resultTypes) !== 1 || count($results) === 0) { diff --git a/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php b/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php index cef472cf06..3328313d9a 100644 --- a/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php @@ -7,6 +7,9 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\ShouldNotHappenException; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\ErrorType; +use PHPStan\Type\FloatType; +use PHPStan\Type\IntegerType; use PHPStan\Type\NullType; use PHPStan\Type\Type; use function count; @@ -44,12 +47,14 @@ public function getTypeFromFunctionCall( case 'strval': return $argType->toString(); case 'intval': - return $argType->toInteger(); + $type = $argType->toInteger(); + return $type instanceof ErrorType ? new IntegerType() : $type; case 'boolval': return $argType->toBoolean(); case 'floatval': case 'doubleval': - return $argType->toFloat(); + $type = $argType->toFloat(); + return $type instanceof ErrorType ? new FloatType() : $type; default: throw new ShouldNotHappenException(); } diff --git a/src/Type/Php/SubstrDynamicReturnTypeExtension.php b/src/Type/Php/SubstrDynamicReturnTypeExtension.php index 1927ea8b63..de54d526fb 100644 --- a/src/Type/Php/SubstrDynamicReturnTypeExtension.php +++ b/src/Type/Php/SubstrDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -17,7 +16,6 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function count; use function is_bool; use function substr; @@ -38,7 +36,7 @@ public function getTypeFromFunctionCall( { $args = $functionCall->getArgs(); if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } if (count($args) >= 2) { @@ -55,7 +53,7 @@ public function getTypeFromFunctionCall( $positiveLength = IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($length)->yes(); } - $constantStrings = TypeUtils::getConstantStrings($string); + $constantStrings = $string->getConstantStrings(); if ( count($constantStrings) > 0 && $offset instanceof ConstantIntegerType diff --git a/src/Type/Php/ThrowableReturnTypeExtension.php b/src/Type/Php/ThrowableReturnTypeExtension.php index fa3a1a8d3b..9437b82485 100644 --- a/src/Type/Php/ThrowableReturnTypeExtension.php +++ b/src/Type/Php/ThrowableReturnTypeExtension.php @@ -13,7 +13,6 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use Throwable; use function count; use function in_array; @@ -37,7 +36,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method $type = $scope->getType($methodCall->var); $types = []; $pdoException = new ObjectType('PDOException'); - foreach (TypeUtils::getDirectClassNames($type) as $class) { + foreach ($type->getObjectClassNames() as $class) { $classType = new ObjectType($class); if ($classType->getClassReflection() !== null) { $classReflection = $classType->getClassReflection(); diff --git a/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php b/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php index 1f7566f28e..1df3f180e5 100644 --- a/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php @@ -4,37 +4,65 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use function count; +use function in_array; +use const E_USER_DEPRECATED; use const E_USER_ERROR; +use const E_USER_NOTICE; +use const E_USER_WARNING; class TriggerErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'trigger_error'; } - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $args = $functionCall->getArgs(); + + if (count($args) === 0) { + return null; + } + + if (count($args) === 1) { + return new ConstantBooleanType(true); } - $errorType = $scope->getType($functionCall->getArgs()[1]->value); - if ($errorType instanceof ConstantScalarType) { - if ($errorType->getValue() === E_USER_ERROR) { + $errorType = $scope->getType($args[1]->value); + + if ($errorType instanceof ConstantIntegerType) { + $errorLevel = $errorType->getValue(); + + if ($errorLevel === E_USER_ERROR) { return new NeverType(true); } + + if (!in_array($errorLevel, [E_USER_WARNING, E_USER_NOTICE, E_USER_DEPRECATED], true)) { + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + return new NeverType(true); + } + + return new ConstantBooleanType(false); + } + + return new ConstantBooleanType(true); } - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } } diff --git a/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php index d28f3c5075..380cfb5b59 100644 --- a/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php @@ -7,7 +7,6 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper; use PHPStan\Type\Constant\ConstantBooleanType; @@ -26,7 +25,7 @@ class TypeSpecifyingFunctionsDynamicReturnTypeExtension implements DynamicFuncti /** * @param string[] $universalObjectCratesClasses */ - public function __construct(private ReflectionProvider $reflectionProvider, private bool $treatPhpDocTypesAsCertain, private array $universalObjectCratesClasses) + public function __construct(private ReflectionProvider $reflectionProvider, private bool $treatPhpDocTypesAsCertain, private array $universalObjectCratesClasses, private bool $nullContextForVoidReturningFunctions) { } @@ -41,21 +40,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo 'array_key_exists', 'key_exists', 'in_array', - 'is_numeric', - 'is_int', - 'is_array', - 'is_bool', - 'is_callable', - 'is_float', - 'is_double', - 'is_real', - 'is_iterable', - 'is_null', - 'is_object', - 'is_scalar', - 'is_string', 'is_subclass_of', - 'is_countable', ], true); } @@ -63,10 +48,10 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { if (count($functionCall->getArgs()) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $isAlways = $this->getHelper()->findSpecifiedType( @@ -74,7 +59,7 @@ public function getTypeFromFunctionCall( $functionCall, ); if ($isAlways === null) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } return new ConstantBooleanType($isAlways); @@ -83,7 +68,7 @@ public function getTypeFromFunctionCall( private function getHelper(): ImpossibleCheckTypeHelper { if ($this->helper === null) { - $this->helper = new ImpossibleCheckTypeHelper($this->reflectionProvider, $this->typeSpecifier, $this->universalObjectCratesClasses, $this->treatPhpDocTypesAsCertain); + $this->helper = new ImpossibleCheckTypeHelper($this->reflectionProvider, $this->typeSpecifier, $this->universalObjectCratesClasses, $this->treatPhpDocTypesAsCertain, $this->nullContextForVoidReturningFunctions); } return $this->helper; diff --git a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php index f695b20899..b8a77540cb 100644 --- a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php @@ -5,14 +5,12 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use function array_filter; use function count; use function version_compare; @@ -29,21 +27,21 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { if (count($functionCall->getArgs()) < 2) { - return ParametersAcceptorSelector::selectFromArgs($scope, $functionCall->getArgs(), $functionReflection->getVariants())->getReturnType(); + return null; } - $version1Strings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[0]->value)); - $version2Strings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[1]->value)); + $version1Strings = $scope->getType($functionCall->getArgs()[0]->value)->getConstantStrings(); + $version2Strings = $scope->getType($functionCall->getArgs()[1]->value)->getConstantStrings(); $counts = [ count($version1Strings), count($version2Strings), ]; if (isset($functionCall->getArgs()[2])) { - $operatorStrings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[2]->value)); + $operatorStrings = $scope->getType($functionCall->getArgs()[2]->value)->getConstantStrings(); $counts[] = count($operatorStrings); $returnType = new BooleanType(); } else { diff --git a/src/Type/ResourceType.php b/src/Type/ResourceType.php index c676787e24..0eee8cea5f 100644 --- a/src/Type/ResourceType.php +++ b/src/Type/ResourceType.php @@ -2,8 +2,13 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonCallableTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; @@ -19,6 +24,7 @@ class ResourceType implements Type { use JustNullableTypeTrait; + use NonArrayTypeTrait; use NonCallableTypeTrait; use NonIterableTypeTrait; use NonObjectTypeTrait; @@ -39,6 +45,11 @@ public function describe(VerbosityLevel $level): string return 'resource'; } + public function getConstantStrings(): array + { + return []; + } + public function toNumber(): Type { return new ErrorType(); @@ -65,9 +76,41 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], + [], + TrinaryLogic::createYes(), ); } + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('resource'); + } + /** * @param mixed[] $properties */ diff --git a/src/Type/SimultaneousTypeTraverser.php b/src/Type/SimultaneousTypeTraverser.php new file mode 100644 index 0000000000..f0e0e3dfe7 --- /dev/null +++ b/src/Type/SimultaneousTypeTraverser.php @@ -0,0 +1,39 @@ +mapInternal($left, $right); + } + + /** @param callable(Type $left, Type $right, callable(Type, Type): Type $traverse): Type $cb */ + private function __construct(callable $cb) + { + $this->cb = $cb; + } + + /** @internal */ + public function mapInternal(Type $left, Type $right): Type + { + return ($this->cb)($left, $right, [$this, 'traverseInternal']); + } + + /** @internal */ + public function traverseInternal(Type $left, Type $right): Type + { + return $left->traverseSimultaneously($right, [$this, 'mapInternal']); + } + +} diff --git a/src/Type/StaticMethodTypeSpecifyingExtension.php b/src/Type/StaticMethodTypeSpecifyingExtension.php index 6f47e14e8f..dbb6a49ffa 100644 --- a/src/Type/StaticMethodTypeSpecifyingExtension.php +++ b/src/Type/StaticMethodTypeSpecifyingExtension.php @@ -8,7 +8,23 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\MethodReflection; -/** @api */ +/** + * This is the interface type-specifying extensions implement for static methods. + * + * To register it in the configuration file use the `phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension` service tag: + * + * ``` + * services: + * - + * class: App\PHPStan\MyExtension + * tags: + * - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension + * ``` + * + * Learn more: https://phpstan.org/developing-extensions/type-specifying-extensions + * + * @api + */ interface StaticMethodTypeSpecifyingExtension { diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index 7cd9706e68..a9809f0362 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -2,11 +2,13 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection; @@ -14,7 +16,6 @@ use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; -use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; @@ -83,10 +84,13 @@ public function getStaticObjectType(): ObjectType if ($this->staticObjectType === null) { if ($this->classReflection->isGeneric()) { $typeMap = $this->classReflection->getActiveTemplateTypeMap()->map(static fn (string $name, Type $type): Type => TemplateTypeHelper::toArgument($type)); + $varianceMap = $this->classReflection->getCallSiteVarianceMap(); return $this->staticObjectType = new GenericObjectType( $this->classReflection->getName(), $this->classReflection->typeMapToList($typeMap), $this->subtractedType, + null, + $this->classReflection->varianceMapToList($varianceMap), ); } @@ -104,28 +108,51 @@ public function getReferencedClasses(): array return $this->getStaticObjectType()->getReferencedClasses(); } + public function getObjectClassNames(): array + { + return $this->getStaticObjectType()->getObjectClassNames(); + } + + public function getObjectClassReflections(): array + { + return $this->getStaticObjectType()->getObjectClassReflections(); + } + + public function getArrays(): array + { + return $this->getStaticObjectType()->getArrays(); + } + + public function getConstantArrays(): array + { + return $this->getStaticObjectType()->getConstantArrays(); + } + + public function getConstantStrings(): array + { + return $this->getStaticObjectType()->getConstantStrings(); + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } if (!$type instanceof static) { - return TrinaryLogic::createNo(); + return AcceptsResult::createNo(); } - return $this->getStaticObjectType()->accepts($type->getStaticObjectType(), $strictTypes); + return $this->getStaticObjectType()->acceptsWithReason($type->getStaticObjectType(), $strictTypes); } public function isSuperTypeOf(Type $type): TrinaryLogic { - if ($type instanceof ObjectType) { - $classReflection = $type->getClassReflection(); - if ($classReflection !== null && $classReflection->isFinal()) { - $type = new StaticType($classReflection, $type->getSubtractedType()); - } - } - if ($type instanceof self) { return $this->getStaticObjectType()->isSuperTypeOf($type); } @@ -135,7 +162,15 @@ public function isSuperTypeOf(Type $type): TrinaryLogic } if ($type instanceof ObjectType) { - return $this->getStaticObjectType()->isSuperTypeOf($type)->and(TrinaryLogic::createMaybe()); + $result = $this->getStaticObjectType()->isSuperTypeOf($type); + if ($result->yes()) { + $classReflection = $type->getClassReflection(); + if ($classReflection !== null && $classReflection->isFinal()) { + return $result; + } + } + + return $result->and(TrinaryLogic::createMaybe()); } if ($type instanceof CompoundType) { @@ -151,8 +186,6 @@ public function equals(Type $type): bool return false; } - /** @var StaticType $type */ - $type = $type; return $this->getStaticObjectType()->equals($type->getStaticObjectType()); } @@ -161,6 +194,21 @@ public function describe(VerbosityLevel $level): string return sprintf('static(%s)', $this->getStaticObjectType()->describe($level)); } + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return $this->getStaticObjectType()->getTemplateType($ancestorClassName, $templateTypeName); + } + + public function isObject(): TrinaryLogic + { + return $this->getStaticObjectType()->isObject(); + } + + public function isEnum(): TrinaryLogic + { + return $this->getStaticObjectType()->isEnum(); + } + public function canAccessProperties(): TrinaryLogic { return $this->getStaticObjectType()->canAccessProperties(); @@ -208,7 +256,7 @@ public function hasMethod(string $methodName): TrinaryLogic return $this->getStaticObjectType()->hasMethod($methodName); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -287,16 +335,41 @@ public function isIterableAtLeastOnce(): TrinaryLogic return $this->getStaticObjectType()->isIterableAtLeastOnce(); } + public function getArraySize(): Type + { + return $this->getStaticObjectType()->getArraySize(); + } + public function getIterableKeyType(): Type { return $this->getStaticObjectType()->getIterableKeyType(); } + public function getFirstIterableKeyType(): Type + { + return $this->getStaticObjectType()->getFirstIterableKeyType(); + } + + public function getLastIterableKeyType(): Type + { + return $this->getStaticObjectType()->getLastIterableKeyType(); + } + public function getIterableValueType(): Type { return $this->getStaticObjectType()->getIterableValueType(); } + public function getFirstIterableValueType(): Type + { + return $this->getStaticObjectType()->getFirstIterableValueType(); + } + + public function getLastIterableValueType(): Type + { + return $this->getStaticObjectType()->getLastIterableValueType(); + } + public function isOffsetAccessible(): TrinaryLogic { return $this->getStaticObjectType()->isOffsetAccessible(); @@ -317,26 +390,141 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this->getStaticObjectType()->setOffsetValueType($offsetType, $valueType, $unionValues); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this->getStaticObjectType()->setExistingOffsetValueType($offsetType, $valueType); + } + public function unsetOffset(Type $offsetType): Type { return $this->getStaticObjectType()->unsetOffset($offsetType); } + public function getKeysArray(): Type + { + return $this->getStaticObjectType()->getKeysArray(); + } + + public function getValuesArray(): Type + { + return $this->getStaticObjectType()->getValuesArray(); + } + + public function fillKeysArray(Type $valueType): Type + { + return $this->getStaticObjectType()->fillKeysArray($valueType); + } + + public function flipArray(): Type + { + return $this->getStaticObjectType()->flipArray(); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + return $this->getStaticObjectType()->intersectKeyArray($otherArraysType); + } + + public function popArray(): Type + { + return $this->getStaticObjectType()->popArray(); + } + + public function searchArray(Type $needleType): Type + { + return $this->getStaticObjectType()->searchArray($needleType); + } + + public function shiftArray(): Type + { + return $this->getStaticObjectType()->shiftArray(); + } + + public function shuffleArray(): Type + { + return $this->getStaticObjectType()->shuffleArray(); + } + public function isCallable(): TrinaryLogic { return $this->getStaticObjectType()->isCallable(); } + public function getEnumCases(): array + { + return $this->getStaticObjectType()->getEnumCases(); + } + public function isArray(): TrinaryLogic { return $this->getStaticObjectType()->isArray(); } + public function isConstantArray(): TrinaryLogic + { + return $this->getStaticObjectType()->isConstantArray(); + } + public function isOversizedArray(): TrinaryLogic { return $this->getStaticObjectType()->isOversizedArray(); } + public function isList(): TrinaryLogic + { + return $this->getStaticObjectType()->isList(); + } + + public function isNull(): TrinaryLogic + { + return $this->getStaticObjectType()->isNull(); + } + + public function isConstantValue(): TrinaryLogic + { + return $this->getStaticObjectType()->isConstantValue(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return $this->getStaticObjectType()->isConstantScalarValue(); + } + + public function getConstantScalarTypes(): array + { + return $this->getStaticObjectType()->getConstantScalarTypes(); + } + + public function getConstantScalarValues(): array + { + return $this->getStaticObjectType()->getConstantScalarValues(); + } + + public function isTrue(): TrinaryLogic + { + return $this->getStaticObjectType()->isTrue(); + } + + public function isFalse(): TrinaryLogic + { + return $this->getStaticObjectType()->isFalse(); + } + + public function isBoolean(): TrinaryLogic + { + return $this->getStaticObjectType()->isBoolean(); + } + + public function isFloat(): TrinaryLogic + { + return $this->getStaticObjectType()->isFloat(); + } + + public function isInteger(): TrinaryLogic + { + return $this->getStaticObjectType()->isInteger(); + } + public function isString(): TrinaryLogic { return $this->getStaticObjectType()->isString(); @@ -362,9 +550,36 @@ public function isLiteralString(): TrinaryLogic return $this->getStaticObjectType()->isLiteralString(); } - /** - * @return ParametersAcceptor[] - */ + public function isClassStringType(): TrinaryLogic + { + return $this->getStaticObjectType()->isClassStringType(); + } + + public function getClassStringObjectType(): Type + { + return $this->getStaticObjectType()->getClassStringObjectType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this; + } + + public function isVoid(): TrinaryLogic + { + return $this->getStaticObjectType()->isVoid(); + } + + public function isScalar(): TrinaryLogic + { + return $this->getStaticObjectType()->isScalar(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return $this->getStaticObjectType()->getCallableParametersAcceptors($scope); @@ -400,6 +615,11 @@ public function toArray(): Type return $this->getStaticObjectType()->toArray(); } + public function toArrayKey(): Type + { + return $this->getStaticObjectType()->toArrayKey(); + } + public function toBoolean(): BooleanType { return $this->getStaticObjectType()->toBoolean(); @@ -419,6 +639,15 @@ public function traverse(callable $cb): Type return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if ($this->subtractedType === null) { + return $this; + } + + return new self($this->classReflection); + } + public function subtract(Type $type): Type { if ($this->subtractedType !== null) { @@ -437,21 +666,17 @@ public function changeSubtractedType(?Type $subtractedType): Type { if ($subtractedType !== null) { $classReflection = $this->getClassReflection(); - if ($classReflection->isEnum()) { + if ($classReflection->getAllowedSubTypes() !== null) { $objectType = $this->getStaticObjectType()->changeSubtractedType($subtractedType); if ($objectType instanceof NeverType) { return $objectType; } - if ($objectType instanceof EnumCaseObjectType) { - return TypeCombinator::intersect($this, $objectType); - } - - if ($objectType instanceof ObjectType) { + if ($objectType instanceof ObjectType && $objectType->getSubtractedType() !== null) { return new self($classReflection, $objectType->getSubtractedType()); } - return $this; + return TypeCombinator::intersect($this, $objectType); } } @@ -472,6 +697,21 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } + public function exponentiate(Type $exponent): Type + { + return $this->getStaticObjectType()->exponentiate($exponent); + } + + public function getFiniteTypes(): array + { + return $this->getStaticObjectType()->getFiniteTypes(); + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('static'); + } + /** * @param mixed[] $properties */ diff --git a/src/Type/StrictMixedType.php b/src/Type/StrictMixedType.php index 5276ef19f7..2cb696d9f5 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -2,9 +2,12 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; @@ -13,7 +16,9 @@ use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; +use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; @@ -21,6 +26,8 @@ class StrictMixedType implements CompoundType { use UndecidedComparisonCompoundTypeTrait; + use NonArrayTypeTrait; + use NonIterableTypeTrait; use NonRemoveableTypeTrait; use NonGeneralizableTypeTrait; @@ -29,21 +36,46 @@ public function getReferencedClasses(): array return []; } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + + public function getConstantStrings(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic { - return TrinaryLogic::createYes(); + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + { + return AcceptsResult::createYes(); } public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + { + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult { if ($acceptingType instanceof self) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } if ($acceptingType instanceof MixedType && !$acceptingType instanceof TemplateMixedType) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } - return TrinaryLogic::createMaybe(); + return AcceptsResult::createMaybe(); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -70,7 +102,27 @@ public function equals(Type $type): bool public function describe(VerbosityLevel $level): string { - return 'mixed'; + return $level->handle( + static fn () => 'mixed', + static fn () => 'mixed', + static fn () => 'mixed', + static fn () => 'strict-mixed', + ); + } + + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return new ErrorType(); + } + + public function isObject(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isEnum(): TrinaryLogic + { + return TrinaryLogic::createNo(); } public function canAccessProperties(): TrinaryLogic @@ -103,7 +155,7 @@ public function hasMethod(string $methodName): TrinaryLogic return TrinaryLogic::createNo(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { throw new ShouldNotHappenException(); } @@ -148,12 +200,52 @@ public function getIterableValueType(): Type return $this; } - public function isArray(): TrinaryLogic + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function isOversizedArray(): TrinaryLogic + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -183,6 +275,36 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function isOffsetAccessible(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -203,6 +325,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new ErrorType(); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return new ErrorType(); + } + public function unsetOffset(Type $offsetType): Type { return new ErrorType(); @@ -253,6 +380,11 @@ public function toArray(): Type return new ErrorType(); } + public function toArrayKey(): Type + { + return new ErrorType(); + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { return TemplateTypeMap::createEmpty(); @@ -263,11 +395,36 @@ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVarianc return []; } + public function getEnumCases(): array + { + return []; + } + public function traverse(callable $cb): Type { return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('mixed'); + } + /** * @param mixed[] $properties */ diff --git a/src/Type/StringAlwaysAcceptingObjectWithToStringType.php b/src/Type/StringAlwaysAcceptingObjectWithToStringType.php index fbfd0b9b99..0130d7e094 100644 --- a/src/Type/StringAlwaysAcceptingObjectWithToStringType.php +++ b/src/Type/StringAlwaysAcceptingObjectWithToStringType.php @@ -8,21 +8,50 @@ class StringAlwaysAcceptingObjectWithToStringType extends StringType { - public function accepts(Type $type, bool $strictTypes): TrinaryLogic + public function isSuperTypeOf(Type $type): TrinaryLogic { - if ($type instanceof TypeWithClassName) { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if (!$reflectionProvider->hasClass($type->getClassName())) { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + $thatClassNames = $type->getObjectClassNames(); + if ($thatClassNames === []) { + return parent::isSuperTypeOf($type); + } + + $result = TrinaryLogic::createNo(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + foreach ($thatClassNames as $thatClassName) { + if (!$reflectionProvider->hasClass($thatClassName)) { return TrinaryLogic::createNo(); } - $typeClass = $reflectionProvider->getClass($type->getClassName()); - return TrinaryLogic::createFromBoolean( - $typeClass->hasNativeMethod('__toString'), - ); + $typeClass = $reflectionProvider->getClass($thatClassName); + $result = $result->or(TrinaryLogic::createFromBoolean($typeClass->hasNativeMethod('__toString'))); + } + + return $result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + { + $thatClassNames = $type->getObjectClassNames(); + if ($thatClassNames === []) { + return parent::acceptsWithReason($type, $strictTypes); + } + + $result = AcceptsResult::createNo(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + foreach ($thatClassNames as $thatClassName) { + if (!$reflectionProvider->hasClass($thatClassName)) { + return AcceptsResult::createNo(); + } + + $typeClass = $reflectionProvider->getClass($thatClassName); + $result = $result->or(AcceptsResult::createFromBoolean($typeClass->hasNativeMethod('__toString'))); } - return parent::accepts($type, $strictTypes); + return $result; } } diff --git a/src/Type/StringType.php b/src/Type/StringType.php index e0b5fa4e68..c9972ee053 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -2,19 +2,25 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +use function count; /** @api */ class StringType implements Type @@ -22,6 +28,7 @@ class StringType implements Type use JustNullableTypeTrait; use MaybeCallableTypeTrait; + use NonArrayTypeTrait; use NonIterableTypeTrait; use NonObjectTypeTrait; use UndecidedBooleanTypeTrait; @@ -39,6 +46,11 @@ public function describe(VerbosityLevel $level): string return 'string'; } + public function getConstantStrings(): array + { + return []; + } + public function isOffsetAccessible(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -46,7 +58,7 @@ public function isOffsetAccessible(): TrinaryLogic public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - return (new IntegerType())->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); + return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); } public function getOffsetValueType(Type $offsetType): Type @@ -69,41 +81,56 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new ErrorType(); } - if ((new IntegerType())->isSuperTypeOf($offsetType)->yes() || $offsetType instanceof MixedType) { + if ($offsetType->isInteger()->yes() || $offsetType instanceof MixedType) { return new StringType(); } return new ErrorType(); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { return new ErrorType(); } public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof self) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } - if ($type instanceof TypeWithClassName && !$strictTypes) { - $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if (!$reflectionProvider->hasClass($type->getClassName())) { - return TrinaryLogic::createNo(); - } + $thatClassNames = $type->getObjectClassNames(); + if (count($thatClassNames) > 1) { + throw new ShouldNotHappenException(); + } - $typeClass = $reflectionProvider->getClass($type->getClassName()); - return TrinaryLogic::createFromBoolean( - $typeClass->hasNativeMethod('__toString'), - ); + if ($thatClassNames === [] || $strictTypes) { + return AcceptsResult::createNo(); } - return TrinaryLogic::createNo(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if (!$reflectionProvider->hasClass($thatClassNames[0])) { + return AcceptsResult::createNo(); + } + + $typeClass = $reflectionProvider->getClass($thatClassNames[0]); + return AcceptsResult::createFromBoolean( + $typeClass->hasNativeMethod('__toString'), + ); } public function toNumber(): Type @@ -132,9 +159,46 @@ public function toArray(): Type [new ConstantIntegerType(0)], [$this], [1], + [], + TrinaryLogic::createYes(), ); } + public function toArrayKey(): Type + { + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -160,6 +224,39 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function hasMethod(string $methodName): TrinaryLogic + { + if ($this->isClassStringType()->yes()) { + return TrinaryLogic::createMaybe(); + } + return TrinaryLogic::createNo(); + } + public function tryRemove(Type $typeToRemove): ?Type { if ($typeToRemove instanceof ConstantStringType && $typeToRemove->getValue() === '') { @@ -173,6 +270,21 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } + public function getFiniteTypes(): array + { + return []; + } + + public function exponentiate(Type $exponent): Type + { + return ExponentiateHelper::exponentiate($this, $exponent); + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('string'); + } + /** * @param mixed[] $properties */ diff --git a/src/Type/ThisType.php b/src/Type/ThisType.php index 5a5b00e554..963f98c191 100644 --- a/src/Type/ThisType.php +++ b/src/Type/ThisType.php @@ -2,6 +2,8 @@ namespace PHPStan\Type; +use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\TrinaryLogic; @@ -71,6 +73,20 @@ public function traverse(callable $cb): Type return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if ($this->getSubtractedType() === null) { + return $this; + } + + return new self($this->getClassReflection()); + } + + public function toPhpDocNode(): TypeNode + { + return new ThisTypeNode(); + } + /** * @param mixed[] $properties */ diff --git a/src/Type/Traits/ConstantScalarTypeTrait.php b/src/Type/Traits/ConstantScalarTypeTrait.php index 4669a27219..5a8d6dcfc5 100644 --- a/src/Type/Traits/ConstantScalarTypeTrait.php +++ b/src/Type/Traits/ConstantScalarTypeTrait.php @@ -2,31 +2,42 @@ namespace PHPStan\Type\Traits; +use PHPStan\Php\PhpVersion; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\LooseComparisonHelper; use PHPStan\Type\Type; trait ConstantScalarTypeTrait { public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof self) { - return TrinaryLogic::createFromBoolean($this->value === $type->value); + return AcceptsResult::createFromBoolean($this->equals($type)); } if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return TrinaryLogic::createNo(); + return parent::acceptsWithReason($type, $strictTypes)->and(AcceptsResult::createMaybe()); } public function isSuperTypeOf(Type $type): TrinaryLogic { if ($type instanceof self) { - return $this->value === $type->value ? TrinaryLogic::createYes() : TrinaryLogic::createNo(); + return TrinaryLogic::createFromBoolean($this->equals($type)); } if ($type instanceof parent) { @@ -40,6 +51,24 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return TrinaryLogic::createNo(); } + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if (!$this instanceof ConstantScalarType) { + throw new ShouldNotHappenException(); + } + + if ($type instanceof ConstantScalarType) { + return LooseComparisonHelper::compareConstantScalars($this, $type, $phpVersion); + } + + if ($type->isConstantArray()->yes() && $type->isIterableAtLeastOnce()->no()) { + // @phpstan-ignore equal.notAllowed, equal.invalid + return new ConstantBooleanType($this->getValue() == []); // phpcs:ignore + } + + return parent::looseCompare($type, $phpVersion); + } + public function equals(Type $type): bool { return $type instanceof self && $this->value === $type->value; @@ -71,4 +100,29 @@ public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getConstantScalarTypes(): array + { + return [$this]; + } + + public function getConstantScalarValues(): array + { + return [$this->getValue()]; + } + + public function getFiniteTypes(): array + { + return [$this]; + } + } diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index 858533a9b1..294726f2d9 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -2,13 +2,15 @@ namespace PHPStan\Type\Traits; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\AcceptsResult; use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Generic\TemplateTypeMap; @@ -21,11 +23,41 @@ trait LateResolvableTypeTrait private ?Type $result = null; + public function getObjectClassNames(): array + { + return $this->resolve()->getObjectClassNames(); + } + + public function getObjectClassReflections(): array + { + return $this->resolve()->getObjectClassReflections(); + } + + public function getArrays(): array + { + return $this->resolve()->getArrays(); + } + + public function getConstantArrays(): array + { + return $this->resolve()->getConstantArrays(); + } + + public function getConstantStrings(): array + { + return $this->resolve()->getConstantStrings(); + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic { return $this->resolve()->accepts($type, $strictTypes); } + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + { + return $this->resolve()->acceptsWithReason($type, $strictTypes); + } + public function isSuperTypeOf(Type $type): TrinaryLogic { return $this->isSuperTypeOfDefault($type); @@ -50,9 +82,24 @@ private function isSuperTypeOfDefault(Type $type): TrinaryLogic return $isSuperType; } + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return $this->resolve()->getTemplateType($ancestorClassName, $templateTypeName); + } + + public function isObject(): TrinaryLogic + { + return $this->resolve()->isObject(); + } + + public function isEnum(): TrinaryLogic + { + return $this->resolve()->isEnum(); + } + public function canAccessProperties(): TrinaryLogic { - return $this->resolve()->canAccessConstants(); + return $this->resolve()->canAccessProperties(); } public function hasProperty(string $propertyName): TrinaryLogic @@ -80,7 +127,7 @@ public function hasMethod(string $methodName): TrinaryLogic return $this->resolve()->hasMethod($methodName); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->resolve()->getMethod($methodName, $scope); } @@ -115,26 +162,61 @@ public function isIterableAtLeastOnce(): TrinaryLogic return $this->resolve()->isIterableAtLeastOnce(); } + public function getArraySize(): Type + { + return $this->resolve()->getArraySize(); + } + public function getIterableKeyType(): Type { return $this->resolve()->getIterableKeyType(); } + public function getFirstIterableKeyType(): Type + { + return $this->resolve()->getFirstIterableKeyType(); + } + + public function getLastIterableKeyType(): Type + { + return $this->resolve()->getLastIterableKeyType(); + } + public function getIterableValueType(): Type { return $this->resolve()->getIterableValueType(); } + public function getFirstIterableValueType(): Type + { + return $this->resolve()->getFirstIterableValueType(); + } + + public function getLastIterableValueType(): Type + { + return $this->resolve()->getLastIterableValueType(); + } + public function isArray(): TrinaryLogic { return $this->resolve()->isArray(); } + public function isConstantArray(): TrinaryLogic + { + return $this->resolve()->isConstantArray(); + } + public function isOversizedArray(): TrinaryLogic { return $this->resolve()->isOversizedArray(); } + public function isList(): TrinaryLogic + { + return $this->resolve()->isList(); + } + public function isOffsetAccessible(): TrinaryLogic { return $this->resolve()->isOffsetAccessible(); @@ -155,16 +237,71 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this->resolve()->setOffsetValueType($offsetType, $valueType, $unionValues); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this->resolve()->setExistingOffsetValueType($offsetType, $valueType); + } + public function unsetOffset(Type $offsetType): Type { return $this->resolve()->unsetOffset($offsetType); } + public function getKeysArray(): Type + { + return $this->resolve()->getKeysArray(); + } + + public function getValuesArray(): Type + { + return $this->resolve()->getValuesArray(); + } + + public function fillKeysArray(Type $valueType): Type + { + return $this->resolve()->fillKeysArray($valueType); + } + + public function flipArray(): Type + { + return $this->resolve()->flipArray(); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + return $this->resolve()->intersectKeyArray($otherArraysType); + } + + public function popArray(): Type + { + return $this->resolve()->popArray(); + } + + public function searchArray(Type $needleType): Type + { + return $this->resolve()->searchArray($needleType); + } + + public function shiftArray(): Type + { + return $this->resolve()->shiftArray(); + } + + public function shuffleArray(): Type + { + return $this->resolve()->shuffleArray(); + } + public function isCallable(): TrinaryLogic { return $this->resolve()->isCallable(); } + public function getEnumCases(): array + { + return $this->resolve()->getEnumCases(); + } + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return $this->resolve()->getCallableParametersAcceptors($scope); @@ -205,6 +342,11 @@ public function toArray(): Type return $this->resolve()->toArray(); } + public function toArrayKey(): Type + { + return $this->resolve()->toArrayKey(); + } + public function isSmallerThan(Type $otherType): TrinaryLogic { return $this->resolve()->isSmallerThan($otherType); @@ -215,6 +357,56 @@ public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic return $this->resolve()->isSmallerThanOrEqual($otherType); } + public function isNull(): TrinaryLogic + { + return $this->resolve()->isNull(); + } + + public function isConstantValue(): TrinaryLogic + { + return $this->resolve()->isConstantValue(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return $this->resolve()->isConstantScalarValue(); + } + + public function getConstantScalarTypes(): array + { + return $this->resolve()->getConstantScalarTypes(); + } + + public function getConstantScalarValues(): array + { + return $this->resolve()->getConstantScalarValues(); + } + + public function isTrue(): TrinaryLogic + { + return $this->resolve()->isTrue(); + } + + public function isFalse(): TrinaryLogic + { + return $this->resolve()->isFalse(); + } + + public function isBoolean(): TrinaryLogic + { + return $this->resolve()->isBoolean(); + } + + public function isFloat(): TrinaryLogic + { + return $this->resolve()->isFloat(); + } + + public function isInteger(): TrinaryLogic + { + return $this->resolve()->isInteger(); + } + public function isString(): TrinaryLogic { return $this->resolve()->isString(); @@ -240,6 +432,36 @@ public function isLiteralString(): TrinaryLogic return $this->resolve()->isLiteralString(); } + public function isClassStringType(): TrinaryLogic + { + return $this->resolve()->isClassStringType(); + } + + public function getClassStringObjectType(): Type + { + return $this->resolve()->getClassStringObjectType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this->resolve()->getObjectTypeOrClassStringObjectType(); + } + + public function isVoid(): TrinaryLogic + { + return $this->resolve()->isVoid(); + } + + public function isScalar(): TrinaryLogic + { + return $this->resolve()->isScalar(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function getSmallerType(): Type { return $this->resolve()->getSmallerType(); @@ -282,14 +504,19 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic } public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + { + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult { $result = $this->resolve(); if ($result instanceof CompoundType) { - return $result->isAcceptedBy($acceptingType, $strictTypes); + return $result->isAcceptedWithReasonBy($acceptingType, $strictTypes); } - return $acceptingType->accepts($result, $strictTypes); + return $acceptingType->acceptsWithReason($result, $strictTypes); } public function isGreaterThan(Type $otherType): TrinaryLogic @@ -314,6 +541,16 @@ public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic return $otherType->isSmallerThanOrEqual($result); } + public function exponentiate(Type $exponent): Type + { + return $this->resolve()->exponentiate($exponent); + } + + public function getFiniteTypes(): array + { + return $this->resolve()->getFiniteTypes(); + } + public function resolve(): Type { if ($this->result === null) { diff --git a/src/Type/Traits/MaybeArrayTypeTrait.php b/src/Type/Traits/MaybeArrayTypeTrait.php new file mode 100644 index 0000000000..da064c82b7 --- /dev/null +++ b/src/Type/Traits/MaybeArrayTypeTrait.php @@ -0,0 +1,87 @@ +isIterable()->no()) { + return new ErrorType(); + } + + if ($this->isIterableAtLeastOnce()->yes()) { + return IntegerRangeType::fromInterval(1, null); + } + + return IntegerRangeType::fromInterval(0, null); + } + public function getIterableKeyType(): Type { return new MixedType(); } + public function getFirstIterableKeyType(): Type + { + return new MixedType(); + } + + public function getLastIterableKeyType(): Type + { + return new MixedType(); + } + public function getIterableValueType(): Type { return new MixedType(); } + public function getFirstIterableValueType(): Type + { + return new MixedType(); + } + + public function getLastIterableValueType(): Type + { + return new MixedType(); + } + } diff --git a/src/Type/Traits/MaybeObjectTypeTrait.php b/src/Type/Traits/MaybeObjectTypeTrait.php index 607e9360ee..b8097947b4 100644 --- a/src/Type/Traits/MaybeObjectTypeTrait.php +++ b/src/Type/Traits/MaybeObjectTypeTrait.php @@ -7,18 +7,34 @@ use PHPStan\Reflection\Dummy\DummyConstantReflection; use PHPStan\Reflection\Dummy\DummyMethodReflection; use PHPStan\Reflection\Dummy\DummyPropertyReflection; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; trait MaybeObjectTypeTrait { + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return new MixedType(); + } + + public function isObject(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isEnum(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function canAccessProperties(): TrinaryLogic { return TrinaryLogic::createMaybe(); @@ -55,7 +71,7 @@ public function hasMethod(string $methodName): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } diff --git a/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php b/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php index 55889eb574..6e83395e95 100644 --- a/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php +++ b/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php @@ -29,6 +29,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { return $this; diff --git a/src/Type/Traits/NonArrayTypeTrait.php b/src/Type/Traits/NonArrayTypeTrait.php new file mode 100644 index 0000000000..def99b0e94 --- /dev/null +++ b/src/Type/Traits/NonArrayTypeTrait.php @@ -0,0 +1,87 @@ +getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -95,17 +113,62 @@ public function getConstant(string $constantName): ConstantReflection return new DummyConstantReflection($constantName); } + public function getConstantStrings(): array + { + return []; + } + public function isCloneable(): TrinaryLogic { return TrinaryLogic::createYes(); } - public function isArray(): TrinaryLogic + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function isOversizedArray(): TrinaryLogic + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -135,6 +198,36 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this; + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function toNumber(): Type { return new ErrorType(); @@ -142,7 +235,7 @@ public function toNumber(): Type public function toString(): Type { - return new StringType(); + return new ErrorType(); } public function toInteger(): Type @@ -160,4 +253,9 @@ public function toArray(): Type return new ArrayType(new MixedType(), new MixedType()); } + public function toArrayKey(): Type + { + return new StringType(); + } + } diff --git a/src/Type/Type.php b/src/Type/Type.php index c9d7657bd8..c046c3048f 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -2,19 +2,28 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\ClassMemberAccessAnswerer; +use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeReference; use PHPStan\Type\Generic\TemplateTypeVariance; -/** @api */ +/** + * @api + * @see https://phpstan.org/developing-extensions/type-system + */ interface Type { @@ -23,8 +32,49 @@ interface Type */ public function getReferencedClasses(): array; + /** @return list */ + public function getObjectClassNames(): array; + + /** + * @return list + */ + public function getObjectClassReflections(): array; + + /** + * Returns object type Foo for class-string and 'Foo' (if Foo is a valid class). + */ + public function getClassStringObjectType(): Type; + + /** + * Returns object type Foo for class-string, 'Foo' (if Foo is a valid class), + * and object type Foo. + */ + public function getObjectTypeOrClassStringObjectType(): Type; + + public function isObject(): TrinaryLogic; + + public function isEnum(): TrinaryLogic; + + /** @return list */ + public function getArrays(): array; + + /** @return list */ + public function getConstantArrays(): array; + + /** @return list */ + public function getConstantStrings(): array; + public function accepts(Type $type, bool $strictTypes): TrinaryLogic; + /** + * This is like accepts() but gives reasons + * why the type was not/might not be accepted in some non-intuitive scenarios. + * + * In PHPStan 2.0 this method will be removed and the return type of accepts() + * will change to AcceptsResult. + */ + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult; + public function isSuperTypeOf(Type $type): TrinaryLogic; public function equals(Type $type): bool; @@ -43,7 +93,7 @@ public function canCallMethods(): TrinaryLogic; public function hasMethod(string $methodName): TrinaryLogic; - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection; + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection; public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection; @@ -57,14 +107,28 @@ public function isIterable(): TrinaryLogic; public function isIterableAtLeastOnce(): TrinaryLogic; + public function getArraySize(): Type; + public function getIterableKeyType(): Type; + public function getFirstIterableKeyType(): Type; + + public function getLastIterableKeyType(): Type; + public function getIterableValueType(): Type; + public function getFirstIterableValueType(): Type; + + public function getLastIterableValueType(): Type; + public function isArray(): TrinaryLogic; + public function isConstantArray(): TrinaryLogic; + public function isOversizedArray(): TrinaryLogic; + public function isList(): TrinaryLogic; + public function isOffsetAccessible(): TrinaryLogic; public function hasOffsetValueType(Type $offsetType): TrinaryLogic; @@ -73,12 +137,55 @@ public function getOffsetValueType(Type $offsetType): Type; public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type; + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type; + public function unsetOffset(Type $offsetType): Type; + public function getKeysArray(): Type; + + public function getValuesArray(): Type; + + public function fillKeysArray(Type $valueType): Type; + + public function flipArray(): Type; + + public function intersectKeyArray(Type $otherArraysType): Type; + + public function popArray(): Type; + + public function searchArray(Type $needleType): Type; + + public function shiftArray(): Type; + + public function shuffleArray(): Type; + + /** + * @return list + */ + public function getEnumCases(): array; + + /** + * Returns a list of finite values. + * + * Examples: + * + * - for bool: [true, false] + * - for int<0, 3>: [0, 1, 2, 3] + * - for enums: list of enum cases + * - for scalars: the scalar itself + * + * For infinite types it returns an empty array. + * + * @return list + */ + public function getFiniteTypes(): array; + + public function exponentiate(Type $exponent): Type; + public function isCallable(): TrinaryLogic; /** - * @return ParametersAcceptor[] + * @return CallableParametersAcceptor[] */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array; @@ -96,10 +203,44 @@ public function toString(): Type; public function toArray(): Type; + public function toArrayKey(): Type; + public function isSmallerThan(Type $otherType): TrinaryLogic; public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic; + /** + * Is Type of a known constant value? Includes literal strings, integers, floats, true, false, null, and array shapes. + */ + public function isConstantValue(): TrinaryLogic; + + /** + * Is Type of a known constant scalar value? Includes literal strings, integers, floats, true, false, and null. + */ + public function isConstantScalarValue(): TrinaryLogic; + + /** + * @return list + */ + public function getConstantScalarTypes(): array; + + /** + * @return list + */ + public function getConstantScalarValues(): array; + + public function isNull(): TrinaryLogic; + + public function isTrue(): TrinaryLogic; + + public function isFalse(): TrinaryLogic; + + public function isBoolean(): TrinaryLogic; + + public function isFloat(): TrinaryLogic; + + public function isInteger(): TrinaryLogic; + public function isString(): TrinaryLogic; public function isNumericString(): TrinaryLogic; @@ -110,6 +251,14 @@ public function isNonFalsyString(): TrinaryLogic; public function isLiteralString(): TrinaryLogic; + public function isClassStringType(): TrinaryLogic; + + public function isVoid(): TrinaryLogic; + + public function isScalar(): TrinaryLogic; + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType; + public function getSmallerType(): Type; public function getSmallerOrEqualType(): Type; @@ -118,6 +267,24 @@ public function getGreaterType(): Type; public function getGreaterOrEqualType(): Type; + /** + * Returns actual template type for a given object. + * + * Example: + * + * @-template T + * class Foo {} + * + * // $fooType is Foo + * $t = $fooType->getTemplateType(Foo::class, 'T'); + * $t->isInteger(); // yes + * + * Returns ErrorType in case of a missing type. + * + * @param class-string $ancestorClassName + */ + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type; + /** * Infers template types * @@ -154,6 +321,15 @@ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVarianc */ public function traverse(callable $cb): Type; + /** + * Traverses inner types while keeping the same context in another type. + * + * @param callable(Type $left, Type $right): Type $cb + */ + public function traverseSimultaneously(Type $right, callable $cb): Type; + + public function toPhpDocNode(): TypeNode; + /** * Return the difference with another type, or null if it cannot be represented. * diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 13bb9fe431..5159711e6f 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -2,10 +2,12 @@ namespace PHPStan\Type; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\HasOffsetValueType; +use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Accessory\OversizedArrayType; use PHPStan\Type\Constant\ConstantArrayType; @@ -14,14 +16,15 @@ use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\Generic\TemplateArrayType; use PHPStan\Type\Generic\TemplateBenevolentUnionType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateUnionType; -use function array_intersect_key; use function array_key_exists; -use function array_keys; +use function array_key_first; use function array_map; use function array_merge; use function array_slice; @@ -73,7 +76,41 @@ public static function remove(Type $fromType, Type $typeToRemove): Type } } - return $fromType->tryRemove($typeToRemove) ?? $fromType; + $removed = $fromType->tryRemove($typeToRemove); + if ($removed !== null) { + return $removed; + } + + $fromFiniteTypes = $fromType->getFiniteTypes(); + if (count($fromFiniteTypes) > 0) { + $finiteTypesToRemove = $typeToRemove->getFiniteTypes(); + if (count($finiteTypesToRemove) === 1) { + $result = []; + foreach ($fromFiniteTypes as $finiteType) { + if ($finiteType->equals($finiteTypesToRemove[0])) { + continue; + } + + $result[] = $finiteType; + } + + if (count($result) === count($fromFiniteTypes)) { + return $fromType; + } + + if (count($result) === 0) { + return new NeverType(); + } + + if (count($result) === 1) { + return $result[0]; + } + + return new UnionType($result); + } + } + + return $fromType; } public static function removeNull(Type $type): Type @@ -142,9 +179,9 @@ public static function union(Type ...$types): Type } $arrayTypes = []; - $arrayAccessoryTypes = []; $scalarTypes = []; $hasGenericScalarTypes = []; + $enumCaseTypes = []; for ($i = 0; $i < $typesCount; $i++) { if ($types[$i] instanceof ConstantScalarType) { $type = $types[$i]; @@ -164,47 +201,18 @@ public static function union(Type ...$types): Type if ($types[$i] instanceof StringType && !$types[$i] instanceof ClassStringType) { $hasGenericScalarTypes[ConstantStringType::class] = true; } - if ($types[$i] instanceof IntersectionType) { - $intermediateArrayType = null; - $intermediateAccessoryTypes = []; - foreach ($types[$i]->getTypes() as $innerType) { - if ($innerType instanceof TemplateType) { - continue 2; - } - if ($innerType instanceof ArrayType) { - $intermediateArrayType = $innerType; - continue; - } - if ($innerType instanceof AccessoryType || $innerType instanceof CallableType) { - if ($innerType instanceof HasOffsetValueType) { - $intermediateAccessoryTypes[sprintf('hasOffsetValue(%s)', $innerType->getOffsetType()->describe(VerbosityLevel::cache()))][] = $innerType; - continue; - } + if ($types[$i] instanceof EnumCaseObjectType) { + $enumCaseTypes[$types[$i]->describe(VerbosityLevel::cache())] = $types[$i]; - $intermediateAccessoryTypes[$innerType->describe(VerbosityLevel::cache())][] = $innerType; - continue; - } - } - - if ($intermediateArrayType !== null) { - $arrayTypes[] = $intermediateArrayType; - $arrayAccessoryTypes[] = $intermediateAccessoryTypes; - unset($types[$i]); - continue; - } + unset($types[$i]); + continue; } - if (!$types[$i] instanceof ArrayType) { + + if (!$types[$i]->isArray()->yes()) { continue; } $arrayTypes[] = $types[$i]; - - if ($types[$i]->isIterableAtLeastOnce()->yes()) { - $nonEmpty = new NonEmptyArrayType(); - $arrayAccessoryTypes[] = [$nonEmpty->describe(VerbosityLevel::cache()) => [$nonEmpty]]; - } else { - $arrayAccessoryTypes[] = []; - } unset($types[$i]); } @@ -212,54 +220,10 @@ public static function union(Type ...$types): Type $scalarTypes[$classType] = array_values($scalarTypeItems); } - /** @var ArrayType[] $arrayTypes */ - $arrayTypes = $arrayTypes; - - $commonArrayAccessoryTypesKeys = []; - if (count($arrayAccessoryTypes) > 1) { - $commonArrayAccessoryTypesKeys = array_keys(array_intersect_key(...$arrayAccessoryTypes)); - } elseif (count($arrayAccessoryTypes) > 0) { - $commonArrayAccessoryTypesKeys = array_keys($arrayAccessoryTypes[0]); - } - - $arrayAccessoryTypesToProcess = []; - foreach ($commonArrayAccessoryTypesKeys as $commonKey) { - $typesToUnion = []; - foreach ($arrayAccessoryTypes as $array) { - foreach ($array[$commonKey] as $arrayAccessoryType) { - $typesToUnion[] = $arrayAccessoryType; - } - } - $arrayAccessoryTypesToProcess[] = self::union(...$typesToUnion); - } - - $types = array_values( - array_merge( - $types, - self::processArrayTypes($arrayTypes, $arrayAccessoryTypesToProcess), - ), - ); + $enumCaseTypes = array_values($enumCaseTypes); + $types = array_values($types); $typesCount = count($types); - // simplify string[] | int[] to (string|int)[] - for ($i = 0; $i < $typesCount; $i++) { - if (! $types[$i] instanceof IterableType) { - continue; - } - - for ($j = $i + 1; $j < $typesCount; $j++) { - if ($types[$j] instanceof IterableType) { - $types[$i] = new IterableType( - self::union($types[$i]->getIterableKeyType(), $types[$j]->getIterableKeyType()), - self::union($types[$i]->getIterableValueType(), $types[$j]->getIterableValueType()), - ); - array_splice($types, $j, 1); - $typesCount--; - continue 2; - } - } - } - foreach ($scalarTypes as $classType => $scalarTypeItems) { if (isset($hasGenericScalarTypes[$classType])) { unset($scalarTypes[$classType]); @@ -305,9 +269,14 @@ public static function union(Type ...$types): Type $newTypes[$type->describe(VerbosityLevel::cache())] = $type; } $types = array_values($newTypes); - $typesCount = count($types); } + $types = array_merge( + $types, + self::processArrayTypes($arrayTypes), + ); + $typesCount = count($types); + // transform A | A to A // transform A | never to A for ($i = 0; $i < $typesCount; $i++) { @@ -333,6 +302,35 @@ public static function union(Type ...$types): Type } } + $enumCasesCount = count($enumCaseTypes); + for ($i = 0; $i < $typesCount; $i++) { + for ($j = 0; $j < $enumCasesCount; $j++) { + $compareResult = self::compareTypesInUnion($types[$i], $enumCaseTypes[$j]); + if ($compareResult === null) { + continue; + } + + [$a, $b] = $compareResult; + if ($a !== null) { + $types[$i] = $a; + array_splice($enumCaseTypes, $j--, 1); + $enumCasesCount--; + continue 1; + } + if ($b !== null) { + $enumCaseTypes[$j] = $b; + array_splice($types, $i--, 1); + $typesCount--; + continue 2; + } + } + } + + foreach ($enumCaseTypes as $enumCaseType) { + $types[] = $enumCaseType; + $typesCount++; + } + foreach ($scalarTypes as $scalarTypeItems) { foreach ($scalarTypeItems as $scalarType) { $types[] = $scalarType; @@ -366,7 +364,7 @@ public static function union(Type ...$types): Type } } - return new UnionType($types); + return new UnionType($types, true); } /** @@ -396,10 +394,21 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array return [new HasOffsetValueType($a->getOffsetType(), self::union($a->getValueType(), $b->getValueType())), null]; } } - if ($a instanceof ConstantArrayType && $b instanceof ConstantArrayType) { + if ($a->isConstantArray()->yes() && $b->isConstantArray()->yes()) { return null; } + // simplify string[] | int[] to (string|int)[] + if ($a instanceof IterableType && $b instanceof IterableType) { + return [ + new IterableType( + self::union($a->getIterableKeyType(), $b->getIterableKeyType()), + self::union($a->getIterableValueType(), $b->getIterableValueType()), + ), + null, + ]; + } + if ($a instanceof SubtractableType) { $typeWithoutSubtractedTypeA = $a->getTypeWithoutSubtractedType(); if ($typeWithoutSubtractedTypeA instanceof MixedType && $b instanceof MixedType) { @@ -487,14 +496,9 @@ private static function unionWithSubtractedType( } if ($type instanceof SubtractableType) { - if ($type->getSubtractedType() === null) { - return $type; - } - - $subtractedType = self::union( - $type->getSubtractedType(), - $subtractedType, - ); + $subtractedType = $type->getSubtractedType() === null + ? $subtractedType + : self::union($type->getSubtractedType(), $subtractedType); if ($subtractedType instanceof NeverType) { $subtractedType = null; } @@ -572,33 +576,84 @@ private static function intersectWithSubtractedType( } /** - * @param ArrayType[] $arrayTypes - * @param Type[] $accessoryTypes + * @param Type[] $arrayTypes * @return Type[] */ - private static function processArrayTypes(array $arrayTypes, array $accessoryTypes): array + private static function processArrayAccessoryTypes(array $arrayTypes): array { - foreach ($arrayTypes as $arrayType) { - if (!$arrayType instanceof ConstantArrayType) { - continue; + $accessoryTypes = []; + foreach ($arrayTypes as $i => $arrayType) { + if ($arrayType instanceof IntersectionType) { + foreach ($arrayType->getTypes() as $innerType) { + if ($innerType instanceof TemplateType) { + break; + } + if (!($innerType instanceof AccessoryType) && !($innerType instanceof CallableType)) { + continue; + } + if ($innerType instanceof HasOffsetValueType) { + $accessoryTypes[sprintf('hasOffsetValue(%s)', $innerType->getOffsetType()->describe(VerbosityLevel::cache()))][$i] = $innerType; + continue; + } + + $accessoryTypes[$innerType->describe(VerbosityLevel::cache())][$i] = $innerType; + } } - if (count($arrayType->getKeyTypes()) > 0) { + + if (!$arrayType->isConstantArray()->yes()) { continue; } + $constantArrays = $arrayType->getConstantArrays(); - foreach ($accessoryTypes as $i => $accessoryType) { - if (!$accessoryType instanceof NonEmptyArrayType) { + foreach ($constantArrays as $constantArray) { + if ($constantArray->isList()->yes() && AccessoryArrayListType::isListTypeEnabled()) { + $list = new AccessoryArrayListType(); + $accessoryTypes[$list->describe(VerbosityLevel::cache())][$i] = $list; + } + + if (!$constantArray->isIterableAtLeastOnce()->yes()) { continue; } - unset($accessoryTypes[$i]); - break 2; + $nonEmpty = new NonEmptyArrayType(); + $accessoryTypes[$nonEmpty->describe(VerbosityLevel::cache())][$i] = $nonEmpty; } } + $commonAccessoryTypes = []; + $arrayTypeCount = count($arrayTypes); + foreach ($accessoryTypes as $accessoryType) { + if (count($accessoryType) !== $arrayTypeCount) { + $firstKey = array_key_first($accessoryType); + if ($accessoryType[$firstKey] instanceof OversizedArrayType) { + $commonAccessoryTypes[] = $accessoryType[$firstKey]; + } + continue; + } + + if ($accessoryType[0] instanceof HasOffsetValueType) { + $commonAccessoryTypes[] = self::union(...$accessoryType); + continue; + } + + $commonAccessoryTypes[] = $accessoryType[0]; + } + + return $commonAccessoryTypes; + } + + /** + * @param list $arrayTypes + * @return Type[] + */ + private static function processArrayTypes(array $arrayTypes): array + { if ($arrayTypes === []) { return []; } + + $accessoryTypes = self::processArrayAccessoryTypes($arrayTypes); + if (count($arrayTypes) === 1) { return [ self::intersect(...$arrayTypes, ...$accessoryTypes), @@ -609,72 +664,209 @@ private static function processArrayTypes(array $arrayTypes, array $accessoryTyp $valueTypesForGeneralArray = []; $generalArrayOccurred = false; $constantKeyTypesNumbered = []; + $filledArrays = 0; + $overflowed = false; /** @var int|float $nextConstantKeyTypeIndex */ $nextConstantKeyTypeIndex = 1; + $constantArraysMap = array_map( + static fn (Type $t) => $t->getConstantArrays(), + $arrayTypes, + ); - foreach ($arrayTypes as $arrayType) { - if (!$arrayType instanceof ConstantArrayType || $generalArrayOccurred) { - $keyTypesForGeneralArray[] = $arrayType->getKeyType(); - $valueTypesForGeneralArray[] = $arrayType->getItemType(); - $generalArrayOccurred = true; + foreach ($arrayTypes as $arrayIdx => $arrayType) { + $constantArrays = $constantArraysMap[$arrayIdx]; + $isConstantArray = $constantArrays !== []; + if (!$isConstantArray || !$arrayType->isIterableAtLeastOnce()->no()) { + $filledArrays++; + } + + if ($generalArrayOccurred || !$isConstantArray) { + foreach ($arrayType->getArrays() as $type) { + $keyTypesForGeneralArray[] = $type->getIterableKeyType(); + $valueTypesForGeneralArray[] = $type->getItemType(); + $generalArrayOccurred = true; + } continue; } - foreach ($arrayType->getKeyTypes() as $i => $keyType) { - $keyTypesForGeneralArray[] = $keyType; - $valueTypesForGeneralArray[] = $arrayType->getValueTypes()[$i]; + $constantArrays = $arrayType->getConstantArrays(); + foreach ($constantArrays as $constantArray) { + foreach ($constantArray->getKeyTypes() as $i => $keyType) { + $keyTypesForGeneralArray[] = $keyType; + $valueTypesForGeneralArray[] = $constantArray->getValueTypes()[$i]; - $keyTypeValue = $keyType->getValue(); - if (array_key_exists($keyTypeValue, $constantKeyTypesNumbered)) { - continue; - } + $keyTypeValue = $keyType->getValue(); + if (array_key_exists($keyTypeValue, $constantKeyTypesNumbered)) { + continue; + } - $constantKeyTypesNumbered[$keyTypeValue] = $nextConstantKeyTypeIndex; - $nextConstantKeyTypeIndex *= 2; - if (!is_int($nextConstantKeyTypeIndex)) { - $generalArrayOccurred = true; - continue; + $constantKeyTypesNumbered[$keyTypeValue] = $nextConstantKeyTypeIndex; + $nextConstantKeyTypeIndex *= 2; + if (!is_int($nextConstantKeyTypeIndex)) { + $generalArrayOccurred = true; + $overflowed = true; + continue 2; + } } } } - if ($generalArrayOccurred) { + if ($generalArrayOccurred && (!$overflowed || $filledArrays > 1)) { + $reducedArrayTypes = self::reduceArrays($arrayTypes, false); + if (count($reducedArrayTypes) === 1) { + return [self::intersect($reducedArrayTypes[0], ...$accessoryTypes)]; + } + $scopes = []; + $useTemplateArray = true; + foreach ($arrayTypes as $arrayType) { + if (!$arrayType instanceof TemplateArrayType) { + $useTemplateArray = false; + break; + } + + $scopes[$arrayType->getScope()->describe()] = $arrayType; + } + + $arrayType = new ArrayType( + self::union(...$keyTypesForGeneralArray), + self::union(...self::optimizeConstantArrays($valueTypesForGeneralArray)), + ); + + if ($useTemplateArray && count($scopes) === 1) { + $templateArray = array_values($scopes)[0]; + $arrayType = new TemplateArrayType( + $templateArray->getScope(), + $templateArray->getStrategy(), + $templateArray->getVariance(), + $templateArray->getName(), + $arrayType, + ); + } + return [ - self::intersect(new ArrayType( - self::union(...$keyTypesForGeneralArray), - self::union(...$valueTypesForGeneralArray), - ), ...$accessoryTypes), + self::intersect($arrayType, ...$accessoryTypes), ]; } + $reducedArrayTypes = self::reduceArrays($arrayTypes, true); + return array_map( static fn (Type $arrayType) => self::intersect($arrayType, ...$accessoryTypes), - self::reduceArrays($arrayTypes), + self::optimizeConstantArrays($reducedArrayTypes), ); } /** - * @param Type[] $constantArrays + * @param Type[] $types * @return Type[] */ - private static function reduceArrays(array $constantArrays): array + private static function optimizeConstantArrays(array $types): array + { + $constantArrayValuesCount = self::countConstantArrayValueTypes($types); + + if ($constantArrayValuesCount > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + $results = []; + foreach ($types as $type) { + $results[] = TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { + if ($type instanceof ConstantArrayType) { + if ($type->isIterableAtLeastOnce()->no()) { + return $type; + } + + $isList = true; + $valueTypes = []; + $keyTypes = []; + $nextAutoIndex = 0; + foreach ($type->getKeyTypes() as $i => $innerKeyType) { + if (!$innerKeyType instanceof ConstantIntegerType) { + $isList = false; + } elseif ($innerKeyType->getValue() !== $nextAutoIndex) { + $isList = false; + $nextAutoIndex = $innerKeyType->getValue() + 1; + } else { + $nextAutoIndex++; + } + + $generalizedKeyType = $innerKeyType->generalize(GeneralizePrecision::moreSpecific()); + $keyTypes[$generalizedKeyType->describe(VerbosityLevel::precise())] = $generalizedKeyType; + + $innerValueType = $type->getValueTypes()[$i]; + $generalizedValueType = TypeTraverser::map($innerValueType, static function (Type $type, callable $innerTraverse) use ($traverse): Type { + if ($type instanceof ArrayType) { + return TypeCombinator::intersect($type, new OversizedArrayType()); + } + + return $traverse($type); + }); + $valueTypes[$generalizedValueType->describe(VerbosityLevel::precise())] = $generalizedValueType; + } + + $keyType = TypeCombinator::union(...array_values($keyTypes)); + $valueType = TypeCombinator::union(...array_values($valueTypes)); + + $arrayType = new ArrayType($keyType, $valueType); + if ($isList) { + $arrayType = AccessoryArrayListType::intersectWith($arrayType); + } + + return TypeCombinator::intersect($arrayType, new NonEmptyArrayType(), new OversizedArrayType()); + } + + return $traverse($type); + }); + } + + return $results; + } + + return $types; + } + + /** + * @param Type[] $types + */ + private static function countConstantArrayValueTypes(array $types): int + { + $constantArrayValuesCount = 0; + foreach ($types as $type) { + TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$constantArrayValuesCount): Type { + if ($type instanceof ConstantArrayType) { + $constantArrayValuesCount += count($type->getValueTypes()); + } + + return $traverse($type); + }); + } + return $constantArrayValuesCount; + } + + /** + * @param list $constantArrays + * @return list + */ + private static function reduceArrays(array $constantArrays, bool $preserveTaggedUnions): array { $newArrays = []; $arraysToProcess = []; $emptyArray = null; foreach ($constantArrays as $constantArray) { - if (!$constantArray instanceof ConstantArrayType) { + if (!$constantArray->isConstantArray()->yes()) { + // This is an optimization for current use-case of $preserveTaggedUnions=false, where we need + // one constant array as a result, or we generalize the $constantArrays. + if (!$preserveTaggedUnions) { + return $constantArrays; + } $newArrays[] = $constantArray; continue; } - if ($constantArray->isEmpty()) { + if ($constantArray->isIterableAtLeastOnce()->no()) { $emptyArray = $constantArray; continue; } - $arraysToProcess[] = $constantArray; + $arraysToProcess = array_merge($arraysToProcess, $constantArray->getConstantArrays()); } if ($emptyArray !== null) { @@ -710,7 +902,8 @@ private static function reduceArrays(array $constantArrays): array } if ( - $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes()) + $preserveTaggedUnions + && $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes()) && $arraysToProcess[$j]->isKeysSupersetOf($arraysToProcess[$i]) ) { $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]); @@ -719,13 +912,25 @@ private static function reduceArrays(array $constantArrays): array } if ( - $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes()) + $preserveTaggedUnions + && $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes()) && $arraysToProcess[$i]->isKeysSupersetOf($arraysToProcess[$j]) ) { $arraysToProcess[$i] = $arraysToProcess[$i]->mergeWith($arraysToProcess[$j]); unset($arraysToProcess[$j]); continue 1; } + + if ( + !$preserveTaggedUnions + // both arrays have same keys + && $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes()) + && $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes()) + ) { + $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]); + unset($arraysToProcess[$i]); + continue 2; + } } } @@ -736,6 +941,14 @@ public static function intersect(Type ...$types): Type { $types = array_values($types); + $typesCount = count($types); + if ($typesCount === 0) { + return new NeverType(); + } + if ($typesCount === 1) { + return $types[0]; + } + $sortTypes = static function (Type $a, Type $b): int { if (!$a instanceof UnionType || !$b instanceof UnionType) { return 0; @@ -825,7 +1038,7 @@ public static function intersect(Type ...$types): Type if ($hasOffsetValueTypeCount > 32) { $newTypes[] = new OversizedArrayType(); - $types = array_values($newTypes); + $types = $newTypes; $typesCount = count($types); } @@ -938,7 +1151,7 @@ public static function intersect(Type ...$types): Type $valueType = $types[$j]->getValueType(); $newValueType = self::intersect($types[$i]->getOffsetValueType($offsetType), $valueType); if ($newValueType instanceof NeverType) { - return new NeverType(); + return $newValueType; } $types[$i] = $types[$i]->setOffsetValueType($offsetType, $newValueType); array_splice($types, $j--, 1); @@ -951,7 +1164,7 @@ public static function intersect(Type ...$types): Type $valueType = $types[$i]->getValueType(); $newValueType = self::intersect($types[$j]->getOffsetValueType($offsetType), $valueType); if ($newValueType instanceof NeverType) { - return new NeverType(); + return $newValueType; } $types[$j] = $types[$j]->setOffsetValueType($offsetType, $newValueType); @@ -972,14 +1185,28 @@ public static function intersect(Type ...$types): Type continue 2; } - if ($types[$i] instanceof ConstantArrayType && $types[$j] instanceof ArrayType && !$types[$j] instanceof ConstantArrayType) { + if ($types[$i] instanceof ObjectShapeType && $types[$j] instanceof HasPropertyType) { + $types[$i] = $types[$i]->makePropertyRequired($types[$j]->getPropertyName()); + array_splice($types, $j--, 1); + $typesCount--; + continue; + } + + if ($types[$j] instanceof ObjectShapeType && $types[$i] instanceof HasPropertyType) { + $types[$j] = $types[$j]->makePropertyRequired($types[$i]->getPropertyName()); + array_splice($types, $i--, 1); + $typesCount--; + continue 2; + } + + if ($types[$i] instanceof ConstantArrayType && $types[$j] instanceof ArrayType) { $newArray = ConstantArrayTypeBuilder::createEmpty(); $valueTypes = $types[$i]->getValueTypes(); foreach ($types[$i]->getKeyTypes() as $k => $keyType) { $newArray->setOffsetValueType( self::intersect($keyType, $types[$j]->getIterableKeyType()), self::intersect($valueTypes[$k], $types[$j]->getIterableValueType()), - $types[$i]->isOptionalKey($k), + $types[$i]->isOptionalKey($k) && !$types[$j]->hasOffsetValueType($keyType)->yes(), ); } $types[$i] = $newArray->getArray(); @@ -988,11 +1215,27 @@ public static function intersect(Type ...$types): Type continue 2; } + if ($types[$j] instanceof ConstantArrayType && $types[$i] instanceof ArrayType) { + $newArray = ConstantArrayTypeBuilder::createEmpty(); + $valueTypes = $types[$j]->getValueTypes(); + foreach ($types[$j]->getKeyTypes() as $k => $keyType) { + $newArray->setOffsetValueType( + self::intersect($keyType, $types[$i]->getIterableKeyType()), + self::intersect($valueTypes[$k], $types[$i]->getIterableValueType()), + $types[$j]->isOptionalKey($k) && !$types[$i]->hasOffsetValueType($keyType)->yes(), + ); + } + $types[$j] = $newArray->getArray(); + array_splice($types, $i--, 1); + $typesCount--; + continue 2; + } + if ( ($types[$i] instanceof ArrayType || $types[$i] instanceof IterableType) && ($types[$j] instanceof ArrayType || $types[$j] instanceof IterableType) ) { - $keyType = self::intersect($types[$i]->getKeyType(), $types[$j]->getKeyType()); + $keyType = self::intersect($types[$i]->getIterableKeyType(), $types[$j]->getKeyType()); $itemType = self::intersect($types[$i]->getItemType(), $types[$j]->getItemType()); if ($types[$i] instanceof IterableType && $types[$j] instanceof IterableType) { $types[$j] = new IterableType($keyType, $itemType); @@ -1012,6 +1255,20 @@ public static function intersect(Type ...$types): Type continue; } + if ( + $types[$i] instanceof ArrayType + && get_class($types[$i]) === ArrayType::class + && $types[$j] instanceof AccessoryArrayListType + && !$types[$j]->getIterableKeyType()->isSuperTypeOf($types[$i]->getIterableKeyType())->yes() + ) { + $keyType = self::intersect($types[$i]->getIterableKeyType(), $types[$j]->getIterableKeyType()); + if ($keyType instanceof NeverType) { + return $keyType; + } + $types[$i] = new ArrayType($keyType, $types[$i]->getItemType()); + continue; + } + continue; } @@ -1034,4 +1291,9 @@ public static function intersect(Type ...$types): Type return new IntersectionType($types); } + public static function removeFalsey(Type $type): Type + { + return self::remove($type, StaticTypeFactory::falsey()); + } + } diff --git a/src/Type/TypeUtils.php b/src/Type/TypeUtils.php index 7dfdd19fc0..c16822e0a4 100644 --- a/src/Type/TypeUtils.php +++ b/src/Type/TypeUtils.php @@ -8,7 +8,9 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Enum\EnumCaseObjectType; +use PHPStan\Type\Generic\TemplateBenevolentUnionType; use PHPStan\Type\Generic\TemplateType; +use PHPStan\Type\Generic\TemplateUnionType; use function array_merge; use function array_unique; use function array_values; @@ -19,6 +21,8 @@ class TypeUtils /** * @return ArrayType[] + * + * @deprecated Use PHPStan\Type\Type::getArrays() instead and handle optional ConstantArrayType keys if necessary. */ public static function getArrays(Type $type): array { @@ -63,6 +67,8 @@ public static function getArrays(Type $type): array /** * @return ConstantArrayType[] + * + * @deprecated Use PHPStan\Type\Type::getConstantArrays() instead and handle optional keys if necessary. */ public static function getConstantArrays(Type $type): array { @@ -89,6 +95,8 @@ public static function getConstantArrays(Type $type): array /** * @return ConstantStringType[] + * + * @deprecated Use PHPStan\Type\Type::getConstantStrings() instead */ public static function getConstantStrings(Type $type): array { @@ -104,6 +112,7 @@ public static function getConstantIntegers(Type $type): array } /** + * @deprecated Use Type::isConstantValue() or Type::generalize() * @return ConstantType[] */ public static function getConstantTypes(Type $type): array @@ -112,6 +121,7 @@ public static function getConstantTypes(Type $type): array } /** + * @deprecated Use Type::isConstantValue() or Type::generalize() * @return ConstantType[] */ public static function getAnyConstantTypes(Type $type): array @@ -121,6 +131,8 @@ public static function getAnyConstantTypes(Type $type): array /** * @return ArrayType[] + * + * @deprecated Use PHPStan\Type\Type::getArrays() instead. */ public static function getAnyArrays(Type $type): array { @@ -136,7 +148,9 @@ public static function generalizeType(Type $type, GeneralizePrecision $precision } /** - * @return string[] + * @return list + * + * @deprecated Use Type::getObjectClassNames() instead. */ public static function getDirectClassNames(Type $type): array { @@ -167,6 +181,7 @@ public static function getIntegerRanges(Type $type): array } /** + * @deprecated Use Type::isConstantScalarValue() or Type::getConstantScalarTypes() or Type::getConstantScalarValues() * @return ConstantScalarType[] */ public static function getConstantScalars(Type $type): array @@ -175,6 +190,7 @@ public static function getConstantScalars(Type $type): array } /** + * @deprecated Use Type::getEnumCases() * @return EnumCaseObjectType[] */ public static function getEnumCaseObjects(Type $type): array @@ -185,6 +201,8 @@ public static function getEnumCaseObjects(Type $type): array /** * @internal * @return ConstantArrayType[] + * + * @deprecated Use PHPStan\Type\Type::getConstantArrays(). */ public static function getOldConstantArrays(Type $type): array { @@ -208,7 +226,9 @@ private static function map( if ($type instanceof UnionType) { $matchingTypes = []; foreach ($type->getTypes() as $innerType) { - if (!$innerType instanceof $typeClass) { + $matchingInner = self::map($typeClass, $innerType, $inspectIntersections, $stopOnUnmatched); + + if ($matchingInner === []) { if ($stopOnUnmatched) { return []; } @@ -216,7 +236,9 @@ private static function map( continue; } - $matchingTypes[] = $innerType; + foreach ($matchingInner as $innerMapped) { + $matchingTypes[] = $innerMapped; + } } return $matchingTypes; @@ -255,6 +277,28 @@ public static function toBenevolentUnion(Type $type): Type return $type; } + /** + * @return ($type is UnionType ? UnionType : Type) + */ + public static function toStrictUnion(Type $type): Type + { + if ($type instanceof TemplateBenevolentUnionType) { + return new TemplateUnionType( + $type->getScope(), + $type->getStrategy(), + $type->getVariance(), + $type->getName(), + static::toStrictUnion($type->getBound()), + ); + } + + if ($type instanceof BenevolentUnionType) { + return new UnionType($type->getTypes()); + } + + return $type; + } + /** * @return Type[] */ @@ -365,13 +409,11 @@ public static function containsTemplateType(Type $type): bool public static function resolveLateResolvableTypes(Type $type, bool $resolveUnresolvableTypes = true): Type { return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($resolveUnresolvableTypes): Type { - $type = $traverse($type); - - if ($type instanceof LateResolvableType && ($resolveUnresolvableTypes || $type->isResolvable())) { + while ($type instanceof LateResolvableType && ($resolveUnresolvableTypes || $type->isResolvable())) { $type = $type->resolve(); } - return $type; + return $traverse($type); }); } diff --git a/src/Type/TypehintHelper.php b/src/Type/TypehintHelper.php index a194e82657..09e33f5c9b 100644 --- a/src/Type/TypehintHelper.php +++ b/src/Type/TypehintHelper.php @@ -2,6 +2,7 @@ namespace PHPStan\Type; +use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantBooleanType; @@ -13,6 +14,7 @@ use function array_map; use function count; use function get_class; +use function is_string; use function sprintf; use function str_ends_with; use function strtolower; @@ -20,7 +22,7 @@ class TypehintHelper { - private static function getTypeObjectFromTypehint(string $typeString, ?string $selfClass): Type + private static function getTypeObjectFromTypehint(string $typeString, ClassReflection|string|null $selfClass): Type { switch (strtolower($typeString)) { case 'int': @@ -48,27 +50,43 @@ private static function getTypeObjectFromTypehint(string $typeString, ?string $s case 'mixed': return new MixedType(true); case 'self': + if ($selfClass instanceof ClassReflection) { + $selfClass = $selfClass->getName(); + } return $selfClass !== null ? new ObjectType($selfClass) : new ErrorType(); case 'parent': $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if ($selfClass !== null && $reflectionProvider->hasClass($selfClass)) { - $classReflection = $reflectionProvider->getClass($selfClass); - if ($classReflection->getParentClass() !== null) { - return new ObjectType($classReflection->getParentClass()->getName()); + if (is_string($selfClass)) { + if ($reflectionProvider->hasClass($selfClass)) { + $selfClass = $reflectionProvider->getClass($selfClass); + } else { + $selfClass = null; + } + } + if ($selfClass !== null) { + if ($selfClass->getParentClass() !== null) { + return new ObjectType($selfClass->getParentClass()->getName()); } } return new NonexistentParentClassType(); case 'static': $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if ($selfClass !== null && $reflectionProvider->hasClass($selfClass)) { - return new StaticType($reflectionProvider->getClass($selfClass)); + if (is_string($selfClass)) { + if ($reflectionProvider->hasClass($selfClass)) { + $selfClass = $reflectionProvider->getClass($selfClass); + } else { + $selfClass = null; + } + } + if ($selfClass !== null) { + return new StaticType($selfClass); } return new ErrorType(); case 'null': return new NullType(); case 'never': - return new NeverType(true); + return new NonAcceptingNeverType(); default: return new ObjectType($typeString); } @@ -78,7 +96,7 @@ private static function getTypeObjectFromTypehint(string $typeString, ?string $s public static function decideTypeFromReflection( ?ReflectionType $reflectionType, ?Type $phpDocType = null, - ?string $selfClass = null, + ClassReflection|string|null $selfClass = null, bool $isVariadic = false, ): Type { @@ -99,7 +117,7 @@ public static function decideTypeFromReflection( $types = []; foreach ($reflectionType->getTypes() as $innerReflectionType) { $innerType = self::decideTypeFromReflection($innerReflectionType, null, $selfClass, false); - if (!$innerType instanceof ObjectType) { + if (!$innerType->isObject()->yes()) { return new NeverType(); } @@ -114,22 +132,18 @@ public static function decideTypeFromReflection( } $reflectionTypeString = $reflectionType->getName(); - if (str_ends_with(strtolower($reflectionTypeString), '\\object')) { + $loweredReflectionTypeString = strtolower($reflectionTypeString); + if (str_ends_with($loweredReflectionTypeString, '\\object')) { $reflectionTypeString = 'object'; - } - if (str_ends_with(strtolower($reflectionTypeString), '\\mixed')) { + } elseif (str_ends_with($loweredReflectionTypeString, '\\mixed')) { $reflectionTypeString = 'mixed'; - } - if (str_ends_with(strtolower($reflectionTypeString), '\\true')) { + } elseif (str_ends_with($loweredReflectionTypeString, '\\true')) { $reflectionTypeString = 'true'; - } - if (str_ends_with(strtolower($reflectionTypeString), '\\false')) { + } elseif (str_ends_with($loweredReflectionTypeString, '\\false')) { $reflectionTypeString = 'false'; - } - if (str_ends_with(strtolower($reflectionTypeString), '\\null')) { + } elseif (str_ends_with($loweredReflectionTypeString, '\\null')) { $reflectionTypeString = 'null'; - } - if (str_ends_with(strtolower($reflectionTypeString), '\\never')) { + } elseif (str_ends_with($loweredReflectionTypeString, '\\never')) { $reflectionTypeString = 'never'; } @@ -148,6 +162,10 @@ public static function decideType( ?Type $phpDocType = null, ): Type { + if ($type instanceof BenevolentUnionType) { + return $type; + } + if ($phpDocType !== null && !$phpDocType instanceof ErrorType) { if ($phpDocType instanceof NeverType && $phpDocType->isExplicit()) { return $phpDocType; @@ -155,7 +173,7 @@ public static function decideType( if ( $type instanceof MixedType && !$type->isExplicitMixed() - && $phpDocType instanceof VoidType + && $phpDocType->isVoid()->yes() ) { return $phpDocType; } @@ -166,7 +184,7 @@ public static function decideType( foreach ($phpDocType->getTypes() as $innerType) { if ($innerType instanceof ArrayType) { $innerTypes[] = new IterableType( - $innerType->getKeyType(), + $innerType->getIterableKeyType(), $innerType->getItemType(), ); } else { diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index e27aa87217..791115abb2 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -5,10 +5,13 @@ use DateTime; use DateTimeImmutable; use DateTimeInterface; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ConstantReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\Type\UnionTypeUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnionTypeUnresolvedPropertyPrototypeReflection; @@ -16,7 +19,6 @@ use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateType; @@ -24,11 +26,18 @@ use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Generic\TemplateUnionType; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; +use function array_diff_assoc; +use function array_fill_keys; use function array_map; +use function array_merge; +use function array_slice; +use function array_unique; +use function array_values; use function count; use function implode; +use function md5; use function sprintf; -use function strpos; +use function str_contains; /** @api */ class UnionType implements CompoundType @@ -38,11 +47,14 @@ class UnionType implements CompoundType private bool $sortedTypes = false; + /** @var array */ + private array $cachedDescriptions = []; + /** * @api * @param Type[] $types */ - public function __construct(private array $types) + public function __construct(private array $types, private bool $normalized = false) { $throwException = static function () use ($types): void { throw new ShouldNotHappenException(sprintf( @@ -74,10 +86,15 @@ public function getTypes(): array return $this->types; } + public function isNormalized(): bool + { + return $this->normalized; + } + /** * @return Type[] */ - private function getSortedTypes(): array + protected function getSortedTypes(): array { if ($this->sortedTypes) { return $this->types; @@ -94,10 +111,62 @@ private function getSortedTypes(): array */ public function getReferencedClasses(): array { - return UnionTypeHelper::getReferencedClasses($this->getTypes()); + $classes = []; + foreach ($this->types as $type) { + foreach ($type->getReferencedClasses() as $className) { + $classes[] = $className; + } + } + + return $classes; + } + + public function getObjectClassNames(): array + { + return array_values(array_unique($this->pickFromTypes( + static fn (Type $type) => $type->getObjectClassNames(), + static fn (Type $type) => $type->isObject()->yes(), + ))); + } + + public function getObjectClassReflections(): array + { + return $this->pickFromTypes( + static fn (Type $type) => $type->getObjectClassReflections(), + static fn (Type $type) => $type->isObject()->yes(), + ); + } + + public function getArrays(): array + { + return $this->pickFromTypes( + static fn (Type $type) => $type->getArrays(), + static fn (Type $type) => $type->isArray()->yes(), + ); + } + + public function getConstantArrays(): array + { + return $this->pickFromTypes( + static fn (Type $type) => $type->getConstantArrays(), + static fn (Type $type) => $type->isArray()->yes(), + ); + } + + public function getConstantStrings(): array + { + return $this->pickFromTypes( + static fn (Type $type) => $type->getConstantStrings(), + static fn (Type $type) => $type->isString()->yes(), + ); } public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ( $type->equals(new ObjectType(DateTimeInterface::class)) @@ -106,20 +175,30 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic $strictTypes, )->yes() ) { - return TrinaryLogic::createYes(); + return AcceptsResult::createYes(); } if ($type instanceof CompoundType && !$type instanceof CallableType && !$type instanceof TemplateType && !$type instanceof IntersectionType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } - $result = TrinaryLogic::createNo()->lazyOr($this->getTypes(), static fn (Type $innerType) => $innerType->accepts($type, $strictTypes)); + $result = AcceptsResult::createNo(); + foreach ($this->getSortedTypes() as $i => $innerType) { + $result = $result->or($innerType->acceptsWithReason($type, $strictTypes)->decorateReasons(static fn (string $reason) => sprintf('Type #%d from the union: %s', $i + 1, $reason))); + } if ($result->yes()) { return $result; } if ($type instanceof TemplateUnionType) { - return $result->or($type->isAcceptedBy($this, $strictTypes)); + return $result->or($type->isAcceptedWithReasonBy($this, $strictTypes)); + } + + if ($type->isEnum()->yes() && !$this->isEnum()->no()) { + $enumCasesUnion = TypeCombinator::union(...$type->getEnumCases()); + if (!$type->equals($enumCasesUnion)) { + return $this->acceptsWithReason($enumCasesUnion, $strictTypes); + } } return $result; @@ -157,7 +236,12 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - return TrinaryLogic::lazyExtremeIdentity($this->getTypes(), static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes)); + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return AcceptsResult::extremeIdentity(...array_map(static fn (Type $innerType) => $acceptingType->acceptsWithReason($innerType, $strictTypes), $this->types)); } public function equals(Type $type): bool @@ -193,6 +277,9 @@ public function equals(Type $type): bool public function describe(VerbosityLevel $level): string { + if (isset($this->cachedDescriptions[$level->getLevelValue()])) { + return $this->cachedDescriptions[$level->getLevelValue()]; + } $joinTypes = static function (array $types) use ($level): string { $typeNames = []; foreach ($types as $i => $type) { @@ -212,7 +299,7 @@ public function describe(VerbosityLevel $level): string } } elseif ($type instanceof IntersectionType) { $intersectionDescription = $type->describe($level); - if (strpos($intersectionDescription, '&') !== false) { + if (str_contains($intersectionDescription, '&')) { $typeNames[] = sprintf('(%s)', $type->describe($level)); } else { $typeNames[] = $intersectionDescription; @@ -222,15 +309,35 @@ public function describe(VerbosityLevel $level): string } } + if ($level->isPrecise()) { + $duplicates = array_diff_assoc($typeNames, array_unique($typeNames)); + if (count($duplicates) > 0) { + $indexByDuplicate = array_fill_keys($duplicates, 0); + foreach ($typeNames as $key => $typeName) { + if (!isset($indexByDuplicate[$typeName])) { + continue; + } + + $typeNames[$key] = $typeName . '#' . ++$indexByDuplicate[$typeName]; + } + } + } else { + $typeNames = array_unique($typeNames); + } + + if (count($typeNames) > 1024) { + return implode('|', array_slice($typeNames, 0, 1024)) . "|\u{2026}"; + } + return implode('|', $typeNames); }; - return $level->handle( + return $this->cachedDescriptions[$level->getLevelValue()] = $level->handle( function () use ($joinTypes): string { $types = TypeCombinator::union(...array_map(static function (Type $type): Type { if ( - $type instanceof ConstantType - && !$type instanceof ConstantBooleanType + $type->isConstantValue()->yes() + && $type->isTrue()->or($type->isFalse())->no() ) { return $type->generalize(GeneralizePrecision::lessSpecific()); } @@ -303,6 +410,21 @@ private function getInternal( return $object; } + public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getTemplateType($ancestorClassName, $templateTypeName)); + } + + public function isObject(): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isObject()); + } + + public function isEnum(): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isEnum()); + } + public function canAccessProperties(): TrinaryLogic { return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canAccessProperties()); @@ -351,7 +473,7 @@ public function hasMethod(string $methodName): TrinaryLogic return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName)); } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection { return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod(); } @@ -410,26 +532,61 @@ public function isIterableAtLeastOnce(): TrinaryLogic return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce()); } + public function getArraySize(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getArraySize()); + } + public function getIterableKeyType(): Type { return $this->unionTypes(static fn (Type $type): Type => $type->getIterableKeyType()); } + public function getFirstIterableKeyType(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getFirstIterableKeyType()); + } + + public function getLastIterableKeyType(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getLastIterableKeyType()); + } + public function getIterableValueType(): Type { return $this->unionTypes(static fn (Type $type): Type => $type->getIterableValueType()); } + public function getFirstIterableValueType(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getFirstIterableValueType()); + } + + public function getLastIterableValueType(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getLastIterableValueType()); + } + public function isArray(): TrinaryLogic { return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isArray()); } + public function isConstantArray(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantArray()); + } + public function isOversizedArray(): TrinaryLogic { return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isOversizedArray()); } + public function isList(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isList()); + } + public function isString(): TrinaryLogic { return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isString()); @@ -455,6 +612,36 @@ public function isLiteralString(): TrinaryLogic return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString()); } + public function isClassStringType(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isClassStringType()); + } + + public function getClassStringObjectType(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getClassStringObjectType()); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getObjectTypeOrClassStringObjectType()); + } + + public function isVoid(): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isVoid()); + } + + public function isScalar(): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isScalar()); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function isOffsetAccessible(): TrinaryLogic { return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible()); @@ -489,30 +676,91 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this->unionTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType)); + } + public function unsetOffset(Type $offsetType): Type { return $this->unionTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType)); } + public function getKeysArray(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getKeysArray()); + } + + public function getValuesArray(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getValuesArray()); + } + + public function fillKeysArray(Type $valueType): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType)); + } + + public function flipArray(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->flipArray()); + } + + public function intersectKeyArray(Type $otherArraysType): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType)); + } + + public function popArray(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->popArray()); + } + + public function searchArray(Type $needleType): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->searchArray($needleType)); + } + + public function shiftArray(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->shiftArray()); + } + + public function shuffleArray(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->shuffleArray()); + } + + public function getEnumCases(): array + { + return $this->pickFromTypes( + static fn (Type $type) => $type->getEnumCases(), + static fn (Type $type) => $type->isObject()->yes(), + ); + } + public function isCallable(): TrinaryLogic { return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCallable()); } - /** - * @return ParametersAcceptor[] - */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { + $acceptors = []; + foreach ($this->types as $type) { if ($type->isCallable()->no()) { continue; } - return $type->getCallableParametersAcceptors($scope); + $acceptors = array_merge($acceptors, $type->getCallableParametersAcceptors($scope)); + } + + if (count($acceptors) === 0) { + throw new ShouldNotHappenException(); } - throw new ShouldNotHappenException(); + return $acceptors; } public function isCloneable(): TrinaryLogic @@ -530,6 +778,56 @@ public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType)); } + public function isNull(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNull()); + } + + public function isConstantValue(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue()); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue()); + } + + public function getConstantScalarTypes(): array + { + return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarTypes()); + } + + public function getConstantScalarValues(): array + { + return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarValues()); + } + + public function isTrue(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isTrue()); + } + + public function isFalse(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isFalse()); + } + + public function isBoolean(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isBoolean()); + } + + public function isFloat(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isFloat()); + } + + public function isInteger(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isInteger()); + } + public function getSmallerType(): Type { return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerType()); @@ -603,6 +901,11 @@ public function toArray(): Type return $type; } + public function toArrayKey(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->toArrayKey()); + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { $types = TemplateTypeMap::createEmpty(); @@ -627,10 +930,8 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap $myTypes = $this->types; } - $myTemplateTypes = []; foreach ($myTypes as $type) { if ($type instanceof TemplateType || ($type instanceof GenericClassStringType && $type->getGenericType() instanceof TemplateType)) { - $myTemplateTypes[] = $type; continue; } $types = $types->union($type->inferTemplateTypes($receivedType)); @@ -691,17 +992,66 @@ public function traverse(callable $cb): Type return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + $types = []; + $changed = false; + + if (!$right instanceof self) { + return $this; + } + + if (count($this->getTypes()) !== count($right->getTypes())) { + return $this; + } + + foreach ($this->getSortedTypes() as $i => $leftType) { + $rightType = $right->getSortedTypes()[$i]; + $newType = $cb($leftType, $rightType); + if ($leftType !== $newType) { + $changed = true; + } + $types[] = $newType; + } + + if ($changed) { + return TypeCombinator::union(...$types); + } + + return $this; + } + public function tryRemove(Type $typeToRemove): ?Type { return $this->unionTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove)); } + public function exponentiate(Type $exponent): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->exponentiate($exponent)); + } + + public function getFiniteTypes(): array + { + $types = $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getFiniteTypes()); + $uniquedTypes = []; + foreach ($types as $type) { + $uniquedTypes[md5($type->describe(VerbosityLevel::cache()))] = $type; + } + + if (count($uniquedTypes) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return []; + } + + return array_values($uniquedTypes); + } + /** * @param mixed[] $properties */ public static function __set_state(array $properties): Type { - return new self($properties['types']); + return new self($properties['types'], $properties['normalized']); } /** @@ -728,4 +1078,69 @@ protected function unionTypes(callable $getType): Type return TypeCombinator::union(...array_map($getType, $this->types)); } + /** + * @template T of Type + * @param callable(Type $type): list $getTypes + * @return list + * + * @deprecated Use pickFromTypes() instead. + */ + protected function pickTypes(callable $getTypes): array + { + return $this->pickFromTypes($getTypes, static fn () => false); + } + + /** + * @template T + * @param callable(Type $type): list $getValues + * @param callable(Type $type): bool $criteria + * @return list + */ + protected function pickFromTypes( + callable $getValues, + callable $criteria, + ): array + { + $values = []; + foreach ($this->types as $type) { + $innerValues = $getValues($type); + if ($innerValues === []) { + return []; + } + + foreach ($innerValues as $innerType) { + $values[] = $innerType; + } + } + + return $values; + } + + public function toPhpDocNode(): TypeNode + { + return new UnionTypeNode(array_map(static fn (Type $type) => $type->toPhpDocNode(), $this->getSortedTypes())); + } + + /** + * @template T + * @param callable(Type $type): list $getValues + * @return list + */ + private function notBenevolentPickFromTypes(callable $getValues): array + { + $values = []; + foreach ($this->types as $type) { + $innerValues = $getValues($type); + if ($innerValues === []) { + return []; + } + + foreach ($innerValues as $innerType) { + $values[] = $innerType; + } + } + + return $values; + } + } diff --git a/src/Type/UnionTypeHelper.php b/src/Type/UnionTypeHelper.php index 4e89b1121b..bbb097accf 100644 --- a/src/Type/UnionTypeHelper.php +++ b/src/Type/UnionTypeHelper.php @@ -3,12 +3,10 @@ namespace PHPStan\Type; use PHPStan\Type\Accessory\AccessoryType; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; -use function array_merge; use function count; use function strcasecmp; use function usort; @@ -17,20 +15,6 @@ class UnionTypeHelper { - /** - * @param Type[] $types - * @return string[] - */ - public static function getReferencedClasses(array $types): array - { - $subTypeClasses = []; - foreach ($types as $type) { - $subTypeClasses[] = $type->getReferencedClasses(); - } - - return array_merge(...$subTypeClasses); - } - /** * @param Type[] $types * @return Type[] @@ -111,20 +95,24 @@ public static function sortTypes(array $types): array return self::compareStrings($a->getValue(), $b->getValue()); } - if ($a instanceof ConstantArrayType && $b instanceof ConstantArrayType) { - if ($a->isEmpty()) { - if ($b->isEmpty()) { + if ($a->isConstantArray()->yes() && $b->isConstantArray()->yes()) { + if ($a->isIterableAtLeastOnce()->no()) { + if ($b->isIterableAtLeastOnce()->no()) { return 0; } return -1; - } elseif ($b->isEmpty()) { + } elseif ($b->isIterableAtLeastOnce()->no()) { return 1; } return self::compareStrings($a->describe(VerbosityLevel::value()), $b->describe(VerbosityLevel::value())); } + if ($a->isCallable()->yes() && $b->isCallable()->yes()) { + return self::compareStrings($a->describe(VerbosityLevel::value()), $b->describe(VerbosityLevel::value())); + } + return self::compareStrings($a->describe(VerbosityLevel::typeOnly()), $b->describe(VerbosityLevel::typeOnly())); }); return $types; diff --git a/src/Type/UsefulTypeAliasResolver.php b/src/Type/UsefulTypeAliasResolver.php index 6cfb76d373..191bad9794 100644 --- a/src/Type/UsefulTypeAliasResolver.php +++ b/src/Type/UsefulTypeAliasResolver.php @@ -69,7 +69,7 @@ private function resolveLocalTypeAlias(string $aliasName, NameScope $nameScope): return null; } - $className = $nameScope->getClassName(); + $className = $nameScope->getClassNameForTypeAlias(); if ($className === null) { return null; } diff --git a/src/Type/ValueOfType.php b/src/Type/ValueOfType.php index c15e6d6d70..9df5411c47 100644 --- a/src/Type/ValueOfType.php +++ b/src/Type/ValueOfType.php @@ -2,6 +2,9 @@ namespace PHPStan\Type; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Traits\LateResolvableTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; @@ -46,6 +49,20 @@ public function isResolvable(): bool protected function getResult(): Type { + if ($this->type->isEnum()->yes()) { + $valueTypes = []; + foreach ($this->type->getEnumCases() as $enumCase) { + $valueType = $enumCase->getBackingValueType(); + if ($valueType === null) { + continue; + } + + $valueTypes[] = $valueType; + } + + return TypeCombinator::union(...$valueTypes); + } + return $this->type->getIterableValueType(); } @@ -60,7 +77,27 @@ public function traverse(callable $cb): Type return $this; } - return new ValueOfType($type); + return new self($type); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } + + $type = $cb($this->type, $right->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type); + } + + public function toPhpDocNode(): TypeNode + { + return new GenericTypeNode(new IdentifierTypeNode('value-of'), [$this->type->toPhpDocNode()]); } /** diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index 9fe9d3d038..c80bcbce99 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -2,7 +2,7 @@ namespace PHPStan\Type; -use PHPStan\ShouldNotHappenException; +use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; @@ -22,16 +22,28 @@ class VerbosityLevel /** @var self[] */ private static array $registry; + /** + * @param self::* $value + */ private function __construct(private int $value) { } + /** + * @param self::* $value + */ private static function create(int $value): self { self::$registry[$value] ??= new self($value); return self::$registry[$value]; } + /** @return self::* */ + public function getLevelValue(): int + { + return $this->value; + } + /** @api */ public static function typeOnly(): self { @@ -66,6 +78,11 @@ public function isValue(): bool return $this->value === self::VALUE; } + public function isPrecise(): bool + { + return $this->value === self::PRECISE; + } + /** @api */ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acceptedType = null): self { @@ -74,7 +91,7 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc $moreVerbose = true; return $type; } - if ($type instanceof ConstantType && !$type instanceof NullType) { + if ($type->isConstantValue()->yes() && $type->isNull()->no()) { $moreVerbose = true; return $type; } @@ -85,6 +102,7 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc || $type instanceof AccessoryLiteralStringType || $type instanceof AccessoryNumericStringType || $type instanceof NonEmptyArrayType + || $type instanceof AccessoryArrayListType ) { $moreVerbose = true; return $type; @@ -172,19 +190,15 @@ public function handle( return $valueCallback(); } - if ($this->value === self::CACHE) { - if ($cacheCallback !== null) { - return $cacheCallback(); - } - - if ($preciseCallback !== null) { - return $preciseCallback(); - } + if ($cacheCallback !== null) { + return $cacheCallback(); + } - return $valueCallback(); + if ($preciseCallback !== null) { + return $preciseCallback(); } - throw new ShouldNotHappenException(); + return $valueCallback(); } } diff --git a/src/Type/VoidType.php b/src/Type/VoidType.php index 5f3e8f4cea..07f755ce16 100644 --- a/src/Type/VoidType.php +++ b/src/Type/VoidType.php @@ -2,8 +2,12 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; use PHPStan\Type\Traits\FalseyBooleanTypeTrait; +use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonCallableTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; @@ -17,6 +21,7 @@ class VoidType implements Type { + use NonArrayTypeTrait; use NonCallableTypeTrait; use NonIterableTypeTrait; use NonObjectTypeTrait; @@ -40,13 +45,28 @@ public function getReferencedClasses(): array return []; } + public function getObjectClassNames(): array + { + return []; + } + + public function getObjectClassReflections(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); + return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return TrinaryLogic::createFromBoolean($type instanceof self); + return new AcceptsResult($type->isVoid()->or($type->isNull()), []); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -97,12 +117,57 @@ public function toArray(): Type return new ErrorType(); } - public function isArray(): TrinaryLogic + public function toArrayKey(): Type + { + return new ErrorType(); + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function isOversizedArray(): TrinaryLogic + public function isInteger(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -132,11 +197,61 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function traverse(callable $cb): Type { return $this; } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function exponentiate(Type $exponent): Type + { + return new ErrorType(); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('void'); + } + /** * @param mixed[] $properties */ diff --git a/stubs/ImagickPixel.stub b/stubs/ImagickPixel.stub new file mode 100644 index 0000000000..49ac0c9161 --- /dev/null +++ b/stubs/ImagickPixel.stub @@ -0,0 +1,9 @@ +, g: int<0, 255>, b: int<0, 255>, a: int<0, 1>} : ($normalized is 1 ? array{r: float, g: float, b: float, a: float} : ($normalized is 2 ? array{r: int<0, 255>, g: int<0, 255>, b: int<0, 255>, a: int<0, 255>} : array{}))) + */ + public function getColor(int $normalized = 0): array; +} diff --git a/stubs/PDOStatement.stub b/stubs/PDOStatement.stub index 79637d8370..46d0be7c5d 100644 --- a/stubs/PDOStatement.stub +++ b/stubs/PDOStatement.stub @@ -7,5 +7,16 @@ */ class PDOStatement implements Traversable, IteratorAggregate { + /** + * @template T of object + * @param class-string $class + * @param array $ctorArgs + * @return false|T + */ + public function fetchObject($class = \stdClass::class, array $ctorArgs = array()) {} + /** + * @return array{name: string, table?: string, native_type?: string, len: int, flags: array, precision: int<0, max>, pdo_type: PDO::PARAM_* }|false + */ + public function getColumnMeta(int $column) {} } diff --git a/stubs/ReflectionClass.stub b/stubs/ReflectionClass.stub index e5d2a0908a..f47d5d89a1 100644 --- a/stubs/ReflectionClass.stub +++ b/stubs/ReflectionClass.stub @@ -2,7 +2,6 @@ /** * @template-covariant T of object - * @property-read class-string $name */ class ReflectionClass { @@ -31,7 +30,7 @@ class ReflectionClass public function newInstance(...$args) {} /** - * @param array $args + * @param array $args * * @return T */ diff --git a/stubs/ReflectionEnum.stub b/stubs/ReflectionEnum.stub new file mode 100644 index 0000000000..20396c04fc --- /dev/null +++ b/stubs/ReflectionEnum.stub @@ -0,0 +1,27 @@ + + */ +class ReflectionEnum extends ReflectionClass +{ + + /** + * @return (T is BackedEnum ? ReflectionEnumBackedCase[] : ReflectionEnumUnitCase[]) + */ + public function getCases(): array {} + + /** + * @return (T is BackedEnum ? ReflectionEnumBackedCase : ReflectionEnumUnitCase) + * @throws ReflectionException + */ + public function getCase(string $name): ReflectionEnumUnitCase {} + + /** + * @phpstan-assert-if-true self $this + * @phpstan-assert-if-true !null $this->getBackingType() + */ + public function isBacked(): bool {} + +} diff --git a/stubs/SplObjectStorage.stub b/stubs/SplObjectStorage.stub index 72a75982dd..3f7c44f120 100644 --- a/stubs/SplObjectStorage.stub +++ b/stubs/SplObjectStorage.stub @@ -31,11 +31,6 @@ class SplObjectStorage implements Countable, Iterator, Serializable, ArrayAccess */ public function detach(object $object): void { } - /** - * @param TObject $object - */ - public function detach(object $object): void { } - /** * @param TObject $object */ @@ -47,12 +42,12 @@ class SplObjectStorage implements Countable, Iterator, Serializable, ArrayAccess public function getInfo() { } /** - * @param \SplObjectStorage $storage + * @param \SplObjectStorage<*, *> $storage */ public function removeAll(SplObjectStorage $storage): void { } /** - * @param \SplObjectStorage $storage + * @param \SplObjectStorage<*, *> $storage */ public function removeAllExcept(SplObjectStorage $storage): void { } diff --git a/stubs/arrayFunctions.stub b/stubs/arrayFunctions.stub index 8c438327cd..25c73249ec 100644 --- a/stubs/arrayFunctions.stub +++ b/stubs/arrayFunctions.stub @@ -17,47 +17,53 @@ function array_reduce( ) {} /** - * @template T of mixed + * @template TKey as (int|string) + * @template T + * @template TArray as array * - * @param array $one - * @param callable(T, T): int $two + * @param TArray $array + * @param callable(T,T):int $callback */ -function uasort( - array &$one, - callable $two -): bool {} +function uasort(array &$array, callable $callback): bool +{} /** - * @template T of mixed + * @template T + * @template TArray as array * - * @param array $one - * @param callable(T, T): int $two + * @param TArray $array + * @param callable(T,T):int $callback */ -function usort( - array &$one, - callable $two -): bool {} +function usort(array &$array, callable $callback): bool +{} /** - * @template T of array-key + * @template TKey as (int|string) + * @template T + * @template TArray as array * - * @param array $one - * @param callable(T, T): int $two + * @param TArray $array + * @param callable(TKey,TKey):int $callback */ -function uksort( - array &$one, - callable $two -): bool {} +function uksort(array &$array, callable $callback): bool +{ +} /** * @template T of mixed * * @param array $one * @param array $two - * @param callable(T, T): int<-1, 1> $three + * @param callable(T, T): int $three */ function array_udiff( array $one, array $two, callable $three ): int {} + +/** + * @param array $value + * @return ($value is __always-list ? true : false) + */ +function array_is_list(array $value): bool {} diff --git a/stubs/bleedingEdge/Rule.stub b/stubs/bleedingEdge/Rule.stub new file mode 100644 index 0000000000..0a86ea9d2c --- /dev/null +++ b/stubs/bleedingEdge/Rule.stub @@ -0,0 +1,26 @@ + + */ + public function getNodeType(): string; + + /** + * @phpstan-param TNodeType $node + * @return list + */ + public function processNode(Node $node, Scope $scope): array; + +} diff --git a/stubs/core.stub b/stubs/core.stub index 13b31ffc7b..2cb6f29448 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -67,3 +67,249 @@ function base64_encode(string $string) : string {} * @return ($string is non-empty-string ? non-empty-string : string) */ function bin2hex(string $string): string {} + +/** + * @return ($string is non-empty-string ? non-empty-string : string) + */ +function str_shuffle(string $string): string {} + +/** + * @param array $result + * @param-out array|string> $result + */ +function parse_str(string $string, array &$result): void {} + +/** + * @param array $result + * @param-out array|string> $result + */ +function mb_parse_str(string $string, array &$result): bool {} + +/** @param-out float $percent */ +function similar_text(string $string1, string $string2, float &$percent = null) : int {} + +/** + * @param mixed $output + * @param mixed $result_code + * + * @param-out list $output + * @param-out int $result_code + * + * @return string|false + */ +function exec(string $command, &$output, &$result_code) {} + +/** + * @param mixed $result_code + * @param-out int $result_code + * + * @return string|false + */ +function system(string $command, &$result_code) {} + +/** + * @param mixed $result_code + * @param-out int $result_code + */ +function passthru(string $command, &$result_code): ?bool {} + + +/** + * @template T + * @template TArray as array + * + * @param TArray $array + */ +function shuffle(array &$array): bool +{ +} + +/** + * @template T + * @template TArray as array + * + * @param TArray $array + */ +function sort(array &$array, int $flags = SORT_REGULAR): bool +{ +} + +/** + * @template T + * @template TArray as array + * + * @param TArray $array + */ +function rsort(array &$array, int $flags = SORT_REGULAR): bool +{ +} + +/** + * @param string $string + * @param-out null $string + */ +function sodium_memzero(string &$string): void +{ +} + +/** + * @param resource $stream + * @param mixed $vars + * @param-out string|int|float|null $vars + * + * @return list|int|false + */ +function fscanf($stream, string $format, &...$vars) {} + +/** + * @param mixed $war + * @param mixed $vars + * @param-out string|int|float|null $war + * @param-out string|int|float|null $vars + * + * @return int|array|null + */ +function sscanf(string $string, string $format, &$war, &...$vars) {} + +/** + * @template TFlags as int + * + * @param string $pattern + * @param string $subject + * @param mixed $matches + * @param TFlags $flags + * @param-out ( + * TFlags is 1 + * ? array> + * : (TFlags is 2 + * ? list> + * : (TFlags is 256|257 + * ? array> + * : (TFlags is 258 + * ? list> + * : (TFlags is 512|513 + * ? array> + * : (TFlags is 514 + * ? list> + * : (TFlags is 770 + * ? list> + * : (TFlags is 0 ? array> : array) + * ) + * ) + * ) + * ) + * ) + * ) + * ) $matches + * @return int|false + */ +function preg_match_all($pattern, $subject, &$matches = [], int $flags = 1, int $offset = 0) {} + +/** + * @template TFlags as int-mask<0, 256, 512> + * + * @param string $pattern + * @param string $subject + * @param mixed $matches + * @param TFlags $flags + * @param-out ( + * TFlags is 256 + * ? array + * : (TFlags is 512 + * ? array + * : (TFlags is 768 + * ? array + * : array + * ) + * ) + * ) $matches + * @return 1|0|false + */ +function preg_match($pattern, $subject, &$matches = [], int $flags = 0, int $offset = 0) {} + +/** + * @param string|array $pattern + * @param callable(array):string $callback + * @param string|array $subject + * @param int $count + * @param-out 0|positive-int $count + * @return ($subject is array ? list|null : string|null) + */ +function preg_replace_callback($pattern, $callback, $subject, int $limit = -1, &$count = null, int $flags = 0) {} + +/** + * @param string|array $pattern + * @param string|array $replacement + * @param string|array $subject + * @param int $count + * @param-out 0|positive-int $count + * @return ($subject is array ? list|null : string|null) + */ +function preg_replace($pattern, $replacement, $subject, int $limit = -1, &$count = null) {} + +/** + * @param string|array $pattern + * @param string|array $replacement + * @param string|array $subject + * @param int $count + * @param-out 0|positive-int $count + * @return ($subject is array ? list : string|null) + */ +function preg_filter($pattern, $replacement, $subject, int $limit = -1, &$count = null) {} + +/** + * @param array|string $search + * @param array|string $replace + * @param array|string $subject + * @param-out int $count + * @return list|string + */ +function str_replace($search, $replace, $subject, int &$count = null) {} + +/** + * @param array|string $search + * @param array|string $replace + * @param array|string $subject + * @param-out int $count + * @return list|string + */ +function str_ireplace($search, $replace, $subject, int &$count = null) {} + +/** + * @template TRead of null|array + * @template TWrite of null|array + * @template TExcept of null|array + * @param TRead $read + * @param TWrite $write + * @param TExcept $except + * @return false|0|positive-int + * @param-out (TRead is null ? null : array) $read + * @param-out (TWrite is null ? null : array) $write + * @param-out (TExcept is null ? null : array) $except + */ +function stream_select(?array &$read, ?array &$write, ?array &$except, ?int $seconds, ?int $microseconds = null) {} + +/** + * @param resource $stream + * @param-out 0|1 $would_block + */ +function flock($stream, int $operation, mixed &$would_block = null): bool {} + +/** + * @param-out int $error_code + * @param-out string $error_message + * @return resource|false + */ +function fsockopen(string $hostname, int $port = -1, int &$error_code = null, string &$error_message = null, ?float $timeout = null) {} + +/** + * @param-out string $filename + * @param-out int $line + */ +function headers_sent(string &$filename = null, int &$line = null): bool {} + +/** + * @param-out callable-string $callable_name + * @return ($value is callable ? true : false) + */ +function is_callable(mixed $value, bool $syntax_only = false, string &$callable_name = null): bool {} diff --git a/stubs/date.stub b/stubs/date.stub index c9dc5c1882..e58c7f0682 100644 --- a/stubs/date.stub +++ b/stubs/date.stub @@ -1,19 +1,9 @@ * @implements \Traversable */ diff --git a/stubs/dom.stub b/stubs/dom.stub index cc9475abea..d2a5c575fc 100644 --- a/stubs/dom.stub +++ b/stubs/dom.stub @@ -98,14 +98,6 @@ class DOMCharacterData } -class DOMCharacterData -{ - - /** @var DOMDocument */ - public $ownerDocument; - -} - class DOMDocumentType { diff --git a/stubs/ext-ds.stub b/stubs/ext-ds.stub index bde5162f70..05fdf38f0a 100644 --- a/stubs/ext-ds.stub +++ b/stubs/ext-ds.stub @@ -18,7 +18,7 @@ use UnderflowException; interface Collection extends IteratorAggregate, Countable, JsonSerializable { /** - * @return Collection + * @return static */ public function copy(); @@ -392,11 +392,6 @@ interface Sequence extends Collection, ArrayAccess */ public function apply(callable $callback); - /** - * @return Sequence - */ - public function copy(); - /** * @param TValue ...$values */ @@ -462,6 +457,7 @@ interface Sequence extends Collection, ArrayAccess /** * @return TValue * @throws \UnderflowException + * @phpstan-impure */ public function pop(); @@ -500,6 +496,7 @@ interface Sequence extends Collection, ArrayAccess /** * @return TValue * @throws \UnderflowException + * @phpstan-impure */ public function shift(); @@ -804,6 +801,7 @@ final class Stack implements Collection, ArrayAccess /** * @return TValue * @throws UnderflowException + * @phpstan-impure */ public function pop() { @@ -857,6 +855,7 @@ final class Queue implements Collection, ArrayAccess /** * @return TValue * @throws UnderflowException + * @phpstan-impure */ public function pop() { @@ -901,6 +900,7 @@ final class PriorityQueue implements Collection /** * @return TValue * @throws UnderflowException + * @phpstan-impure */ public function pop() { diff --git a/stubs/ibm_db2.stub b/stubs/ibm_db2.stub new file mode 100644 index 0000000000..1b0e578bfc --- /dev/null +++ b/stubs/ibm_db2.stub @@ -0,0 +1,9 @@ + $flags + * @phpstan-assert-if-true =non-empty-string $json + */ +function json_validate(string $json, int $depth = 512, int $flags = 0): bool +{ +} diff --git a/stubs/mysqli.stub b/stubs/mysqli.stub index 350a645ba3..cc88f4f6e1 100644 --- a/stubs/mysqli.stub +++ b/stubs/mysqli.stub @@ -1,10 +1,43 @@ |numeric-string + */ + public $affected_rows; +} +class mysqli_result +{ /** - * @var int|string + * @var int<0,max>|numeric-string + */ + public $num_rows; + + /** + * @template T of object + * @param class-string $class + * @param array $constructor_args + * @return T|null|false + */ + function fetch_object(string $class = 'stdClass', array $constructor_args = []) {} +} + + +/** + * @template T of object + * + * @param class-string $class + * @param array $constructor_args + * @return T|null|false + */ +function mysqli_fetch_object(mysqli_result $result, string $class = 'stdClass', array $constructor_args = []) {} + +class mysqli_stmt +{ + /** + * @var int<-1,max>|numeric-string */ public $affected_rows; @@ -34,7 +67,7 @@ class mysqli_stmt public $insert_id; /** - * @var 0|positive-int + * @var int<0,max>|numeric-string */ public $num_rows; diff --git a/stubs/socket_select.stub b/stubs/socket_select.stub new file mode 100644 index 0000000000..25a6cb1f1a --- /dev/null +++ b/stubs/socket_select.stub @@ -0,0 +1,12 @@ +|null &$read + * @param array|null &$write + * @param array|null &$except + * @param-out ($read is not null ? array : null) $read + * @param-out ($write is not null ? array : null) $write + * @param-out ($except is not null ? array : null) $except + * @return int|false + */ +function socket_select(?array &$read, ?array &$write, ?array &$except, ?int $seconds, int $microseconds = 0) {} diff --git a/stubs/socket_select_php8.stub b/stubs/socket_select_php8.stub new file mode 100644 index 0000000000..f2a1704b42 --- /dev/null +++ b/stubs/socket_select_php8.stub @@ -0,0 +1,11 @@ +|null &$read + * @param array|null &$write + * @param array|null &$except + * @param-out ($read is not null ? array : null) $read + * @param-out ($write is not null ? array : null) $write + * @param-out ($except is not null ? array : null) $except + */ +function socket_select(?array &$read, ?array &$write, ?array &$except, ?int $seconds, int $microseconds = 0): int|false {} diff --git a/stubs/spl.stub b/stubs/spl.stub index 43a2a06d97..daf46ae1a7 100644 --- a/stubs/spl.stub +++ b/stubs/spl.stub @@ -46,6 +46,12 @@ class SplDoublyLinkedList implements \Iterator, \ArrayAccess { * @return TValue */ public function bottom () {} + + /** + * @param int $offset + * @return TValue + */ + public function offsetGet ($offset) {} } /** diff --git a/stubs/typeCheckingFunctions.stub b/stubs/typeCheckingFunctions.stub new file mode 100644 index 0000000000..82d9c83cde --- /dev/null +++ b/stubs/typeCheckingFunctions.stub @@ -0,0 +1,130 @@ +|\Countable ? true : false) + */ +function is_countable(mixed $value): bool +{ + +} + +/** + * @return ($value is object ? true : false) + */ +function is_object(mixed $value): bool +{ + +} + +/** + * @return ($value is scalar ? true : false) + */ +function is_scalar(mixed $value): bool +{ + +} + +/** + * @return ($value is int ? true : false) + */ +function is_int(mixed $value): bool +{ + +} + +/** + * @return ($value is int ? true : false) + */ +function is_integer(mixed $value): bool +{ + +} + +/** + * @return ($value is int ? true : false) + */ +function is_long(mixed $value): bool +{ + +} + +/** + * @phpstan-assert-if-true resource $value + * @return bool + */ +function is_resource(mixed $value): bool +{ + +} + +/** + * @return ($value is array ? true : false) + */ +function is_array(mixed $value): bool +{ + +} + +/** + * @return ($value is iterable ? true : false) + */ +function is_iterable(mixed $value): bool +{ + +} diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 91d78697b8..380dc7d7dc 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -5,7 +5,6 @@ use Bug4288\MyClass; use Bug4713\Service; use ExtendingKnownClassWithCheck\Foo; -use PHPStan\File\FileHelper; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\ParametersAcceptorSelector; @@ -63,7 +62,7 @@ public function testAnonymousClassWithInheritedConstructor(): void public function testNestedFunctionCallsDoNotCauseExcessiveFunctionNesting(): void { if (extension_loaded('xdebug')) { - $this->markTestSkipped('This test takes too long with XDebug enabled.'); + $this->markTestSkipped('This test takes too long with Xdebug enabled.'); } $errors = $this->runAnalyse(__DIR__ . '/data/nested-functions.php'); $this->assertNoErrors($errors); @@ -233,7 +232,9 @@ public function testBug6936(): void public function testBug3405(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-3405.php'); - $this->assertNoErrors($errors); + $this->assertCount(1, $errors); + $this->assertSame('Magic constant __TRAIT__ is always empty outside a trait.', $errors[0]->getMessage()); + $this->assertSame(16, $errors[0]->getLine()); } public function testBug3415(): void @@ -347,6 +348,13 @@ public function testBug1843(): void $this->assertNoErrors($errors); } + public function testBug9711(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-9711.php'); + $this->assertCount(1, $errors); + $this->assertSame('Function in_array invoked with 1 parameter, 2-3 required.', $errors[0]->getMessage()); + } + public function testBug4713(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-4713.php'); @@ -545,7 +553,9 @@ public function testBug6375(): void public function testBug6501(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-6501.php'); - $this->assertNoErrors($errors); + $this->assertCount(1, $errors); + $this->assertSame('PHPDoc tag @var with type R of Exception|stdClass is not subtype of native type stdClass.', $errors[0]->getMessage()); + $this->assertSame(24, $errors[0]->getLine()); } public function testBug6114(): void @@ -591,9 +601,12 @@ public function testBug6649(): void public function testBug6842(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-6842.php'); - $this->assertCount(1, $errors); + $this->assertCount(2, $errors); $this->assertSame('Generator expects value type T of DateTimeInterface, DateTime|DateTimeImmutable|T of DateTimeInterface given.', $errors[0]->getMessage()); $this->assertSame(28, $errors[0]->getLine()); + + $this->assertSame('Generator expects value type T of DateTimeInterface, DateTime|DateTimeImmutable|T of DateTimeInterface given.', $errors[1]->getMessage()); + $this->assertSame(54, $errors[1]->getLine()); } public function testBug6896(): void @@ -617,7 +630,9 @@ public function testBug6896(): void public function testBug6940(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-6940.php'); - $this->assertNoErrors($errors); + $this->assertCount(1, $errors); + $this->assertSame('Loose comparison using == between array{} and array{} will always evaluate to true.', $errors[0]->getMessage()); + $this->assertSame(12, $errors[0]->getLine()); } public function testBug1447(): void @@ -675,7 +690,7 @@ public function testBug7030(): void $errors = $this->runAnalyse(__DIR__ . '/data/bug-7030.php'); $this->assertCount(1, $errors); $this->assertSame('PHPDoc tag @method has invalid value (array getItemsForID($id, $quantity, $shippingPostCode = null, $wholesalerList = null, $shippingLatitude = - null, $shippingLongitude = null, $shippingNeutralShipping = null)): Unexpected token "\n * ", expected type at offset 193', $errors[0]->getMessage()); + null, $shippingLongitude = null, $shippingNeutralShipping = null)): Unexpected token "\n * ", expected type at offset 193 on line 6', $errors[0]->getMessage()); } public function testBug7012(): void @@ -766,11 +781,11 @@ public function testDiscussion7124(): void $errors = $this->runAnalyse(__DIR__ . '/data/discussion-7124.php'); $this->assertCount(4, $errors); - $this->assertSame('Parameter #2 $callback of function Discussion7124\filter expects callable(bool, int=): bool, Closure(int, bool): bool given.', $errors[0]->getMessage()); + $this->assertSame('Parameter #2 $callback of function Discussion7124\filter expects callable(bool, 0|1|2=): bool, Closure(int, bool): bool given.', $errors[0]->getMessage()); $this->assertSame(38, $errors[0]->getLine()); - $this->assertSame('Parameter #2 $callback of function Discussion7124\filter expects callable(bool, int=): bool, Closure(int): bool given.', $errors[1]->getMessage()); + $this->assertSame('Parameter #2 $callback of function Discussion7124\filter expects callable(bool, 0|1|2=): bool, Closure(int): bool given.', $errors[1]->getMessage()); $this->assertSame(45, $errors[1]->getLine()); - $this->assertSame('Parameter #2 $callback of function Discussion7124\filter expects callable(int): bool, Closure(bool): bool given.', $errors[2]->getMessage()); + $this->assertSame('Parameter #2 $callback of function Discussion7124\filter expects callable(0|1|2): bool, Closure(bool): bool given.', $errors[2]->getMessage()); $this->assertSame(52, $errors[2]->getLine()); $this->assertSame('Parameter #2 $callback of function Discussion7124\filter expects callable(bool): bool, Closure(int): bool given.', $errors[3]->getMessage()); $this->assertSame(59, $errors[3]->getLine()); @@ -871,23 +886,26 @@ public function testBug7554(): void $errors = $this->runAnalyse(__DIR__ . '/data/bug-7554.php'); $this->assertCount(2, $errors); - $this->assertSame(sprintf('Parameter #1 $%s of function count expects array|Countable, array|string>>|false given.', PHP_VERSION_ID < 80000 ? 'var' : 'value'), $errors[0]->getMessage()); + $this->assertSame(sprintf('Parameter #1 $%s of function count expects array|Countable, array>|false given.', PHP_VERSION_ID < 80000 ? 'var' : 'value'), $errors[0]->getMessage()); $this->assertSame(26, $errors[0]->getLine()); - $this->assertSame('Cannot access offset int<1, max> on array}>|false.', $errors[1]->getMessage()); + $this->assertSame('Cannot access offset int<1, max> on list}>|false.', $errors[1]->getMessage()); $this->assertSame(27, $errors[1]->getLine()); } public function testBug7637(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-7637.php'); - $this->assertCount(2, $errors); + $this->assertCount(3, $errors); $this->assertSame('Method Bug7637\HelloWorld::getProperty() has invalid return type Bug7637\rex_backend_login.', $errors[0]->getMessage()); $this->assertSame(54, $errors[0]->getLine()); $this->assertSame('Method Bug7637\HelloWorld::getProperty() has invalid return type Bug7637\rex_timer.', $errors[1]->getMessage()); $this->assertSame(54, $errors[1]->getLine()); + + $this->assertSame('Call to function is_string() with string will always evaluate to true.', $errors[2]->getMessage()); + $this->assertSame(57, $errors[2]->getLine()); } public function testBug7737(): void @@ -931,19 +949,7 @@ public function testBug7581(): void public function testBug7903(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-7903.php'); - $this->assertCount(6, $errors); - $this->assertSame('Comparison operation ">" between 0 and 0 is always false.', $errors[0]->getMessage()); - $this->assertSame(212, $errors[0]->getLine()); - $this->assertSame('Comparison operation ">" between 0 and 0 is always false.', $errors[1]->getMessage()); - $this->assertSame(213, $errors[1]->getLine()); - $this->assertSame('Comparison operation ">" between 0 and 0 is always false.', $errors[2]->getMessage()); - $this->assertSame(214, $errors[2]->getLine()); - $this->assertSame('Comparison operation ">" between 0 and 0 is always false.', $errors[3]->getMessage()); - $this->assertSame(215, $errors[3]->getLine()); - $this->assertSame('Comparison operation ">" between 0 and 0 is always false.', $errors[4]->getMessage()); - $this->assertSame(229, $errors[4]->getLine()); - $this->assertSame('Comparison operation ">" between 0 and 0 is always false.', $errors[5]->getMessage()); - $this->assertSame(230, $errors[5]->getLine()); + $this->assertNoErrors($errors); } public function testBug7901(): void @@ -1017,6 +1023,58 @@ public function testBug3865(): void $this->assertSame(14, $errors[0]->getLine()); } + public function testBug5312(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5312.php'); + $this->assertCount(3, $errors); + $this->assertSame('Parameter $object of method Bug5312\Updatable::update() has invalid type Bug5312\T.', $errors[0]->getMessage()); + $this->assertSame(13, $errors[0]->getLine()); + $this->assertSame('Type Bug5312\T in generic type Bug5312\Updatable in PHPDoc tag @param for parameter $object is not subtype of template type T of Bug5312\Updatable of interface Bug5312\Updatable.', $errors[1]->getMessage()); + $this->assertSame(13, $errors[1]->getLine()); + $this->assertSame('Type Bug5312\T in generic type Bug5312\Updatable in PHPDoc tag @param for parameter $object is not subtype of template type T of Bug5312\Updatable of interface Bug5312\Updatable.', $errors[2]->getMessage()); + $this->assertSame(13, $errors[2]->getLine()); + } + + public function testBug5390(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5390.php'); + $this->assertCount(3, $errors); + $this->assertSame('Property Bug5390\A::$b is never written, only read.', $errors[0]->getMessage()); + $this->assertSame(9, $errors[0]->getLine()); + $this->assertSame('Method Bug5390\A::infiniteRecursion() has no return type specified.', $errors[1]->getMessage()); + $this->assertSame(11, $errors[1]->getLine()); + $this->assertSame('Call to an undefined method Bug5390\B::someMethod().', $errors[2]->getMessage()); + $this->assertSame(12, $errors[2]->getLine()); + } + + public function testBug7110(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7110.php'); + $this->assertCount(1, $errors); + $this->assertSame('Parameter #1 $s of function Bug7110\takesInt expects int, string given.', $errors[0]->getMessage()); + $this->assertSame(34, $errors[0]->getLine()); + } + + public function testBug8376(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8376.php'); + $this->assertNoErrors($errors); + } + + public function testAssertDocblock(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/assert-docblock.php'); + $this->assertCount(4, $errors); + $this->assertSame('Call to method AssertDocblock\A::testInt() with string will always evaluate to false.', $errors[0]->getMessage()); + $this->assertSame(218, $errors[0]->getLine()); + $this->assertSame('Call to method AssertDocblock\A::testNotInt() with string will always evaluate to true.', $errors[1]->getMessage()); + $this->assertSame(224, $errors[1]->getLine()); + $this->assertSame('Call to method AssertDocblock\A::testInt() with int will always evaluate to true.', $errors[2]->getMessage()); + $this->assertSame(232, $errors[2]->getLine()); + $this->assertSame('Call to method AssertDocblock\A::testNotInt() with int will always evaluate to false.', $errors[3]->getMessage()); + $this->assertSame(238, $errors[3]->getLine()); + } + public function testBug8147(): void { if (PHP_VERSION_ID < 80000) { @@ -1027,6 +1085,259 @@ public function testBug8147(): void $this->assertNoErrors($errors); } + public function testConditionalExpressionInfiniteLoop(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/conditional-expression-infinite-loop.php'); + $this->assertNoErrors($errors); + } + + public function testPr2030(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/pr-2030.php'); + $this->assertNoErrors($errors); + } + + public function testBug6265(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6265.php'); + $this->assertNotEmpty($errors); + } + + public function testBug8503(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8503.php'); + $this->assertNoErrors($errors); + } + + public function testBug8537(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8537.php'); + $this->assertNoErrors($errors); + } + + public function testBug8146(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8146b.php'); + $this->assertNoErrors($errors); + } + + public function testBug8215(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8215.php'); + $this->assertNoErrors($errors); + } + + public function testBug8146a(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8146a.php'); + $this->assertNoErrors($errors); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + ]; + } + + public function testBug8004(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8004.php'); + $this->assertCount(2, $errors); + $this->assertSame('Strict comparison using !== between null and DateTimeInterface|string will always evaluate to true.', $errors[0]->getMessage()); + $this->assertSame(49, $errors[0]->getLine()); + + $this->assertSame('Strict comparison using !== between null and DateTimeInterface|string will always evaluate to true.', $errors[1]->getMessage()); + $this->assertSame(59, $errors[1]->getLine()); + } + + public function testSkipCheckNoGenericClasses(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/skip-check-no-generic-classes.php'); + $this->assertCount(1, $errors); + $this->assertSame('Method SkipCheckNoGenericClasses\Foo::doFoo() has parameter $i with generic class LimitIterator but does not specify its types: TKey, TValue, TIterator', $errors[0]->getMessage()); + } + + public function testBug8983(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-8983.php'); + $this->assertNoErrors($errors); + } + + public function testBug9008(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-9008.php'); + $this->assertNoErrors($errors); + } + + public function testBug5091(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5091.php'); + $this->assertNoErrors($errors); + } + + public function testBug9459(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-9459.php'); + $this->assertCount(1, $errors); + $this->assertSame('PHPDoc tag @var with type callable(): array is not subtype of native type Closure(): array{}.', $errors[0]->getMessage()); + $this->assertSame(10, $errors[0]->getLine()); + } + + public function testBug9573(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-9573.php'); + $this->assertNoErrors($errors); + } + + public function testBug9039(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-9039.php'); + $this->assertCount(1, $errors); + $this->assertSame('Constant Bug9039\Test::RULES is unused.', $errors[0]->getMessage()); + } + + public function testDiscussion9053(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/discussion-9053.php'); + $this->assertNoErrors($errors); + } + + public function testProcessCalledMethodInfiniteLoop(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/process-called-method-infinite-loop.php'); + $this->assertNoErrors($errors); + } + + public function testBug9428(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-9428.php'); + $this->assertNoErrors($errors); + } + + public function testBug9690(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-9690.php'); + $this->assertNoErrors($errors); + } + + public function testIgnoreIdentifiers(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/ignore-identifiers.php'); + $this->assertCount(5, $errors); + + $this->assertSame('No error with identifier wrong.id is reported on line 12.', $errors[0]->getMessage()); + $this->assertSame(12, $errors[0]->getLine()); + + $this->assertSame('Undefined variable: $foo', $errors[1]->getMessage()); + $this->assertSame(12, $errors[1]->getLine()); + + $this->assertSame('Undefined variable: $bar', $errors[2]->getMessage()); + $this->assertSame(14, $errors[2]->getLine()); + + $this->assertSame('Undefined variable: $foo', $errors[3]->getMessage()); + $this->assertSame(14, $errors[3]->getLine()); + + $this->assertSame('Undefined variable: $bar', $errors[4]->getMessage()); + $this->assertSame(16, $errors[4]->getLine()); + } + + public function testBug9994(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-9994.php'); + $this->assertCount(2, $errors); + $this->assertSame('Negated boolean expression is always false.', $errors[0]->getMessage()); + $this->assertSame('Parameter #2 $callback of function array_filter expects (callable(1|2|3|null): bool)|null, false given.', $errors[1]->getMessage()); + } + + public function testBug10049(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10049-recursive.php'); + $this->assertNoErrors($errors); + } + + public function testBug10086(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10086.php'); + $this->assertNoErrors($errors); + } + + public function testBug10147(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10147.php'); + $this->assertNoErrors($errors); + } + + public function testBug10302(): void + { + if (PHP_VERSION_ID < 80200) { + $this->markTestSkipped('Test requires PHP 8.2'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10302.php'); + $this->assertNoErrors($errors); + } + + public function testBug10358(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10358.php'); + $this->assertCount(1, $errors); + $this->assertSame('Cannot use Ns\Foo2 as Foo because the name is already in use', $errors[0]->getMessage()); + $this->assertSame(6, $errors[0]->getLine()); + } + + public function testBug10509(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10509.php'); + $this->assertCount(2, $errors); + $this->assertSame('Method Bug10509\Foo::doFoo() has no return type specified.', $errors[0]->getMessage()); + $this->assertSame('PHPDoc tag @return contains unresolvable type.', $errors[1]->getMessage()); + } + + public function testBug10538(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10538.php'); + $this->assertNoErrors($errors); + } + + public function testBug10772(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-10772.php'); + $this->assertNoErrors($errors); + } + /** * @param string[]|null $allAnalysedFiles * @return Error[] @@ -1034,14 +1345,15 @@ public function testBug8147(): void private function runAnalyse(string $file, ?array $allAnalysedFiles = null): array { $file = $this->getFileHelper()->normalizePath($file); - /** @var Analyser $analyser */ + $analyser = self::getContainer()->getByType(Analyser::class); - /** @var FileHelper $fileHelper */ - $fileHelper = self::getContainer()->getByType(FileHelper::class); - /** @var Error[] $errors */ - $errors = $analyser->analyse([$file], null, null, true, $allAnalysedFiles)->getErrors(); + $finalizer = self::getContainer()->getByType(AnalyserResultFinalizer::class); + $errors = $finalizer->finalize( + $analyser->analyse([$file], null, null, true, $allAnalysedFiles), + false, + )->getErrors(); foreach ($errors as $error) { - $this->assertSame($fileHelper->normalizePath($file), $error->getFilePath()); + $this->assertSame($file, $error->getFilePath()); } return $errors; diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index 57d216fe21..5491fb3559 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -5,6 +5,8 @@ use PhpParser\Lexer; use PhpParser\NodeVisitor\NameResolver; use PhpParser\Parser\Php7; +use PHPStan\Analyser\Ignore\IgnoredErrorHelper; +use PHPStan\Analyser\Ignore\IgnoreLexer; use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; use PHPStan\Dependency\ExportedNodeResolver; @@ -16,11 +18,13 @@ use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\StubPhpDocProvider; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\SignatureMap\SignatureMapProvider; use PHPStan\Rules\AlwaysFailRule; use PHPStan\Rules\DirectRegistry as DirectRuleRegistry; use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\FileTypeMapper; +use stdClass; use function array_map; use function array_merge; use function assert; @@ -67,6 +71,38 @@ public function testFileWithAnIgnoredErrorMessage(): void $this->assertEmpty($result); } + public function testFileWithAnIgnoredErrorMessageAndWrongIdentifier(): void + { + $result = $this->runAnalyser([['message' => '#Fail\.#', 'identifier' => 'wrong.identifier']], true, __DIR__ . '/data/bootstrap-error.php', false); + $this->assertCount(2, $result); + assert($result[0] instanceof Error); + $this->assertSame('Fail.', $result[0]->getMessage()); + assert(is_string($result[1])); + $this->assertSame('Ignored error pattern #Fail\.# (wrong.identifier) was not matched in reported errors.', $result[1]); + } + + public function testFileWithAnIgnoredWrongIdentifier(): void + { + $result = $this->runAnalyser([['identifier' => 'wrong.identifier']], true, __DIR__ . '/data/bootstrap-error.php', false); + $this->assertCount(2, $result); + assert($result[0] instanceof Error); + $this->assertSame('Fail.', $result[0]->getMessage()); + assert(is_string($result[1])); + $this->assertSame('Ignored error pattern wrong.identifier was not matched in reported errors.', $result[1]); + } + + public function testFileWithAnIgnoredErrorMessageAndCorrectIdentifier(): void + { + $result = $this->runAnalyser([['message' => '#Fail\.#', 'identifier' => 'tests.alwaysFail']], true, __DIR__ . '/data/bootstrap-error.php', false); + $this->assertEmpty($result); + } + + public function testFileWithAnIgnoredErrorIdentifier(): void + { + $result = $this->runAnalyser([['identifier' => 'tests.alwaysFail']], true, __DIR__ . '/data/bootstrap-error.php', false); + $this->assertEmpty($result); + } + public function testFileWithAnIgnoredErrorMessages(): void { $result = $this->runAnalyser([['messages' => ['#Fail\.#']]], true, __DIR__ . '/data/bootstrap-error.php', false); @@ -110,15 +146,54 @@ public function testIgnoreErrorMultiByPath(): void $this->assertNoErrors($result); } - public function testIgnoreErrorByPathAndCount(): void + public function dataIgnoreErrorByPathAndCount(): iterable { - $ignoreErrors = [ + yield [ [ - 'message' => '#Fail\.#', - 'count' => 3, - 'path' => __DIR__ . '/data/two-fails.php', + [ + 'message' => '#Fail\.#', + 'count' => 3, + 'path' => __DIR__ . '/data/two-fails.php', + ], + ], + ]; + + yield [ + [ + [ + 'message' => '#Fail\.#', + 'count' => 2, + 'path' => __DIR__ . '/data/two-fails.php', + ], + [ + 'message' => '#Fail\.#', + 'count' => 1, + 'path' => __DIR__ . '/data/two-fails.php', + ], + ], + ]; + + yield [ + [ + [ + 'message' => '#Fail\.#', + 'count' => 2, + 'path' => __DIR__ . '/data/two-fails.php', + ], + [ + 'message' => '#Fail\.#', + 'path' => __DIR__ . '/data/two-fails.php', + ], ], ]; + } + + /** + * @dataProvider dataIgnoreErrorByPathAndCount + * @param mixed[] $ignoreErrors + */ + public function testIgnoreErrorByPathAndCount(array $ignoreErrors): void + { $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/two-fails.php', false); $this->assertNoErrors($result); } @@ -361,7 +436,7 @@ public function testIgnoredErrorMissingMessage(): void $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/empty/empty.php', false); $this->assertCount(1, $result); - $this->assertSame('Ignored error {"path":"' . $expectedPath . '/data/empty/empty.php"} is missing a message.', $result[0]); + $this->assertSame('Ignored error {"path":"' . $expectedPath . '/data/empty/empty.php"} is missing a message or an identifier.', $result[0]); } public function testReportMultipleParserErrorsAtOnce(): void @@ -424,30 +499,44 @@ public function testDoNotReportUnmatchedIgnoredErrorsFromPathWithCountIfPathWasN $this->assertNoErrors($result); } - /** - * @dataProvider dataTrueAndFalse - */ - public function testIgnoreNextLine(bool $reportUnmatchedIgnoredErrors): void + public function testIgnoreNextLine(): void { - $result = $this->runAnalyser([], $reportUnmatchedIgnoredErrors, [ + $result = $this->runAnalyser([], false, [ __DIR__ . '/data/ignore-next-line.php', ], true); - $this->assertCount($reportUnmatchedIgnoredErrors ? 4 : 3, $result); - foreach ([10, 30, 34] as $i => $line) { + $this->assertCount(5, $result); + foreach ([10, 20, 24, 31, 50] as $i => $line) { $this->assertArrayHasKey($i, $result); $this->assertInstanceOf(Error::class, $result[$i]); $this->assertSame('Fail.', $result[$i]->getMessage()); $this->assertSame($line, $result[$i]->getLine()); } + } - if (!$reportUnmatchedIgnoredErrors) { - return; + public function testIgnoreNextLineUnmatched(): void + { + $result = $this->runAnalyser([], true, [ + __DIR__ . '/data/ignore-next-line-unmatched.php', + ], true); + $this->assertCount(2, $result); + foreach ([11, 15] as $i => $line) { + $this->assertArrayHasKey($i, $result); + $this->assertInstanceOf(Error::class, $result[$i]); + $this->assertStringContainsString('No error to ignore is reported on line', $result[$i]->getMessage()); + $this->assertSame($line, $result[$i]->getLine()); } + } - $this->assertArrayHasKey(3, $result); - $this->assertInstanceOf(Error::class, $result[3]); - $this->assertSame('No error to ignore is reported on line 38.', $result[3]->getMessage()); - $this->assertSame(38, $result[3]->getLine()); + public function testIgnoreNextLineLegacyBehaviour(): void + { + $result = $this->runAnalyser([], false, [__DIR__ . '/data/ignore-next-line-legacy.php'], true, false); + + foreach ([10, 32, 36] as $i => $line) { + $this->assertArrayHasKey($i, $result); + $this->assertInstanceOf(Error::class, $result[$i]); + $this->assertSame('Fail.', $result[$i]->getMessage()); + $this->assertSame($line, $result[$i]->getLine()); + } } /** @@ -536,9 +625,10 @@ private function runAnalyser( bool $reportUnmatchedIgnoredErrors, $filePaths, bool $onlyFiles, + bool $enableIgnoreErrorsWithinPhpDocs = true, ): array { - $analyser = $this->createAnalyser($reportUnmatchedIgnoredErrors); + $analyser = $this->createAnalyser($enableIgnoreErrorsWithinPhpDocs); if (is_string($filePaths)) { $filePaths = [$filePaths]; @@ -558,7 +648,21 @@ private function runAnalyser( $analyserResult = $analyser->analyse($normalizedFilePaths); - $errors = $ignoredErrorHelperResult->process($analyserResult->getErrors(), $onlyFiles, $normalizedFilePaths, $analyserResult->hasReachedInternalErrorsCountLimit()); + $finalizer = new AnalyserResultFinalizer( + new DirectRuleRegistry([]), + new RuleErrorTransformer(), + $this->createScopeFactory( + $this->createReflectionProvider(), + self::getContainer()->getService('typeSpecifier'), + ), + new LocalIgnoresProcessor(), + $reportUnmatchedIgnoredErrors, + ); + $analyserResult = $finalizer->finalize($analyserResult, $onlyFiles)->getAnalyserResult(); + + $ignoredErrorHelperProcessedResult = $ignoredErrorHelperResult->process($analyserResult->getErrors(), $onlyFiles, $normalizedFilePaths, $analyserResult->hasReachedInternalErrorsCountLimit()); + $errors = $ignoredErrorHelperProcessedResult->getNotIgnoredErrors(); + $errors = array_merge($errors, $ignoredErrorHelperProcessedResult->getOtherIgnoreMessages()); if ($analyserResult->hasReachedInternalErrorsCountLimit()) { $errors[] = sprintf('Reached internal errors count limit of %d, exiting...', 50); } @@ -569,7 +673,7 @@ private function runAnalyser( ); } - private function createAnalyser(bool $reportUnmatchedIgnoredErrors): Analyser + private function createAnalyser(bool $enableIgnoreErrorsWithinPhpDocs): Analyser { $ruleRegistry = new DirectRuleRegistry([ new AlwaysFailRule(), @@ -587,21 +691,27 @@ private function createAnalyser(bool $reportUnmatchedIgnoredErrors): Analyser $reflectionProvider, self::getContainer()->getByType(InitializerExprTypeResolver::class), self::getReflector(), - $this->getClassReflectionExtensionRegistryProvider(), + self::getClassReflectionExtensionRegistryProvider(), $this->getParser(), $fileTypeMapper, self::getContainer()->getByType(StubPhpDocProvider::class), self::getContainer()->getByType(PhpVersion::class), + self::getContainer()->getByType(SignatureMapProvider::class), $phpDocInheritanceResolver, $fileHelper, $typeSpecifier, self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class), + self::createScopeFactory($reflectionProvider, $typeSpecifier), false, true, [], [], + [stdClass::class], true, + $this->shouldTreatPhpDocTypesAsCertain(), + self::getContainer()->getParameter('featureToggles')['detectDeadTypeInMultiCatch'], + self::getContainer()->getParameter('featureToggles')['paramOutType'], ); $lexer = new Lexer(['usedAttributes' => ['comments', 'startLine', 'endLine', 'startTokenPos', 'endTokenPos']]); $fileAnalyser = new FileAnalyser( @@ -612,10 +722,12 @@ private function createAnalyser(bool $reportUnmatchedIgnoredErrors): Analyser $lexer, new NameResolver(), self::getContainer(), + new IgnoreLexer(), + $enableIgnoreErrorsWithinPhpDocs, ), new DependencyResolver($fileHelper, $reflectionProvider, new ExportedNodeResolver($fileTypeMapper, new ExprPrinter(new Printer())), $fileTypeMapper), new RuleErrorTransformer(), - $reportUnmatchedIgnoredErrors, + new LocalIgnoresProcessor(), ); return new Analyser( diff --git a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php index 611d11121d..023d00e9f4 100644 --- a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php @@ -5,7 +5,11 @@ use PHPStan\File\FileHelper; use PHPStan\Testing\PHPStanTestCase; use function array_map; +use function array_merge; +use function array_unique; use function sprintf; +use function usort; +use const PHP_VERSION_ID; class AnalyserTraitsIntegrationTest extends PHPStanTestCase { @@ -165,6 +169,36 @@ public function testMissingReturnInAbstractTraitMethod(): void $this->assertNoErrors($errors); } + public function testUnititializedReadonlyPropertyAccessedInTrait(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped(); + } + + $errors = $this->runAnalyse([ + __DIR__ . '/traits/uninitializedProperty/FooClass.php', + __DIR__ . '/traits/uninitializedProperty/FooTrait.php', + ]); + $this->assertCount(3, $errors); + usort($errors, static fn (Error $a, Error $b) => $a->getLine() <=> $b->getLine()); + $expectedFile = sprintf('%s (in context of class TraitsUnititializedProperty\FooClass)', $this->fileHelper->normalizePath(__DIR__ . '/traits/uninitializedProperty/FooTrait.php')); + + $error = $errors[0]; + $this->assertSame('Access to an uninitialized readonly property TraitsUnititializedProperty\FooClass::$x.', $error->getMessage()); + $this->assertSame(15, $error->getLine()); + $this->assertSame($expectedFile, $error->getFile()); + + $error = $errors[1]; + $this->assertSame('Access to an uninitialized @readonly property TraitsUnititializedProperty\FooClass::$y.', $error->getMessage()); + $this->assertSame(16, $error->getLine()); + $this->assertSame($expectedFile, $error->getFile()); + + $error = $errors[2]; + $this->assertSame('Access to an uninitialized property TraitsUnititializedProperty\FooClass::$z.', $error->getMessage()); + $this->assertSame(17, $error->getLine()); + $this->assertSame($expectedFile, $error->getFile()); + } + /** * @param string[] $files * @return Error[] @@ -174,9 +208,21 @@ private function runAnalyse(array $files): array $files = array_map(fn (string $file): string => $this->getFileHelper()->normalizePath($file), $files); /** @var Analyser $analyser */ $analyser = self::getContainer()->getByType(Analyser::class); - /** @var Error[] $errors */ - $errors = $analyser->analyse($files)->getErrors(); - return $errors; + + return $analyser->analyse($files)->getErrors(); + } + + public static function getAdditionalConfigFiles(): array + { + return array_unique( + array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/traits-integration.neon', + ], + ), + ); } } diff --git a/tests/PHPStan/Analyser/AnonymousClassNameRule.php b/tests/PHPStan/Analyser/AnonymousClassNameRule.php index 0bba0df265..2c4f09e0eb 100644 --- a/tests/PHPStan/Analyser/AnonymousClassNameRule.php +++ b/tests/PHPStan/Analyser/AnonymousClassNameRule.php @@ -7,7 +7,11 @@ use PHPStan\Broker\ClassNotFoundException; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +/** + * @implements Rule + */ class AnonymousClassNameRule implements Rule { @@ -20,10 +24,6 @@ public function getNodeType(): string return Class_::class; } - /** - * @param Class_ $node - * @return string[] - */ public function processNode(Node $node, Scope $scope): array { $className = isset($node->namespacedName) @@ -32,10 +32,18 @@ public function processNode(Node $node, Scope $scope): array try { $this->reflectionProvider->getClass($className); } catch (ClassNotFoundException) { - return ['not found']; + return [ + RuleErrorBuilder::message('not found') + ->identifier('tests.anonymousClassName') + ->build(), + ]; } - return ['found']; + return [ + RuleErrorBuilder::message('found') + ->identifier('tests.anonymousClassName') + ->build(), + ]; } } diff --git a/tests/PHPStan/Analyser/ArgumentsNormalizerTest.php b/tests/PHPStan/Analyser/ArgumentsNormalizerTest.php index 311d189ca6..3d28594e7e 100644 --- a/tests/PHPStan/Analyser/ArgumentsNormalizerTest.php +++ b/tests/PHPStan/Analyser/ArgumentsNormalizerTest.php @@ -202,6 +202,46 @@ public function dataReorderValid(): iterable new StringType(), ], ]; + + yield [ + [ + ['one', true, false, new IntegerType()], + ['two', true, false, new StringType()], + ['three', true, false, new FloatType()], + ], + [], + [], + ]; + + yield [ + [ + ['one', true, false, new IntegerType()], + ['two', true, false, new StringType()], + ['three', true, false, new FloatType()], + ], + [ + [new StringType(), 'onee'], + ], + [ + new StringType(), + ], + ]; + + yield [ + [ + ['one', true, false, new IntegerType()], + ['two', true, false, new StringType()], + ['three', true, false, new FloatType()], + ], + [ + [new IntegerType(), null], + [new StringType(), 'onee'], + ], + [ + new IntegerType(), + new StringType(), + ], + ]; } /** @@ -282,29 +322,6 @@ public function dataReorderInvalid(): iterable [new StringType(), 'three'], ], ]; - - yield [ - [ - ['one', true, false, new IntegerType()], - ['two', true, false, new StringType()], - ['three', true, false, new FloatType()], - ], - [ - [new StringType(), 'onee'], - ], - ]; - - yield [ - [ - ['one', true, false, new IntegerType()], - ['two', true, false, new StringType()], - ['three', true, false, new FloatType()], - ], - [ - [new IntegerType(), null], - [new StringType(), 'onee'], - ], - ]; } /** diff --git a/tests/PHPStan/Analyser/AssertStubTest.php b/tests/PHPStan/Analyser/AssertStubTest.php new file mode 100644 index 0000000000..2ea24ac4ce --- /dev/null +++ b/tests/PHPStan/Analyser/AssertStubTest.php @@ -0,0 +1,36 @@ +gatherAssertTypes(__DIR__ . '/data/assert-stub.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/assert-stub.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/Bug10922Test.php b/tests/PHPStan/Analyser/Bug10922Test.php new file mode 100644 index 0000000000..2dccf946c8 --- /dev/null +++ b/tests/PHPStan/Analyser/Bug10922Test.php @@ -0,0 +1,36 @@ +gatherAssertTypes(__DIR__ . '/data/bug-10922.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/bug-10922.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php new file mode 100644 index 0000000000..ff0a0daa9e --- /dev/null +++ b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php @@ -0,0 +1,43 @@ + + */ +class Bug9307CallMethodsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, true, false, true, false); + return new CallMethodsRule( + new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), + new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion(PHP_VERSION_ID), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true), + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return false; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/bug-9307.php'], []); + } + +} diff --git a/tests/PHPStan/Analyser/Bug9307Test.php b/tests/PHPStan/Analyser/Bug9307Test.php new file mode 100644 index 0000000000..6502ac2701 --- /dev/null +++ b/tests/PHPStan/Analyser/Bug9307Test.php @@ -0,0 +1,36 @@ +gatherAssertTypes(__DIR__ . '/data/bug-9307.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/bug-9307.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php b/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php index 75198a63cd..0d5f8a8dc8 100644 --- a/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php +++ b/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php @@ -15,7 +15,7 @@ public function dataFileAsserts(): iterable } yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-throw-type-extension.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-throw-type-extension-named-args.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-throw-type-extension-named-args-fixture.php'); } /** diff --git a/tests/PHPStan/Analyser/ErrorTest.php b/tests/PHPStan/Analyser/ErrorTest.php index 5e7d1e8ad0..7bd49a242a 100644 --- a/tests/PHPStan/Analyser/ErrorTest.php +++ b/tests/PHPStan/Analyser/ErrorTest.php @@ -15,4 +15,45 @@ public function testError(): void $this->assertSame(10, $error->getLine()); } + public function dataValidIdentifier(): iterable + { + yield ['a']; + yield ['aa']; + yield ['phpstan']; + yield ['phpstan.internal']; + yield ['phpstan.alwaysFail']; + yield ['Phpstan.alwaysFail']; + yield ['phpstan.internal.foo']; + yield ['foo2.test']; + yield ['phpstan123']; + yield ['3m.blah']; + } + + /** + * @dataProvider dataValidIdentifier + */ + public function testValidIdentifier(string $identifier): void + { + $this->assertTrue(Error::validateIdentifier($identifier)); + } + + public function dataInvalidIdentifier(): iterable + { + yield ['']; + yield [' ']; + yield ['phpstan ']; + yield [' phpstan']; + yield ['.phpstan']; + yield ['phpstan.']; + yield ['.']; + } + + /** + * @dataProvider dataInvalidIdentifier + */ + public function testInvalidIdentifier(string $identifier): void + { + $this->assertFalse(Error::validateIdentifier($identifier)); + } + } diff --git a/tests/PHPStan/Analyser/EvaluationOrderRule.php b/tests/PHPStan/Analyser/EvaluationOrderRule.php index 14fe278783..6cec461953 100644 --- a/tests/PHPStan/Analyser/EvaluationOrderRule.php +++ b/tests/PHPStan/Analyser/EvaluationOrderRule.php @@ -4,7 +4,11 @@ use PhpParser\Node; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +/** + * @implements Rule + */ class EvaluationOrderRule implements Rule { @@ -13,20 +17,25 @@ public function getNodeType(): string return Node::class; } - /** - * @return string[] - */ public function processNode(Node $node, Scope $scope): array { if ( $node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name ) { - return [$node->name->toString()]; + return [ + RuleErrorBuilder::message($node->name->toString()) + ->identifier('tests.evaluationOrder') + ->build(), + ]; } if ($node instanceof Node\Scalar\String_) { - return [$node->value]; + return [ + RuleErrorBuilder::message($node->value) + ->identifier('tests.evaluationOrder') + ->build(), + ]; } return []; diff --git a/tests/PHPStan/Analyser/ExpressionTypeResolverExtensionTest.php b/tests/PHPStan/Analyser/ExpressionTypeResolverExtensionTest.php new file mode 100644 index 0000000000..b8dda807d4 --- /dev/null +++ b/tests/PHPStan/Analyser/ExpressionTypeResolverExtensionTest.php @@ -0,0 +1,35 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/expression-type-resolver-extension.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/Ignore/IgnoreLexerTest.php b/tests/PHPStan/Analyser/Ignore/IgnoreLexerTest.php new file mode 100644 index 0000000000..68075be52d --- /dev/null +++ b/tests/PHPStan/Analyser/Ignore/IgnoreLexerTest.php @@ -0,0 +1,90 @@ + $expectedTokens + */ + public function testTokenize(string $input, array $expectedTokens): void + { + $lexer = new IgnoreLexer(); + $this->assertSame($expectedTokens, $lexer->tokenize($input)); + } + +} diff --git a/tests/PHPStan/Analyser/ImmediatelyCalledFunctionWithoutImplicitThrowTest.php b/tests/PHPStan/Analyser/ImmediatelyCalledFunctionWithoutImplicitThrowTest.php new file mode 100644 index 0000000000..f810ed04a2 --- /dev/null +++ b/tests/PHPStan/Analyser/ImmediatelyCalledFunctionWithoutImplicitThrowTest.php @@ -0,0 +1,37 @@ +gatherAssertTypes(__DIR__ . '/data/immediately-called-function-without-implicit-throw.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [__DIR__ . '/data/immediately-called-function-without-implicit-throw.neon'], + ); + } + +} diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index b6ef211944..94cb5f86af 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -36,7 +36,7 @@ class LegacyNodeScopeResolverTest extends TypeInferenceTestCase public function testClassMethodScope(): void { - $this->processFile(__DIR__ . '/data/class.php', function (Node $node, Scope $scope): void { + self::processFile(__DIR__ . '/data/class.php', function (Node $node, Scope $scope): void { if (!($node instanceof Exit_)) { return; } @@ -65,7 +65,6 @@ public function testClassMethodScope(): void private function getFileScope(string $filename): Scope { - /** @var Scope $testScope */ $testScope = null; $this->processFile($filename, static function (Node $node, Scope $scope) use (&$testScope): void { if (!($node instanceof Exit_)) { @@ -75,6 +74,7 @@ private function getFileScope(string $filename): Scope $testScope = $scope; }); + /** @var Scope */ return $testScope; } @@ -302,7 +302,7 @@ public function dataAssignInIf(): array $testScope, 'matches', TrinaryLogic::createYes(), - 'mixed', + 'array', ], [ $testScope, @@ -343,7 +343,7 @@ public function dataAssignInIf(): array $testScope, 'matches2', TrinaryLogic::createMaybe(), - 'mixed', + 'array', ], [ $testScope, @@ -355,13 +355,13 @@ public function dataAssignInIf(): array $testScope, 'matches3', TrinaryLogic::createYes(), - 'mixed', + 'array', ], [ $testScope, 'matches4', TrinaryLogic::createMaybe(), - 'mixed', + 'array', ], [ $testScope, @@ -415,7 +415,7 @@ public function dataAssignInIf(): array $testScope, 'ternaryMatches', TrinaryLogic::createYes(), - 'mixed', + 'array', ], [ $testScope, @@ -1282,7 +1282,7 @@ public function dataParameterTypes(): array '$callable', ], [ - PHP_VERSION_ID < 80000 ? 'array' : 'array', + PHP_VERSION_ID < 80000 ? 'list' : 'array', '$variadicStrings', ], [ @@ -1412,11 +1412,11 @@ public function dataVarAnnotations(): array '$callable', ], [ - 'callable(int, ...string): void', + 'callable(int, string ...): void', '$callableWithTypes', ], [ - 'Closure(int, ...string): void', + 'Closure(int, string ...): void', '$closureWithTypes', ], [ @@ -2916,7 +2916,7 @@ public function dataBinaryOperations(): array '"$fooString bar"', ], [ - '*ERROR*', + 'non-falsy-string', '"$std bar"', ], [ @@ -2956,19 +2956,19 @@ public function dataBinaryOperations(): array '$string--', ], [ - 'string', + '(float|int|string)', '++$string', ], [ - 'string', + '(float|int|string)', '--$string', ], [ - 'string', + '(float|int|string)', '$incrementedString', ], [ - 'string', + '(float|int|string)', '$decrementedString', ], [ @@ -2996,15 +2996,15 @@ public function dataBinaryOperations(): array '$decrementedFooString', ], [ - 'literal-string&non-falsy-string', + "'barbar'|'barfoo'|'foobar'|'foofoo'", '$conditionalString . $conditionalString', ], [ - 'literal-string&non-falsy-string', + "'baripsum'|'barlorem'|'fooipsum'|'foolorem'", '$conditionalString . $anotherConditionalString', ], [ - 'literal-string&non-falsy-string', + "'ipsumbar'|'ipsumfoo'|'lorembar'|'loremfoo'", '$anotherConditionalString . $conditionalString', ], [ @@ -3100,13 +3100,21 @@ public function dataBinaryOperations(): array '$coalesceArray', ], [ - 'array<0|1|2, 1|2|3>', + 'array{1, 2, 3}', '$arrayToBeUnset', ], [ - 'array<0|1|2, 1|2|3>', + 'array{1, 2, 3}', '$arrayToBeUnset2', ], + [ + 'array{0?: 1, 1?: 2, 2?: 3}', + '$arrayToBeUnset3', + ], + [ + 'array{0?: 1, 1?: 2, 2?: 3}', + '$arrayToBeUnset4', + ], [ 'array', '$shiftedNonEmptyArray', @@ -3612,6 +3620,23 @@ public function testTypeFromFunctionPhpDocsPhpstanPrefix( ); } + /** + * @dataProvider dataTypeFromFunctionPhpDocs + * @dataProvider dataTypeFromFunctionPrefixedPhpDocs + */ + public function testTypeFromFunctionPhpDocsPhanPrefix( + string $description, + string $expression, + ): void + { + require_once __DIR__ . '/data/functionPhpDocs-phanPrefix.php'; + $this->assertTypes( + __DIR__ . '/data/functionPhpDocs-phanPrefix.php', + $description, + $expression, + ); + } + public function dataTypeFromMethodPhpDocs(): array { return [ @@ -3821,6 +3846,31 @@ public function testTypeFromMethodPhpDocsPhpstanPrefix( ); } + /** + * @dataProvider dataTypeFromFunctionPhpDocs + * @dataProvider dataTypeFromMethodPhpDocs + */ + public function testTypeFromMethodPhpDocsPhanPrefix( + string $description, + string $expression, + bool $replaceClass = true, + ): void + { + $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooPhanPrefix)', $description); + + if ($replaceClass && $expression !== '$this->doFoo()') { + $description = str_replace('$this(MethodPhpDocsNamespace\Foo)', '$this(MethodPhpDocsNamespace\FooPhanPrefix)', $description); + if ($description === 'MethodPhpDocsNamespace\Foo') { + $description = 'MethodPhpDocsNamespace\FooPhanPrefix'; + } + } + $this->assertTypes( + __DIR__ . '/data/methodPhpDocs-phanPrefix.php', + $description, + $expression, + ); + } + /** * @dataProvider dataTypeFromFunctionPhpDocs * @dataProvider dataTypeFromMethodPhpDocs @@ -3857,11 +3907,11 @@ public function testTypeFromMethodPhpDocsInheritDocWithoutCurlyBraces( ): void { if ($replaceClass) { - $description = str_replace('$this(MethodPhpDocsNamespace\Foo)', '$this(MethodPhpDocsNamespace\FooInheritDocChild)', $description); - $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooInheritDocChild)', $description); + $description = str_replace('$this(MethodPhpDocsNamespace\Foo)', '$this(MethodPhpDocsNamespace\FooInheritDocChildWithoutCurly)', $description); + $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooInheritDocChildWithoutCurly)', $description); $description = str_replace('MethodPhpDocsNamespace\FooParent', 'MethodPhpDocsNamespace\Foo', $description); if ($expression === '$inlineSelf') { - $description = 'MethodPhpDocsNamespace\FooInheritDocChild'; + $description = 'MethodPhpDocsNamespace\FooInheritDocChildWithoutCurly'; } } $this->assertTypes( @@ -3975,7 +4025,7 @@ public function testNotSwitchInstanceof(): void { $this->assertTypes( __DIR__ . '/data/switch-instanceof-not.php', - '*ERROR*', + '*NEVER*', '$foo', ); } @@ -4222,7 +4272,7 @@ public function dataAnonymousFunction(): array '$str', ], [ - PHP_VERSION_ID < 80000 ? 'array' : 'array', + PHP_VERSION_ID < 80000 ? 'list' : 'array', '$arr', ], [ @@ -4351,7 +4401,7 @@ public function dataForeachArrayType(): array ], [ __DIR__ . '/data/foreach/foreach-iterable-with-specified-key-type.php', - 'ForeachWithGenericsPhpDoc\Bar|ForeachWithGenericsPhpDoc\Foo', + 'ForeachWithGenericsPhpDocIterable\Bar|ForeachWithGenericsPhpDocIterable\Foo', '$key', ], [ @@ -4361,7 +4411,7 @@ public function dataForeachArrayType(): array ], [ __DIR__ . '/data/foreach/foreach-iterable-with-complex-value-type.php', - 'float|ForeachWithComplexValueType\Foo', + 'float|ForeachIterableWithComplexValueType\Foo', '$value', ], [ @@ -4547,7 +4597,7 @@ public function dataArrayFunctions(): array 'array_combine([1], [2])', ], [ - 'false', + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', 'array_combine([1, 2], [3])', ], [ @@ -4616,7 +4666,7 @@ public function dataArrayFunctions(): array ], [ 'array', - 'array_intersect_key(...$generalIntegersInAnotherArray, [])', + 'array_intersect_key(...$generalIntegersInAnotherArray)', ], [ 'array<0|1|2, 1|2|3>', @@ -4655,7 +4705,7 @@ public function dataArrayFunctions(): array '$filledIntegersWithKeys', ], [ - 'non-empty-array', + 'non-empty-list<\'foo\'>', '$filledNonEmptyArray', ], [ @@ -4667,11 +4717,11 @@ public function dataArrayFunctions(): array '$filledNegativeConstAlwaysFalse', ], [ - 'array|false', + PHP_VERSION_ID < 80000 ? 'list<1>|false' : 'list<1>', '$filledByMaybeNegativeRange', ], [ - 'non-empty-array', + 'non-empty-list<1>', '$filledByPositiveRange', ], [ @@ -4687,7 +4737,7 @@ public function dataArrayFunctions(): array 'array_keys($stringOrIntegerKeys)', ], [ - 'array', + 'list', 'array_keys($generalStringKeys)', ], [ @@ -4695,7 +4745,7 @@ public function dataArrayFunctions(): array 'array_values($integerKeys)', ], [ - 'array', + 'list', 'array_values($generalStringKeys)', ], [ @@ -4752,7 +4802,7 @@ public function dataArrayFunctions(): array 'array_fill(5, 6, \'banana\')', ], [ - 'non-empty-array', + 'non-empty-list<\'apple\'>', 'array_fill(0, 101, \'apple\')', ], [ @@ -4764,7 +4814,7 @@ public function dataArrayFunctions(): array 'array_fill($integer, 2, new \stdClass())', ], [ - 'array', + PHP_VERSION_ID < 80000 ? 'array|false' : 'array', 'array_fill(2, $integer, new \stdClass())', ], [ @@ -4892,7 +4942,7 @@ public function dataArrayFunctions(): array 'array_search(9, $generalStringKeys)', ], [ - 'null', + PHP_VERSION_ID < 80000 ? 'null' : '*NEVER*', 'array_search(999, $integer, true)', ], [ @@ -4940,19 +4990,19 @@ public function dataArrayFunctions(): array 'array_search(\'id\', $generalIntegerOrStringKeysMixedValues, true)', ], [ - 'int|string|false|null', + '*ERROR*', 'array_search(\'id\', doFoo() ? $generalIntegerOrStringKeys : false, true)', ], [ - 'false|null', + '*ERROR*', 'array_search(\'id\', doFoo() ? [] : false, true)', ], [ - 'null', + PHP_VERSION_ID < 80000 ? 'null' : '*NEVER*', 'array_search(\'id\', false, true)', ], [ - 'null', + PHP_VERSION_ID < 80000 ? 'null' : '*NEVER*', 'array_search(\'id\', false)', ], [ @@ -5203,12 +5253,12 @@ public function testArrayFunctions( public function dataFunctions(): array { - $strSplitDefaultReturnType = 'non-empty-array|false'; + $strSplitDefaultReturnType = 'non-empty-list|false'; if (PHP_VERSION_ID >= 80000) { - $strSplitDefaultReturnType = 'non-empty-array'; + $strSplitDefaultReturnType = 'non-empty-list'; } if (PHP_VERSION_ID >= 80200) { - $strSplitDefaultReturnType = 'array'; + $strSplitDefaultReturnType = 'list'; } return [ @@ -5325,7 +5375,7 @@ public function dataFunctions(): array '$mbInternalEncodingWithUnknownEncoding', ], [ - 'array', + 'list', '$mbEncodingAliasesWithValidEncoding', ], [ @@ -5333,11 +5383,11 @@ public function dataFunctions(): array '$mbEncodingAliasesWithInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbEncodingAliasesWithValidAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbEncodingAliasesWithUnknownEncoding', ], [ @@ -5409,7 +5459,7 @@ public function dataFunctions(): array '$strSplitConstantStringWithoutDefinedSplitLength', ], [ - PHP_VERSION_ID < 80200 ? 'non-empty-array' : 'array', + PHP_VERSION_ID < 80200 ? 'non-empty-list' : 'list', '$strSplitStringWithoutDefinedSplitLength', ], [ @@ -5644,7 +5694,7 @@ public function dataRangeFunction(): array 'range(2, 5, 2)', ], [ - 'array{2.0, 3.0, 4.0, 5.0}', + PHP_VERSION_ID < 80300 ? 'array{2.0, 3.0, 4.0, 5.0}' : 'array{2, 3, 4, 5}', 'range(2, 5, 1.0)', ], [ @@ -5652,19 +5702,19 @@ public function dataRangeFunction(): array 'range(2.1, 5)', ], [ - 'array', + 'list', 'range(2, 5, $integer)', ], [ - 'array', + 'list', 'range($float, 5, $integer)', ], [ - 'array', + 'list<(float|int|string)>', 'range($float, $mixed, $integer)', ], [ - 'array', + 'list<(float|int|string)>', 'range($integer, $mixed)', ], [ @@ -5680,7 +5730,7 @@ public function dataRangeFunction(): array 'range(3, -1)', ], [ - 'non-empty-array>', + 'non-empty-list>', 'range(0, 50)', ], ]; @@ -6064,15 +6114,15 @@ public function dataVoid(): array { return [ [ - 'void', + 'null', '$this->doFoo()', ], [ - 'void', + 'null', '$this->doBar()', ], [ - 'void', + 'null', '$this->doConflictingVoid()', ], ]; @@ -6876,12 +6926,12 @@ public function dataForeachLoopVariables(): array "'end'", ], [ - 'non-empty-array', + 'non-empty-list<1|2|3>', '$integers', "'end'", ], [ - 'array', + 'list<1|2|3>', '$integers', "'afterLoop'", ], @@ -7234,7 +7284,7 @@ public function dataExplode(): array { return [ [ - 'non-empty-array', + 'non-empty-list', '$sureArray', ], [ @@ -7242,15 +7292,15 @@ public function dataExplode(): array '$sureFalse', ], [ - PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$arrayOrFalse', ], [ - PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$anotherArrayOrFalse', ], [ - PHP_VERSION_ID < 80000 ? '(non-empty-array|false)' : 'non-empty-array', + PHP_VERSION_ID < 80000 ? '(list|false)' : 'list', '$benevolentArrayOrFalse', ], ]; @@ -7392,19 +7442,19 @@ public function dataReplaceFunctions(): array '$anotherExpectedArray', ], [ - 'array|string', + 'list|string', '$expectedArrayOrString', ], [ - '(array|string)', + '(list|string)', '$expectedBenevolentArrayOrString', ], [ - 'array|string|null', + 'list|string|null', '$expectedArrayOrString2', ], [ - 'array|string|null', + 'list|string|null', '$anotherExpectedArrayOrString', ], [ @@ -7454,11 +7504,13 @@ public function dataFilterVar(): Generator 'FILTER_SANITIZE_SPECIAL_CHARS', 'FILTER_SANITIZE_STRING', 'FILTER_SANITIZE_URL', + 'FILTER_VALIDATE_REGEXP', + ], + 'non-falsy-string' => [ 'FILTER_VALIDATE_EMAIL', 'FILTER_VALIDATE_IP', '$filterIp', 'FILTER_VALIDATE_MAC', - 'FILTER_VALIDATE_REGEXP', 'FILTER_VALIDATE_URL', ], 'int' => ['FILTER_VALIDATE_INT'], @@ -7951,11 +8003,11 @@ public function dataPassedByReference(): array '$arr', ], [ - 'mixed', + 'array', '$matches', ], [ - 'mixed', + 'string', '$s', ], ]; @@ -7988,11 +8040,11 @@ public function dataCallables(): array '$closure()', ], [ - 'Callables\\Bar', + PHP_VERSION_ID < 80000 ? 'Callables\\Bar' : '*ERROR*', '$arrayWithStaticMethod()', ], [ - 'float', + PHP_VERSION_ID < 80000 ? 'float' : '*ERROR*', '$stringWithStaticMethod()', ], [ @@ -8041,7 +8093,7 @@ public function dataArrayKeysInBranches(): array '$arrayAppendedInIf', ], [ - 'non-empty-array', + 'non-empty-list<\'bar\'|\'baz\'|\'foo\'>', '$arrayAppendedInForeach', ], [ @@ -8373,7 +8425,7 @@ public function dataDynamicConstants(): array 'DynamicConstants\NoDynamicConstantClass::DYNAMIC_CONSTANT_IN_CLASS', ], [ - 'bool', + 'false', 'GLOBAL_DYNAMIC_CONSTANT', ], [ @@ -8403,6 +8455,52 @@ public function testDynamicConstants( ); } + public function dataDynamicConstantsWithNativeTypes(): array + { + return [ + [ + 'int', + 'DynamicConstantNativeTypes\Foo::FOO', + ], + [ + 'int|string', + 'DynamicConstantNativeTypes\Foo::BAR', + ], + [ + 'int', + '$foo::FOO', + ], + [ + 'int|string', + '$foo::BAR', + ], + ]; + } + + /** + * @dataProvider dataDynamicConstantsWithNativeTypes + */ + public function testDynamicConstantsWithNativeTypes( + string $description, + string $expression, + ): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->assertTypes( + __DIR__ . '/data/dynamic-constant-native-types.php', + $description, + $expression, + 'die', + [ + 'DynamicConstantNativeTypes\Foo::FOO', + 'DynamicConstantNativeTypes\Foo::BAR', + ], + ); + } + public function dataIsset(): array { return [ @@ -8532,43 +8630,6 @@ public function testPropertyArrayAssignment( ); } - public function dataInArray(): array - { - return [ - [ - '\'bar\'|\'foo\'', - '$s', - ], - [ - 'string', - '$mixed', - ], - [ - 'string', - '$r', - ], - [ - '\'foo\'', - '$fooOrBarOrBaz', - ], - ]; - } - - /** - * @dataProvider dataInArray - */ - public function testInArray( - string $description, - string $expression, - ): void - { - $this->assertTypes( - __DIR__ . '/data/in-array.php', - $description, - $expression, - ); - } - public function dataGetParentClass(): array { return [ @@ -8697,19 +8758,19 @@ public function dataPhp73Functions(): array { return [ [ - 'string|false', + 'non-empty-string|false', 'json_encode($mixed)', ], [ - 'string', + 'non-empty-string', 'json_encode($mixed, JSON_THROW_ON_ERROR)', ], [ - 'string', + 'non-empty-string', 'json_encode($mixed, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ - 'string', + 'non-empty-string', 'json_encode($mixed, $integer | JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK)', ], [ @@ -8837,7 +8898,7 @@ public function dataPhp74Functions(): array { return [ [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithoutDefinedParameters', ], [ @@ -8845,7 +8906,7 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithoutDefinedSplitLength', ], [ - 'array', + 'list', '$mbStrSplitStringWithoutDefinedSplitLength', ], [ @@ -8861,7 +8922,7 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithFailureSplitLength', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithInvalidSplitLengthType', ], [ @@ -8869,7 +8930,7 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLength', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLength', ], [ @@ -8881,7 +8942,7 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding', ], [ @@ -8893,7 +8954,7 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding', ], [ @@ -8909,7 +8970,7 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding', ], [ @@ -8917,7 +8978,7 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding', ], [ @@ -8929,11 +8990,11 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding', ], [ @@ -8941,7 +9002,7 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding', ], ]; @@ -9052,7 +9113,7 @@ public function dataGeneralizeScope(): array { return [ [ - 'array, removeCount: int<0, max>, loadCount: int<0, max>, hitCount: int<0, max>}>>', + 'array, removeCount: int<0, max>, loadCount: int<0, max>, hitCount: int<0, max>}>>', '$statistics', ], ]; @@ -9077,7 +9138,7 @@ public function dataGeneralizeScopeRecursiveType(): array { return [ [ - 'array{}|array{foo: array}', + 'array{}|array{foo?: array}', '$data', ], ]; @@ -9344,11 +9405,11 @@ public function dataArraySpread(): array { return [ [ - 'non-empty-array', + 'non-empty-list', '$integersOne', ], [ - 'non-empty-array', + 'non-empty-list', '$integersTwo', ], [ @@ -9356,11 +9417,11 @@ public function dataArraySpread(): array '$integersThree', ], [ - 'non-empty-array', + 'non-empty-list', '$integersFour', ], [ - 'non-empty-array', + 'non-empty-list', '$integersFive', ], [ @@ -9421,7 +9482,7 @@ public function dataPhp74FunctionsIn74(): array { return [ [ - 'array', + 'list', 'password_algos()', ], ]; @@ -9511,24 +9572,25 @@ private function assertTypes( $assertType(self::$assertTypesCache[$file][$evaluatedPointExpression]); return; } - $this->processFile( - $file, - static function (Node $node, Scope $scope) use ($file, $evaluatedPointExpression, $assertType): void { - if ($node instanceof VirtualNode) { - return; - } - $printer = new Printer(); - $printedNode = $printer->prettyPrint([$node]); - if ($printedNode !== $evaluatedPointExpression) { - return; - } - - self::$assertTypesCache[$file][$evaluatedPointExpression] = $scope; - - $assertType($scope); - }, - $dynamicConstantNames, - ); + + self::processFile( + $file, + static function (Node $node, Scope $scope) use ($file, $evaluatedPointExpression, $assertType): void { + if ($node instanceof VirtualNode) { + return; + } + $printer = new Printer(); + $printedNode = $printer->prettyPrint([$node]); + if ($printedNode !== $evaluatedPointExpression) { + return; + } + + self::$assertTypesCache[$file][$evaluatedPointExpression] = $scope; + + $assertType($scope); + }, + $dynamicConstantNames, + ); } public static function getAdditionalConfigFiles(): array @@ -9562,7 +9624,7 @@ public function dataDeclareStrictTypes(): array */ public function testDeclareStrictTypes(string $file, bool $result): void { - $this->processFile($file, function (Node $node, Scope $scope) use ($result): void { + self::processFile($file, function (Node $node, Scope $scope) use ($result): void { if (!($node instanceof Exit_)) { return; } @@ -9573,7 +9635,7 @@ public function testDeclareStrictTypes(string $file, bool $result): void public function testEarlyTermination(): void { - $this->processFile(__DIR__ . '/data/early-termination.php', function (Node $node, Scope $scope): void { + self::processFile(__DIR__ . '/data/early-termination.php', function (Node $node, Scope $scope): void { if (!($node instanceof Exit_)) { return; } @@ -9584,7 +9646,7 @@ public function testEarlyTermination(): void }); } - protected function getEarlyTerminatingMethodCalls(): array + protected static function getEarlyTerminatingMethodCalls(): array { return [ \EarlyTermination\Foo::class => [ @@ -9594,7 +9656,7 @@ protected function getEarlyTerminatingMethodCalls(): array ]; } - protected function getEarlyTerminatingFunctionCalls(): array + protected static function getEarlyTerminatingFunctionCalls(): array { return ['baz']; } @@ -9614,7 +9676,7 @@ private function assertTypeDescribe( } /** @return string[] */ - protected function getAdditionalAnalysedFiles(): array + protected static function getAdditionalAnalysedFiles(): array { return [ __DIR__ . '/data/methodPhpDocs-trait-defined.php', diff --git a/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php b/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php new file mode 100644 index 0000000000..83bb27b70b --- /dev/null +++ b/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php @@ -0,0 +1,40 @@ +> + */ + public function dataFileAsserts(): iterable + { + // compares constants according to the php-version phpstan configuration, + // _NOT_ the current php runtime version + yield from $this->gatherAssertTypes(__DIR__ . '/data/loose-const-comparison-php7.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/looseConstComparisonPhp7.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php b/tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php new file mode 100644 index 0000000000..e765ca01d5 --- /dev/null +++ b/tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php @@ -0,0 +1,40 @@ +> + */ + public function dataFileAsserts(): iterable + { + // compares constants according to the php-version phpstan configuration, + // _NOT_ the current php runtime version + yield from $this->gatherAssertTypes(__DIR__ . '/data/loose-const-comparison-php8.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/looseConstComparisonPhp8.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 22095c8e15..13b5e0b62c 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -22,10 +22,12 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/narrow_type_with_force_array.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/invalid_type.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/json_object_as_array.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-method-tags.php'); require_once __DIR__ . '/data/bug2574.php'; yield from $this->gatherAssertTypes(__DIR__ . '/data/bug2574.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-callables.php'); require_once __DIR__ . '/data/bug2577.php'; @@ -40,6 +42,9 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-class-string.php'); if (PHP_VERSION_ID >= 80100) { yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-enum-class-string.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7162.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10201.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10445.php'); } require_once __DIR__ . '/data/generic-generalization.php'; @@ -58,6 +63,10 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/strtotime-return-type-extensions.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-return-type-extensions.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-key.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6633.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10283.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10442.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/discussion-9972.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/intersection-static.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/static-properties.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/static-methods.php'); @@ -77,7 +86,22 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2750.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2850.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2863.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/native-types.php'); + + if (PHP_VERSION_ID >= 70400) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/native-types.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/reflection-type.php'); + } + + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/native-types-first-class-callables.php'); + } + + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/native-types-ftp-connect.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/native-types-ftp-connect-resource.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/type-change-after-array-access-assignment.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/iterator_to_array.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/key-of.php'); @@ -103,8 +127,10 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/inherit-phpdoc-merging-template.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3266.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3269.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5086.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/assign-nested-arrays.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3276.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-6856.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/shadowed-trait-methods.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/const-in-functions.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/const-in-functions-namespaced.php'); @@ -133,13 +159,22 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/../Reflection/data/staticReturnType.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/minmax-arrays.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/minmax.php'); + if (PHP_VERSION_ID < 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/minmax-arrays.php'); + } + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/minmax-php8.php'); + } yield from $this->gatherAssertTypes(__DIR__ . '/data/classPhpDocs.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-array-key-type.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3133.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Variables/data/bug-10577.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Variables/data/bug-10610.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-2550.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2899.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/preg_split.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-shape-list-optional.php'); if (PHP_VERSION_ID < 80000) { yield from $this->gatherAssertTypes(__DIR__ . '/data/bcmath-dynamic-return-php7.php'); @@ -148,8 +183,10 @@ public function dataFileAsserts(): iterable } yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3875.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10327.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2611.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3548.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10131.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3866.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1014.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-pr-339.php'); @@ -173,9 +210,14 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/count-type.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-sprintf.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/get-class-static-class.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2816.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2816-2.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10473.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3985.php'); @@ -191,6 +233,17 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3997.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4016.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8127.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7944.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6196.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7301.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9472.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9764.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10092.php'); + + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9084.php'); + } yield from $this->gatherAssertTypes(__DIR__ . '/data/promoted-properties-types.php'); @@ -199,12 +252,40 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3915.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2378.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/generics-do-not-generalize.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9985.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6294.php'); + + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/discussion-10285-php8.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/discussion-10285.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6462.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2580.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9753.php'); + + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9721.php'); + } + + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9734.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/enum-reflection.php'); + } + if (PHP_VERSION_ID >= 80200) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/enum-reflection-php82.php'); + } elseif (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/enum-reflection-php81.php'); + } yield from $this->gatherAssertTypes(__DIR__ . '/data/match-expr.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/nullsafe.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/specified-types-closure-use.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/specified-types-closure-edge.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/cast-to-numeric-string.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2539.php'); @@ -224,6 +305,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-empty-array.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4205.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/dependent-variable-certainty.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/dependent-expression-certainty.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1865.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/conditional-non-empty-array.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/foreach-dependent-key-value.php'); @@ -233,6 +315,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-801.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1209.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9784.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2980.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3986.php'); @@ -247,6 +330,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4343.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/impure-method.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/impure-constructor.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/pure-callable.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4351.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/var-above-use.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/var-above-declare.php'); @@ -257,6 +341,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4500.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4504.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4436.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10699.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Properties/data/bug-3777.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2549.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1945.php'); @@ -287,14 +372,22 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-arrow-functions.php'); } - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-flip.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-map.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-map-closure.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-merge.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-merge2.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-sum.php'); + + if (PHP_VERSION_ID >= 70400) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/param-closure-this.php'); + } yield from $this->gatherAssertTypes(__DIR__ . '/data/array-plus.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4573.php'); + + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9881.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4577.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4579.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3321.php'); @@ -364,6 +457,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-987.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3677.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4215.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10224.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4695.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2977.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3190.php'); @@ -457,6 +551,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5219.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/strval.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-next.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10566.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string-replace-functions.php'); @@ -491,6 +586,7 @@ public function dataFileAsserts(): iterable } yield from $this->gatherAssertTypes(__DIR__ . '/data/class-constant-types.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/class-implements.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3379.php'); @@ -508,7 +604,12 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5529.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/sizeof.php'); + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/sizeof-php8.php'); + } + if (PHP_VERSION_ID < 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/sizeof.php'); + } yield from $this->gatherAssertTypes(__DIR__ . '/data/div-by-zero.php'); @@ -517,6 +618,7 @@ public function dataFileAsserts(): iterable } yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5530.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1861.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/param-out-default.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4843.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4602.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4499.php'); @@ -531,6 +633,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/array-unshift.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array_map_multiple.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/range-numeric-string.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/range-int-range.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/missing-closure-native-return-typehint.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4741.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/more-type-strings.php'); @@ -568,6 +671,10 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/never.php'); + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10627.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/native-intersection.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2760.php'); @@ -588,6 +695,7 @@ public function dataFileAsserts(): iterable if (PHP_VERSION_ID >= 80100) { yield from $this->gatherAssertTypes(__DIR__ . '/data/array-is-list-type-specifying.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-is-list-unset.php'); } if (PHP_VERSION_ID >= 80100) { @@ -596,10 +704,23 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/filesystem-functions.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-input.php'); + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-input-php8.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-input-php7.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-input-array.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-var.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-var-array.php'); + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/constant.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/enums.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/enums-import-alias.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7176.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/in-array-enum.php'); } if (PHP_VERSION_ID >= 80000) { @@ -650,9 +771,16 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6404.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6399.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4357.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10863.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5817.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-chunk.php'); + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-chunk-php8.php'); + } + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-chunk-php81.php'); + } if (PHP_VERSION_ID < 80200) { yield from $this->gatherAssertTypes(__DIR__ . '/data/array-column.php'); @@ -719,7 +847,9 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6672.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6687.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/callable-in-union.php'); + if (PHP_VERSION_ID >= 70400) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/callable-in-union.php'); + } if (PHP_VERSION_ID < 80000) { yield from $this->gatherAssertTypes(__DIR__ . '/data/preg_match_php7.php'); @@ -808,8 +938,30 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6584.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/weird-strlen-cases.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6439.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-string-unions.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6748.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-fill-keys.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-flip.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-intersect-key.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-intersect-key-constant.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-search.php'); + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-fill-keys-php8.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-flip-php8.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-intersect-key-php8.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-search-php8.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array_keys.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array_values.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-fill-keys-php7.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-flip-php7.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-intersect-key-php7.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-search-php7.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array_keys-php7.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array_values-php7.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-search-type-specifying.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-pop.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-push.php'); @@ -817,6 +969,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/array-reverse.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6889.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6891.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10088.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/shuffle.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/simplexml.php'); @@ -824,14 +977,17 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6904.php'); } + if (PHP_VERSION_ID >= 80300) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/class-constant-native-type.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Constants/data/bug-10212.php'); + } + if (PHP_VERSION_ID >= 80000) { yield from $this->gatherAssertTypes(__DIR__ . '/data/array-combine-php8.php'); } else { yield from $this->gatherAssertTypes(__DIR__ . '/data/array-combine-php7.php'); } - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-fill-keys.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6917.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6936-limit.php'); @@ -863,7 +1019,9 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7078.php'); } - if (PHP_VERSION_ID >= 80200) { + if (PHP_VERSION_ID >= 80300) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/mb-strlen-php83.php'); + } elseif (PHP_VERSION_ID >= 80200) { yield from $this->gatherAssertTypes(__DIR__ . '/data/mb-strlen-php82.php'); } elseif (PHP_VERSION_ID >= 80000) { yield from $this->gatherAssertTypes(__DIR__ . '/data/mb-strlen-php8.php'); @@ -877,6 +1035,11 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7096.php'); } + if (PHP_VERSION_ID >= 80300) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/json-validate.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/return-type-class-constant.php'); + } + if (PHP_VERSION_ID >= 80100) { yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7167.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6864.php'); @@ -907,7 +1070,10 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/initializer-expr-type-resolver.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/offset-access.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/str-casing.php'); + + if (PHP_VERSION_ID >= 70300) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/str-casing.php'); + } yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string-substr-specifying.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/unset-conditional-expressions.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/conditional-types-inference.php'); @@ -926,7 +1092,9 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7031.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-array-intersect.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7153.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/in-array.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/in-array-non-empty.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/in-array-haystack-subtract.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4117.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7490.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/remember-possibly-impure-function-values.php'); @@ -972,9 +1140,11 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-7469.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Variables/data/bug-3391.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6901.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/var-in-and-out-of-function.php'); if (PHP_VERSION_ID >= 70400) { yield from $this->gatherAssertTypes(__DIR__ . '/data/arrow-function-argument-type.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-anonymous-function-method-constant.php'); } yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-argument-type.php'); @@ -994,6 +1164,7 @@ public function dataFileAsserts(): iterable } if (PHP_VERSION_ID >= 80000) { yield from $this->gatherAssertTypes(__DIR__ . '/data/loose-comparisons-php8.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10084.php'); } yield from $this->gatherAssertTypes(__DIR__ . '/data/loose-comparisons.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7563.php'); @@ -1001,7 +1172,6 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5845.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-flip-constant.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-constant.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/array-intersect-key-constant.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/composer-array-bug.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/tagged-unions.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7492.php'); @@ -1037,6 +1207,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/scope-generalization.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8015.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7993.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7996.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7141.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/cli-globals.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8033.php'); @@ -1044,7 +1215,6 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7987.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7963-three.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8017.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8004.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/global-namespace.php'); if (PHP_VERSION_ID >= 80000) { @@ -1056,11 +1226,258 @@ public function dataFileAsserts(): iterable } yield from $this->gatherAssertTypes(__DIR__ . '/data/array-offset-unset.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-constructor.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-docblock.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-empty.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-method.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-property.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-this.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-methods.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-intersected.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-invariant.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-conditional.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/docblock-assert-equality.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8008.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-class-type.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5552.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/extra-extra-int-types.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/list-count.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Properties/data/bug-7839.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/self-out.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/native-expressions.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Classes/data/bug-5333.php'); + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/allowed-subtypes-enum.php'); + } + + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10071.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9394.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-nullsafe-prop-static-access.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/allowed-subtypes-datetime.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/allowed-subtypes-throwable.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-8174.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-8169.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7519.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8087.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5785.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/callable-object.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/callable-string.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8225.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8242.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/composer-treatPhpDocTypesAsCertainBug.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-retain-expression-types.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7913.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-8280.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8272.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-8277.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/strtr.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/static-has-method.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/mixed-to-number.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Variables/data/bug-8113.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/phpunit-integration.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8361.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8373.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-8389.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8421.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/imagick-pixel.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-8467a.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8467b.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8442.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/PDOStatement.php'); + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-8485.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/discussion-8447.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/discussion-9134.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7805.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-82.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4565.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8249.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3789.php'); + + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8543.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8520.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10002.php'); + + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-9007.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-var-dynamic-return-type-extension-regression.php'); + + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/pathinfo-php8.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/always-true-elseif.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7547.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9341.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/pathinfo.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8568.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/DeadCode/data/bug-8620.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8635.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8625.php'); + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7239-php8.php'); + } + if (PHP_VERSION_ID < 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7239.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8621.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8084.php'); + + if (PHP_VERSION_ID >= 80300) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/str_increment.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/str_decrement.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3019.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/get-native-type.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/callsite-cast-narrowing.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8775.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8752.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-affected-rows.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-result-num-rows.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-stmt-affected-rows-and-num-rows.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/list-shapes.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3013.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7607.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10373.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/ibm_db2.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/benevolent-union-math.php'); + + if (PHP_VERSION_ID >= 80200) { + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Constants/data/bug-8957.php'); + } + + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8486.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9000.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/enum-from.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8956.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8917.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/ds-copy.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8803.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8827.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4907.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8924.php'); + + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-9499.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9939.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5998.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/trait-type-alias.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8609.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/PhpDoc/data/bug-8609-function.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9131.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/more-types.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/invalid-type-aliases.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/asymmetric-properties.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9062.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8092.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-5365.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-6551.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Variables/data/bug-9403.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/object-shape.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/rule-error-builder.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/memcache-get.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4302b.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/ini-get.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9274.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/extract.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/image-size.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/base64_decode.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9404.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/foreach-partially-non-iterable.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/globals.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9208.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/finite-types.php'); + + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5782b-php8.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5782b-php7.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/call-user-func.php'); + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/call-user-func-php8.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/call-user-func-php7.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/gettype.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array_splice.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-9542.php'); + + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/trigger-error-php8.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/trigger-error-php7.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/preserve-large-constant-array.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9397.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10080.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/impure-error-log.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/falsy-isset.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/falsey-coalesce.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/falsey-ternary-certainty.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7915.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9714.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9105.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5172.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9293.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/nullsafe-vs-scalar.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8517.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-9803.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/impure-connection-fns.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9963.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/str-shuffle.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9995.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/enum_exists.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9778.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9867.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/falsey-isset-certainty.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/falsey-empty-certainty.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8366.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7291.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10264.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/conditional-vars.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/sort.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3312.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5961.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10122.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10189.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10317.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10302-interface-extends.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10302-trait-extends.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10302-trait-implements.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-inheritance.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9123.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10037.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/PhpDoc/data/bug-10594.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/set-type-type-specifying.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli_fetch_object.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10468.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6613.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10187.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10834.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10952.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10952b.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10893.php'); } /** diff --git a/tests/PHPStan/Analyser/ParamClosureThisStubsTest.php b/tests/PHPStan/Analyser/ParamClosureThisStubsTest.php new file mode 100644 index 0000000000..795707aabb --- /dev/null +++ b/tests/PHPStan/Analyser/ParamClosureThisStubsTest.php @@ -0,0 +1,35 @@ +gatherAssertTypes(__DIR__ . '/data/param-closure-this-stubs.php'); + } + + /** + * @dataProvider dataAsserts + * @param mixed ...$args + */ + public function testAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/param-closure-this-stubs.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/ParamOutTypeTest.php b/tests/PHPStan/Analyser/ParamOutTypeTest.php new file mode 100644 index 0000000000..b29b6fbd0c --- /dev/null +++ b/tests/PHPStan/Analyser/ParamOutTypeTest.php @@ -0,0 +1,37 @@ +gatherAssertTypes(__DIR__ . '/data/param-out.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/typeAliases.neon', + __DIR__ . '/param-out.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/PathConstantsTest.php b/tests/PHPStan/Analyser/PathConstantsTest.php new file mode 100644 index 0000000000..c22864f698 --- /dev/null +++ b/tests/PHPStan/Analyser/PathConstantsTest.php @@ -0,0 +1,35 @@ +gatherAssertTypes(__DIR__ . '/data/pathConstants.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/usePathConstantsAsConstantString.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/ScopeTest.php b/tests/PHPStan/Analyser/ScopeTest.php index f32942c2aa..7ae319c0e3 100644 --- a/tests/PHPStan/Analyser/ScopeTest.php +++ b/tests/PHPStan/Analyser/ScopeTest.php @@ -139,7 +139,7 @@ public function dataGeneralize(): array new ConstantIntegerType(1), new ConstantIntegerType(1), ]), - 'array', + 'non-empty-array', ], [ new ConstantArrayType([ @@ -154,7 +154,7 @@ public function dataGeneralize(): array new ConstantIntegerType(1), new ConstantIntegerType(2), ]), - 'array>', + 'non-empty-array>', ], [ new UnionType([ @@ -232,8 +232,8 @@ public function testGeneralize(Type $a, Type $b, string $expectedTypeDescription { /** @var ScopeFactory $scopeFactory */ $scopeFactory = self::getContainer()->getByType(ScopeFactory::class); - $scopeA = $scopeFactory->create(ScopeContext::create('file.php'))->assignVariable('a', $a); - $scopeB = $scopeFactory->create(ScopeContext::create('file.php'))->assignVariable('a', $b); + $scopeA = $scopeFactory->create(ScopeContext::create('file.php'))->assignVariable('a', $a, $a); + $scopeB = $scopeFactory->create(ScopeContext::create('file.php'))->assignVariable('a', $b, $b); $resultScope = $scopeA->generalizeWith($scopeB); $this->assertSame($expectedTypeDescription, $resultScope->getVariableType('a')->describe(VerbosityLevel::precise())); } diff --git a/tests/PHPStan/Analyser/StatementResultTest.php b/tests/PHPStan/Analyser/StatementResultTest.php index 228ad689c6..fb3a9dd5b1 100644 --- a/tests/PHPStan/Analyser/StatementResultTest.php +++ b/tests/PHPStan/Analyser/StatementResultTest.php @@ -395,10 +395,10 @@ public function testIsAlwaysTerminating( /** @var ScopeFactory $scopeFactory */ $scopeFactory = self::getContainer()->getByType(ScopeFactory::class); $scope = $scopeFactory->create(ScopeContext::create('test.php')) - ->assignVariable('string', new StringType()) - ->assignVariable('x', new IntegerType()) - ->assignVariable('cond', new MixedType()) - ->assignVariable('arr', new ArrayType(new MixedType(), new MixedType())); + ->assignVariable('string', new StringType(), new StringType()) + ->assignVariable('x', new IntegerType(), new IntegerType()) + ->assignVariable('cond', new MixedType(), new MixedType()) + ->assignVariable('arr', new ArrayType(new MixedType(), new MixedType()), new ArrayType(new MixedType(), new MixedType())); $result = $nodeScopeResolver->processStmtNodes( new Stmt\Namespace_(null, $stmts), $stmts, diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index 052906f97f..245ba3d554 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -18,6 +18,7 @@ use PhpParser\Node\Scalar\String_; use PhpParser\Node\VarLikeIdentifier; use PhpParser\PrettyPrinter\Standard; +use PHPStan\Node\Expr\AlwaysRememberedExpr; use PHPStan\Node\Printer\Printer; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\ArrayType; @@ -37,6 +38,7 @@ use function sprintf; use const PHP_INT_MAX; use const PHP_INT_MIN; +use const PHP_VERSION_ID; class TypeSpecifierTest extends PHPStanTestCase { @@ -60,19 +62,20 @@ protected function setUp(): void $this->typeSpecifier = self::getContainer()->getService('typeSpecifier'); $this->scope = $this->createScopeFactory($reflectionProvider, $this->typeSpecifier)->create(ScopeContext::create('')); $this->scope = $this->scope->enterClass($reflectionProvider->getClass('DateTime')); - $this->scope = $this->scope->assignVariable('bar', new ObjectType('Bar')); - $this->scope = $this->scope->assignVariable('stringOrNull', new UnionType([new StringType(), new NullType()])); - $this->scope = $this->scope->assignVariable('string', new StringType()); - $this->scope = $this->scope->assignVariable('barOrNull', new UnionType([new ObjectType('Bar'), new NullType()])); - $this->scope = $this->scope->assignVariable('barOrFalse', new UnionType([new ObjectType('Bar'), new ConstantBooleanType(false)])); - $this->scope = $this->scope->assignVariable('stringOrFalse', new UnionType([new StringType(), new ConstantBooleanType(false)])); - $this->scope = $this->scope->assignVariable('array', new ArrayType(new MixedType(), new MixedType())); - $this->scope = $this->scope->assignVariable('foo', new MixedType()); - $this->scope = $this->scope->assignVariable('classString', new ClassStringType()); - $this->scope = $this->scope->assignVariable('genericClassString', new GenericClassStringType(new ObjectType('Bar'))); - $this->scope = $this->scope->assignVariable('object', new ObjectWithoutClassType()); - $this->scope = $this->scope->assignVariable('int', new IntegerType()); - $this->scope = $this->scope->assignVariable('float', new FloatType()); + $this->scope = $this->scope->assignVariable('bar', new ObjectType('Bar'), new ObjectType('Bar')); + $this->scope = $this->scope->assignVariable('stringOrNull', new UnionType([new StringType(), new NullType()]), new UnionType([new StringType(), new NullType()])); + $this->scope = $this->scope->assignVariable('string', new StringType(), new StringType()); + $this->scope = $this->scope->assignVariable('fooOrNull', new UnionType([new ObjectType('Foo'), new NullType()]), new UnionType([new ObjectType('Foo'), new NullType()])); + $this->scope = $this->scope->assignVariable('barOrNull', new UnionType([new ObjectType('Bar'), new NullType()]), new UnionType([new ObjectType('Bar'), new NullType()])); + $this->scope = $this->scope->assignVariable('barOrFalse', new UnionType([new ObjectType('Bar'), new ConstantBooleanType(false)]), new UnionType([new ObjectType('Bar'), new ConstantBooleanType(false)])); + $this->scope = $this->scope->assignVariable('stringOrFalse', new UnionType([new StringType(), new ConstantBooleanType(false)]), new UnionType([new StringType(), new ConstantBooleanType(false)])); + $this->scope = $this->scope->assignVariable('array', new ArrayType(new MixedType(), new MixedType()), new ArrayType(new MixedType(), new MixedType())); + $this->scope = $this->scope->assignVariable('foo', new MixedType(), new MixedType()); + $this->scope = $this->scope->assignVariable('classString', new ClassStringType(), new ClassStringType()); + $this->scope = $this->scope->assignVariable('genericClassString', new GenericClassStringType(new ObjectType('Bar')), new GenericClassStringType(new ObjectType('Bar'))); + $this->scope = $this->scope->assignVariable('object', new ObjectWithoutClassType(), new ObjectWithoutClassType()); + $this->scope = $this->scope->assignVariable('int', new IntegerType(), new IntegerType()); + $this->scope = $this->scope->assignVariable('float', new FloatType(), new FloatType()); } /** @@ -91,9 +94,41 @@ public function testCondition(Expr $expr, array $expectedPositiveResult, array $ $this->assertSame($expectedNegatedResult, $actualResult, sprintf('if not (%s)', $this->printer->prettyPrintExpr($expr))); } - public function dataCondition(): array + public function dataCondition(): iterable { - return [ + if (PHP_VERSION_ID >= 80100) { + yield [ + new Identical( + new PropertyFetch(new Variable('foo'), 'bar'), + new Expr\ClassConstFetch(new Name('Bug9499\\FooEnum'), 'A'), + ), + [ + '$foo->bar' => 'Bug9499\FooEnum::A', + ], + [ + '$foo->bar' => '~Bug9499\FooEnum::A', + ], + ]; + yield [ + new Identical( + new AlwaysRememberedExpr( + new PropertyFetch(new Variable('foo'), 'bar'), + new ObjectType('Bug9499\\FooEnum'), + new ObjectType('Bug9499\\FooEnum'), + ), + new Expr\ClassConstFetch(new Name('Bug9499\\FooEnum'), 'A'), + ), + [ + '__phpstanRembered($foo->bar)' => 'Bug9499\FooEnum::A', + '$foo->bar' => 'Bug9499\FooEnum::A', + ], + [ + '__phpstanRembered($foo->bar)' => '~Bug9499\FooEnum::A', + '$foo->bar' => '~Bug9499\FooEnum::A', + ], + ]; + } + yield from [ [ $this->createFunctionCall('is_int'), ['$foo' => 'int'], @@ -193,8 +228,8 @@ public function dataCondition(): array ]), new String_('Foo'), ), - ['$foo' => 'Foo'], - ['$foo' => '~Foo'], + ['$foo' => 'Foo', 'get_class($foo)' => '\'Foo\''], + ['get_class($foo)' => '~\'Foo\''], ], [ new Equal( @@ -203,8 +238,8 @@ public function dataCondition(): array new Arg(new Variable('foo')), ]), ), - ['$foo' => 'Foo'], - ['$foo' => '~Foo'], + ['$foo' => 'Foo', 'get_class($foo)' => '\'Foo\''], + ['get_class($foo)' => '~\'Foo\''], ], [ new BooleanNot( @@ -377,6 +412,14 @@ public function dataCondition(): array ['is_int($foo)' => 'true', '$foo' => 'int'], ['is_int($foo)' => '~true', '$foo' => '~int'], ], + [ + new Identical( + $this->createFunctionCall('is_string'), + new Expr\ConstFetch(new Name('true')), + ), + ['is_string($foo)' => 'true', '$foo' => 'string'], + ['is_string($foo)' => '~true', '$foo' => '~string'], + ], [ new Identical( $this->createFunctionCall('is_int'), @@ -528,6 +571,17 @@ public function dataCondition(): array ['$foo' => self::SURE_NOT_FALSEY], ['$foo' => self::SURE_NOT_TRUTHY], ], + [ + new Expr\Isset_([ + new Variable('stringOrNull'), + ]), + [ + '$stringOrNull' => '~null', + ], + [ + '$stringOrNull' => 'null', + ], + ], [ new Expr\Isset_([ new Variable('stringOrNull'), @@ -537,10 +591,20 @@ public function dataCondition(): array '$stringOrNull' => '~null', '$barOrNull' => '~null', ], + [], + ], + [ + new Expr\Isset_([ + new Variable('stringOrNull'), + new Variable('barOrNull'), + new Variable('fooOrNull'), + ]), [ - '$stringOrNull' => self::SURE_NOT_TRUTHY, - '$barOrNull' => self::SURE_NOT_TRUTHY, + '$stringOrNull' => '~null', + '$barOrNull' => '~null', + '$fooOrNull' => '~null', ], + [], ], [ new Expr\BooleanNot(new Expr\Empty_(new Variable('stringOrNull'))), @@ -564,7 +628,7 @@ public function dataCondition(): array [ new Expr\Empty_(new Variable('array')), [ - '$array' => 'array{}', + '$array' => 'array{}|null', ], [ '$array' => '~0|0.0|\'\'|\'0\'|array{}|false|null', @@ -576,7 +640,7 @@ public function dataCondition(): array '$array' => '~0|0.0|\'\'|\'0\'|array{}|false|null', ], [ - '$array' => 'array{}', + '$array' => 'array{}|null', ], ], [ @@ -710,9 +774,11 @@ public function dataCondition(): array ), [ '$notNullBar' => 'null', + '$barOrNull' => 'null', ], [ '$notNullBar' => '~null', + '$barOrNull' => '~null', ], ], [ @@ -832,9 +898,11 @@ public function dataCondition(): array ), [ '$notNullBar' => '~null', + '$barOrNull' => '~null', ], [ '$notNullBar' => 'null', + '$barOrNull' => 'null', ], ], [ @@ -859,9 +927,32 @@ public function dataCondition(): array ), [ '$notFalseBar' => 'false & ' . self::SURE_NOT_TRUTHY, + '$barOrFalse' => 'false', ], [ '$notFalseBar' => '~false', + '$barOrFalse' => '~false', + ], + ], + [ + new Identical( + new Expr\ConstFetch(new Name('null')), + new Expr\AssignOp\Coalesce( + new Variable('a'), + new Expr\Ternary( + new Variable('b'), + new Variable('b'), + new Expr\ConstFetch( + new Name('null'), + ), + ), + ), + ), + [ + '$a' => 'null', + ], + [ + '$a' => '~null', ], ], [ @@ -886,9 +977,11 @@ public function dataCondition(): array ), [ '$notFalseBar' => '~false', + '$barOrFalse' => '~false', ], [ '$notFalseBar' => 'false & ' . self::SURE_NOT_TRUTHY, + '$barOrFalse' => 'false', ], ], [ @@ -901,9 +994,11 @@ public function dataCondition(): array ), [ '$notFalseBar' => 'Bar', + '$barOrFalse' => 'Bar', ], [ '$notFalseBar' => '~Bar', + '$barOrFalse' => '~Bar', ], ], [ diff --git a/tests/PHPStan/Analyser/assert-stub.neon b/tests/PHPStan/Analyser/assert-stub.neon new file mode 100644 index 0000000000..6c5d7b7d60 --- /dev/null +++ b/tests/PHPStan/Analyser/assert-stub.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - data/assert.stub diff --git a/tests/PHPStan/Analyser/bug-10922.neon b/tests/PHPStan/Analyser/bug-10922.neon new file mode 100644 index 0000000000..3ee516d3be --- /dev/null +++ b/tests/PHPStan/Analyser/bug-10922.neon @@ -0,0 +1,2 @@ +parameters: + polluteScopeWithAlwaysIterableForeach: false diff --git a/tests/PHPStan/Analyser/bug-9307.neon b/tests/PHPStan/Analyser/bug-9307.neon new file mode 100644 index 0000000000..c551b84f1f --- /dev/null +++ b/tests/PHPStan/Analyser/bug-9307.neon @@ -0,0 +1,2 @@ +parameters: + treatPhpDocTypesAsCertain: false diff --git a/tests/PHPStan/Analyser/data/AnonymousClassesWithComments.php b/tests/PHPStan/Analyser/data/AnonymousClassesWithComments.php index 3ee8656661..b66f99e76b 100644 --- a/tests/PHPStan/Analyser/data/AnonymousClassesWithComments.php +++ b/tests/PHPStan/Analyser/data/AnonymousClassesWithComments.php @@ -1,7 +1,7 @@ modify($modify)); - assertType('DateTimeImmutable|false', $dateTimeImmutable->modify($modify)); + assertType('(DateTime|false)', $datetime->modify($modify)); + assertType('(DateTimeImmutable|false)', $dateTimeImmutable->modify($modify)); } /** @@ -32,8 +33,8 @@ public function modifyWithInvalidConstant(DateTime $datetime, DateTimeImmutable * @param '+1 day'|'koko' $modify */ public function modifyWithBothConstant(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void { - assertType('DateTime|false', $datetime->modify($modify)); - assertType('DateTimeImmutable|false', $dateTimeImmutable->modify($modify)); + assertType('(DateTime|false)', $datetime->modify($modify)); + assertType('(DateTimeImmutable|false)', $dateTimeImmutable->modify($modify)); } } diff --git a/tests/PHPStan/Analyser/data/MethodCallReturnsBoolExpressionTypeResolverExtension.php b/tests/PHPStan/Analyser/data/MethodCallReturnsBoolExpressionTypeResolverExtension.php new file mode 100644 index 0000000000..fa6fb0a43a --- /dev/null +++ b/tests/PHPStan/Analyser/data/MethodCallReturnsBoolExpressionTypeResolverExtension.php @@ -0,0 +1,46 @@ +name instanceof Identifier) { + return null; + } + + if ($expr->name->name !== 'methodReturningBoolNoMatterTheCallerUnlessReturnsString') { + return null; + } + + $methodReflection = $scope->getMethodReflection($scope->getType($expr->var), $expr->name->name); + + if ($methodReflection === null) { + return null; + } + + $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + + if ($returnType instanceof StringType) { + return null; + } + + return new BooleanType(); + } + +} diff --git a/tests/PHPStan/Analyser/data/PDOStatement.php b/tests/PHPStan/Analyser/data/PDOStatement.php new file mode 100644 index 0000000000..d9e930061e --- /dev/null +++ b/tests/PHPStan/Analyser/data/PDOStatement.php @@ -0,0 +1,22 @@ +fetchObject(Bar::class); + assertType('PDOStatement\Bar|false', $bar); + + $bar = $statement->fetchObject(); + assertType('stdClass|false', $bar); + } + +} + diff --git a/tests/PHPStan/Analyser/data/allowed-subtypes-datetime.php b/tests/PHPStan/Analyser/data/allowed-subtypes-datetime.php new file mode 100644 index 0000000000..46c43086e0 --- /dev/null +++ b/tests/PHPStan/Analyser/data/allowed-subtypes-datetime.php @@ -0,0 +1,17 @@ + $negativeRange + * @param int<-5, 0> $negativeWithZero + */ + public function negativeLength(array $arr, $negativeRange, $negativeWithZero) { + assertType('*NEVER*', array_chunk($arr, $negativeRange)); + assertType('*NEVER*', array_chunk($arr, $negativeWithZero)); + } + +} diff --git a/tests/PHPStan/Analyser/data/array-chunk-php81.php b/tests/PHPStan/Analyser/data/array-chunk-php81.php new file mode 100644 index 0000000000..5009d428b1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-chunk-php81.php @@ -0,0 +1,30 @@ +>', array_chunk($arr, 2)); - assertType('array', array_chunk($arr, 2, true)); + assertType('list>', array_chunk($arr, 2)); + assertType('list', array_chunk($arr, 2, true)); /** @var array $arr */ - assertType('array>', array_chunk($arr, 2)); - assertType('array>', array_chunk($arr, 2, true)); + assertType('list>', array_chunk($arr, 2)); + assertType('list>', array_chunk($arr, 2, true)); /** @var non-empty-array $arr */ - assertType('non-empty-array>', array_chunk($arr, 1)); - assertType('non-empty-array>', array_chunk($arr, 1, true)); + assertType('non-empty-list>', array_chunk($arr, 1)); + assertType('non-empty-list>', array_chunk($arr, 1, true)); } @@ -43,4 +43,36 @@ public function constantArraysWithOptionalKeys(array $arr): void assertType('array{array{a?: 0, b?: 1, c?: 2}, array{c?: 2}}', array_chunk($arr, 2, true)); } + /** + * @param int<2, 3> $positiveRange + * @param 2|3 $positiveUnion + */ + public function chunkUnionTypeLength(array $arr, $positiveRange, $positiveUnion) { + /** @var array{a: 0, b?: 1, c: 2} $arr */ + assertType('array{0: array{0: 0, 1?: 1|2, 2?: 2}, 1?: array{0?: 2}}', array_chunk($arr, $positiveRange)); + assertType('array{0: array{a: 0, b?: 1, c?: 2}, 1?: array{c?: 2}}', array_chunk($arr, $positiveRange, true)); + assertType('array{0: array{0: 0, 1?: 1|2, 2?: 2}, 1?: array{0?: 2}}', array_chunk($arr, $positiveUnion)); + assertType('array{0: array{a: 0, b?: 1, c?: 2}, 1?: array{c?: 2}}', array_chunk($arr, $positiveUnion, true)); + } + + /** + * @param positive-int $positiveInt + * @param int<50, max> $bigger50 + */ + public function lengthIntRanges(array $arr, int $positiveInt, int $bigger50) { + assertType('list>', array_chunk($arr, $positiveInt)); + assertType('list>', array_chunk($arr, $bigger50)); + } + + /** + * @param int<1, 4> $oneToFour + * @param int<1, 5> $tooBig + */ + function testLimits(array $arr, int $oneToFour, int $tooBig) { + /** @var array{a: 0, b?: 1, c: 2, d: 3} $arr */ + assertType('array{0: array{0: 0, 1?: 1|2, 2?: 2|3, 3?: 3}, 1?: array{0?: 2|3, 1?: 3}}|array{array{0}, array{0?: 1|2, 1?: 2}, array{0?: 2|3, 1?: 3}, array{0?: 3}}', array_chunk($arr, $oneToFour)); + assertType('non-empty-list>', array_chunk($arr, $tooBig)); + } + + } diff --git a/tests/PHPStan/Analyser/data/array-column-php82.php b/tests/PHPStan/Analyser/data/array-column-php82.php index 61d25ed694..b700980a37 100644 --- a/tests/PHPStan/Analyser/data/array-column-php82.php +++ b/tests/PHPStan/Analyser/data/array-column-php82.php @@ -1,6 +1,6 @@ > $array */ public function testArray1(array $array): void { - assertType('array', array_column($array, 'column')); + assertType('list', array_column($array, 'column')); assertType('array', array_column($array, 'column', 'key')); assertType('array>', array_column($array, null, 'key')); } @@ -21,7 +21,7 @@ public function testArray1(array $array): void public function testArray2(array $array): void { // Note: Array may still be empty! - assertType('array', array_column($array, 'column')); + assertType('list', array_column($array, 'column')); } /** @param array{} $array */ @@ -65,7 +65,7 @@ public function testArray8(array $array): void /** @param array $array */ public function testConstantArray1(array $array): void { - assertType('array', array_column($array, 'column')); + assertType('list', array_column($array, 'column')); assertType('array', array_column($array, 'column', 'key')); assertType('array', array_column($array, null, 'key')); } @@ -95,7 +95,7 @@ public function testConstantArray4(array $array): void /** @param array $array */ public function testConstantArray5(array $array): void { - assertType("array", array_column($array, 'column')); + assertType("list<'foo'>", array_column($array, 'column')); assertType("array<'bar'|int, 'foo'>", array_column($array, 'column', 'key')); assertType("array<'bar'|int, array{column?: 'foo', key?: 'bar'}>", array_column($array, null, 'key')); } @@ -103,13 +103,13 @@ public function testConstantArray5(array $array): void /** @param array $array */ public function testConstantArray6(array $array): void { - assertType('array', array_column($array, mt_rand(0, 1) === 0 ? 'column1' : 'column2')); + assertType('list', array_column($array, mt_rand(0, 1) === 0 ? 'column1' : 'column2')); } /** @param non-empty-array $array */ public function testConstantArray7(array $array): void { - assertType('non-empty-array', array_column($array, 'column')); + assertType('non-empty-list', array_column($array, 'column')); assertType('non-empty-array', array_column($array, 'column', 'key')); assertType('non-empty-array', array_column($array, null, 'key')); } @@ -150,7 +150,7 @@ public function testConstantArray12(array $array): void /** @param array{array{column?: 'foo', key: 'bar'}} $array */ public function testImprecise1(array $array): void { - assertType("array", array_column($array, 'column')); + assertType("list<'foo'>", array_column($array, 'column')); assertType("array<'bar', 'foo'>", array_column($array, 'column', 'key')); assertType("array{bar: array{column?: 'foo', key: 'bar'}}", array_column($array, null, 'key')); } @@ -165,18 +165,18 @@ public function testImprecise2(array $array): void /** @param array{array{column: 'foo', key: 'bar'}}|array> $array */ public function testImprecise3(array $array): void { - assertType('array', array_column($array, 'column')); + assertType('list', array_column($array, 'column')); assertType('array', array_column($array, 'column', 'key')); } /** @param array $array */ public function testImprecise5(array $array): void { - assertType('array', array_column($array, 'nodeName')); + assertType('list', array_column($array, 'nodeName')); assertType('array', array_column($array, 'nodeName', 'tagName')); assertType('array', array_column($array, null, 'tagName')); - assertType('array{}', array_column($array, 'foo')); - assertType('array{}', array_column($array, 'foo', 'tagName')); + assertType('list', array_column($array, 'foo')); + assertType('array', array_column($array, 'foo', 'tagName')); assertType('array', array_column($array, 'nodeName', 'foo')); assertType('array', array_column($array, null, 'foo')); } @@ -184,11 +184,11 @@ public function testImprecise5(array $array): void /** @param non-empty-array $array */ public function testObjects1(array $array): void { - assertType('non-empty-array', array_column($array, 'nodeName')); + assertType('non-empty-list', array_column($array, 'nodeName')); assertType('non-empty-array', array_column($array, 'nodeName', 'tagName')); assertType('non-empty-array', array_column($array, null, 'tagName')); - assertType('array{}', array_column($array, 'foo')); - assertType('array{}', array_column($array, 'foo', 'tagName')); + assertType('list', array_column($array, 'foo')); + assertType('array', array_column($array, 'foo', 'tagName')); assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); assertType('non-empty-array', array_column($array, null, 'foo')); } @@ -199,10 +199,22 @@ public function testObjects2(array $array): void assertType('array{string}', array_column($array, 'nodeName')); assertType('non-empty-array', array_column($array, 'nodeName', 'tagName')); assertType('non-empty-array', array_column($array, null, 'tagName')); - assertType('array{*NEVER*}', array_column($array, 'foo')); - assertType('non-empty-array', array_column($array, 'foo', 'tagName')); + assertType('list', array_column($array, 'foo')); + assertType('array', array_column($array, 'foo', 'tagName')); assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); assertType('non-empty-array', array_column($array, null, 'foo')); } } + +final class Foo +{ + + /** @param array $a */ + public function doFoo(array $a): void + { + assertType('array{}', array_column($a, 'nodeName')); + assertType('array{}', array_column($a, 'nodeName', 'tagName')); + } + +} diff --git a/tests/PHPStan/Analyser/data/array-column.php b/tests/PHPStan/Analyser/data/array-column.php index 337744312a..2bf6c929ba 100644 --- a/tests/PHPStan/Analyser/data/array-column.php +++ b/tests/PHPStan/Analyser/data/array-column.php @@ -12,7 +12,7 @@ class ArrayColumnTest /** @param array> $array */ public function testArray1(array $array): void { - assertType('array', array_column($array, 'column')); + assertType('list', array_column($array, 'column')); assertType('array', array_column($array, 'column', 'key')); assertType('array>', array_column($array, null, 'key')); } @@ -21,7 +21,7 @@ public function testArray1(array $array): void public function testArray2(array $array): void { // Note: Array may still be empty! - assertType('array', array_column($array, 'column')); + assertType('list', array_column($array, 'column')); } /** @param array{} $array */ @@ -65,7 +65,7 @@ public function testArray8(array $array): void /** @param array $array */ public function testConstantArray1(array $array): void { - assertType('array', array_column($array, 'column')); + assertType('list', array_column($array, 'column')); assertType('array', array_column($array, 'column', 'key')); assertType('array', array_column($array, null, 'key')); } @@ -95,7 +95,7 @@ public function testConstantArray4(array $array): void /** @param array $array */ public function testConstantArray5(array $array): void { - assertType("array", array_column($array, 'column')); + assertType("list<'foo'>", array_column($array, 'column')); assertType("array<'bar'|int, 'foo'>", array_column($array, 'column', 'key')); assertType("array<'bar'|int, array{column?: 'foo', key?: 'bar'}>", array_column($array, null, 'key')); } @@ -103,13 +103,13 @@ public function testConstantArray5(array $array): void /** @param array $array */ public function testConstantArray6(array $array): void { - assertType('array', array_column($array, mt_rand(0, 1) === 0 ? 'column1' : 'column2')); + assertType('list', array_column($array, mt_rand(0, 1) === 0 ? 'column1' : 'column2')); } /** @param non-empty-array $array */ public function testConstantArray7(array $array): void { - assertType('non-empty-array', array_column($array, 'column')); + assertType('non-empty-list', array_column($array, 'column')); assertType('non-empty-array', array_column($array, 'column', 'key')); assertType('non-empty-array', array_column($array, null, 'key')); } @@ -164,7 +164,7 @@ public function testConstantArray14(array $array): void /** @param array{array{column?: 'foo', key: 'bar'}} $array */ public function testImprecise1(array $array): void { - assertType("array", array_column($array, 'column')); + assertType("list<'foo'>", array_column($array, 'column')); assertType("array<'bar', 'foo'>", array_column($array, 'column', 'key')); assertType("array{bar: array{column?: 'foo', key: 'bar'}}", array_column($array, null, 'key')); } @@ -179,17 +179,17 @@ public function testImprecise2(array $array): void /** @param array{array{column: 'foo', key: 'bar'}}|array> $array */ public function testImprecise3(array $array): void { - assertType('array', array_column($array, 'column')); + assertType('list', array_column($array, 'column')); assertType('array', array_column($array, 'column', 'key')); } /** @param array $array */ public function testImprecise5(array $array): void { - assertType('array', array_column($array, 'nodeName')); + assertType('list', array_column($array, 'nodeName')); assertType('array', array_column($array, 'nodeName', 'tagName')); assertType('array', array_column($array, null, 'tagName')); - assertType('array', array_column($array, 'foo')); + assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); assertType('array', array_column($array, 'nodeName', 'foo')); assertType('array', array_column($array, null, 'foo')); @@ -198,10 +198,10 @@ public function testImprecise5(array $array): void /** @param non-empty-array $array */ public function testObjects1(array $array): void { - assertType('non-empty-array', array_column($array, 'nodeName')); + assertType('non-empty-list', array_column($array, 'nodeName')); assertType('non-empty-array', array_column($array, 'nodeName', 'tagName')); assertType('non-empty-array', array_column($array, null, 'tagName')); - assertType('array', array_column($array, 'foo')); + assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); assertType('non-empty-array', array_column($array, null, 'foo')); @@ -213,10 +213,22 @@ public function testObjects2(array $array): void assertType('array{string}', array_column($array, 'nodeName')); assertType('non-empty-array', array_column($array, 'nodeName', 'tagName')); assertType('non-empty-array', array_column($array, null, 'tagName')); - assertType('array', array_column($array, 'foo')); + assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); assertType('non-empty-array', array_column($array, null, 'foo')); } } + +final class Foo +{ + + /** @param array $a */ + public function doFoo(array $a): void + { + assertType('list', array_column($a, 'nodeName')); + assertType('array', array_column($a, 'nodeName', 'tagName')); + } + +} diff --git a/tests/PHPStan/Analyser/data/array-combine-php7.php b/tests/PHPStan/Analyser/data/array-combine-php7.php index e40184184b..0ff9f4399b 100644 --- a/tests/PHPStan/Analyser/data/array-combine-php7.php +++ b/tests/PHPStan/Analyser/data/array-combine-php7.php @@ -78,3 +78,8 @@ function withNonEmptyArray(array $a, array $b): void { assertType("non-empty-array<'bar'|'baz'|'foo', 'apple'|'avocado'|'banana'>|false", array_combine($a, $b)); } + +function withDifferentNumberOfElements(): void +{ + assertType('false', array_combine(['foo'], ['bar', 'baz'])); +} diff --git a/tests/PHPStan/Analyser/data/array-combine-php8.php b/tests/PHPStan/Analyser/data/array-combine-php8.php index 18c0eb6ca2..77b362498b 100644 --- a/tests/PHPStan/Analyser/data/array-combine-php8.php +++ b/tests/PHPStan/Analyser/data/array-combine-php8.php @@ -78,3 +78,8 @@ function withNonEmptyArray(array $a, array $b): void { assertType("non-empty-array<'bar'|'baz'|'foo', 'apple'|'avocado'|'banana'>", array_combine($a, $b)); } + +function withDifferentNumberOfElements(): void +{ + assertType('*NEVER*', array_combine(['foo'], ['bar', 'baz'])); +} diff --git a/tests/PHPStan/Analyser/data/array-fill-keys-php7.php b/tests/PHPStan/Analyser/data/array-fill-keys-php7.php new file mode 100644 index 0000000000..80f3f43f7f --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-fill-keys-php7.php @@ -0,0 +1,14 @@ +", array_fill_keys($mixed, 'b')); + } else { + assertType("null", array_fill_keys($mixed, 'b')); + } +} diff --git a/tests/PHPStan/Analyser/data/array-fill-keys-php8.php b/tests/PHPStan/Analyser/data/array-fill-keys-php8.php new file mode 100644 index 0000000000..7782a204ce --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-fill-keys-php8.php @@ -0,0 +1,14 @@ +", array_fill_keys($mixed, 'b')); + } else { + assertType("*NEVER*", array_fill_keys($mixed, 'b')); + } +} diff --git a/tests/PHPStan/Analyser/data/array-fill-keys.php b/tests/PHPStan/Analyser/data/array-fill-keys.php index fcf82d1094..5054186320 100644 --- a/tests/PHPStan/Analyser/data/array-fill-keys.php +++ b/tests/PHPStan/Analyser/data/array-fill-keys.php @@ -48,7 +48,7 @@ function withObjectKey() : array { assertType("array{foo: 'b'}", array_fill_keys([new Foo()], 'b')); assertType("non-empty-array", array_fill_keys([new Bar()], 'b')); - assertType("*NEVER*", array_fill_keys([new Baz()], 'b')); + assertType("*ERROR*", array_fill_keys([new Baz()], 'b')); } function withUnionKeys(): void @@ -71,6 +71,10 @@ function withOptionalKeys(): void $arr1[] = 'baz'; } assertType("array{foo: 'b', bar: 'b', baz?: 'b'}", array_fill_keys($arr1, 'b')); + + /** @var array{0?: 'foo', 1: 'bar', }|array{0: 'baz', 1?: 'foobar'} $arr2 */ + $arr2 = []; + assertType("array{baz: 'b', foobar?: 'b'}|array{foo?: 'b', bar: 'b'}", array_fill_keys($arr2, 'b')); } /** @@ -79,12 +83,24 @@ function withOptionalKeys(): void * @param Foo[] $baz * @param float[] $floats * @param array $mixed + * @param list $list + * @param Baz[] $objectsWithoutToString */ -function withNotConstantArray(array $foo, array $bar, array $baz, array $floats, array $mixed): void +function withNotConstantArray(array $foo, array $bar, array $baz, array $floats, array $mixed, array $list, array $objectsWithoutToString): void { assertType("array", array_fill_keys($foo, null)); assertType("array", array_fill_keys($bar, null)); assertType("array<'foo', null>", array_fill_keys($baz, null)); assertType("array", array_fill_keys($floats, null)); assertType("array", array_fill_keys($mixed, null)); + assertType('array', array_fill_keys($list, null)); + assertType('*ERROR*', array_fill_keys($objectsWithoutToString, null)); + + if (array_key_exists(17, $mixed)) { + assertType('non-empty-array', array_fill_keys($mixed, null)); + } + + if (array_key_exists(17, $mixed) && $mixed[17] === 'foo') { + assertType('non-empty-array', array_fill_keys($mixed, null)); + } } diff --git a/tests/PHPStan/Analyser/data/array-filter-arrow-functions.php b/tests/PHPStan/Analyser/data/array-filter-arrow-functions.php index 0c39643fc9..8b3b17f477 100644 --- a/tests/PHPStan/Analyser/data/array-filter-arrow-functions.php +++ b/tests/PHPStan/Analyser/data/array-filter-arrow-functions.php @@ -1,6 +1,6 @@ = 7.4 -namespace ArrayFilter; +namespace ArrayFilterArrowFunctions; use function PHPStan\Testing\assertType; diff --git a/tests/PHPStan/Analyser/data/array-filter-callables.php b/tests/PHPStan/Analyser/data/array-filter-callables.php index 6855d1fe92..3ac42c8790 100644 --- a/tests/PHPStan/Analyser/data/array-filter-callables.php +++ b/tests/PHPStan/Analyser/data/array-filter-callables.php @@ -1,6 +1,6 @@ ', array_flip($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('null', array_flip($mixed)); + } +} diff --git a/tests/PHPStan/Analyser/data/array-flip-php8.php b/tests/PHPStan/Analyser/data/array-flip-php8.php new file mode 100644 index 0000000000..fb0817f178 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-flip-php8.php @@ -0,0 +1,15 @@ +', array_flip($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('*NEVER*', array_flip($mixed)); + } +} diff --git a/tests/PHPStan/Analyser/data/array-flip.php b/tests/PHPStan/Analyser/data/array-flip.php index 2275170d0d..2f02f1e733 100644 --- a/tests/PHPStan/Analyser/data/array-flip.php +++ b/tests/PHPStan/Analyser/data/array-flip.php @@ -41,3 +41,55 @@ function foo5($array) $flip = array_flip($array); assertType('array', $flip); } + +/** + * @param non-empty-array<1|2|3, 4|5|6> $array + */ +function foo6($array) +{ + $flip = array_flip($array); + assertType('non-empty-array<4|5|6, 1|2|3>', $flip); +} + +/** + * @param list<1|2|3> $array + */ +function foo7($array) +{ + $flip = array_flip($array); + assertType('array<1|2|3, int<0, max>>', $flip); +} + +function foo8($mixed) +{ + assertType('mixed', $mixed); + $mixed = array_flip($mixed); + assertType('array', $mixed); +} + +/** @param array $array */ +function foo10(array $array) +{ + if (array_key_exists('foo', $array)) { + assertType('array&hasOffset(\'foo\')', $array); + assertType('array', array_flip($array)); + } + + if (array_key_exists('foo', $array) && is_int($array['foo'])) { + assertType("array&hasOffsetValue('foo', int)", $array); + assertType('array', array_flip($array)); + } + + if (array_key_exists('foo', $array) && $array['foo'] === 17) { + assertType("array&hasOffsetValue('foo', 17)", $array); + assertType("array&hasOffsetValue(17, 'foo')", array_flip($array)); + } + + if ( + array_key_exists('foo', $array) && $array['foo'] === 17 + && array_key_exists('bar', $array) && $array['bar'] === 17 + ) { + assertType("array&hasOffsetValue('bar', 17)&hasOffsetValue('foo', 17)", $array); + assertType("*NEVER*", array_flip($array)); // this could be array&hasOffsetValue(17, 'bar') according to https://3v4l.org/1TAFk + } +} diff --git a/tests/PHPStan/Analyser/data/array-intersect-key-php7.php b/tests/PHPStan/Analyser/data/array-intersect-key-php7.php new file mode 100644 index 0000000000..002db78bb0 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-intersect-key-php7.php @@ -0,0 +1,27 @@ + $otherArrs */ + assertType('array', array_intersect_key($mixed, $otherArrs)); + /** @var array $otherArrs */ + assertType('array', array_intersect_key($mixed, $otherArrs)); + } else { + assertType('mixed~array', $mixed); + /** @var array $otherArrs */ + assertType('null', array_intersect_key($mixed, $otherArrs)); + /** @var array $otherArrs */ + assertType('null', array_intersect_key($mixed, $otherArrs)); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/array-intersect-key-php8.php b/tests/PHPStan/Analyser/data/array-intersect-key-php8.php new file mode 100644 index 0000000000..53f8c7c9b5 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-intersect-key-php8.php @@ -0,0 +1,27 @@ + $otherArrs */ + assertType('array', array_intersect_key($mixed, $otherArrs)); + /** @var array $otherArrs */ + assertType('array', array_intersect_key($mixed, $otherArrs)); + } else { + assertType('mixed~array', $mixed); + /** @var array $otherArrs */ + assertType('*NEVER*', array_intersect_key($mixed, $otherArrs)); + /** @var array $otherArrs */ + assertType('*NEVER*', array_intersect_key($mixed, $otherArrs)); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/array-intersect-key.php b/tests/PHPStan/Analyser/data/array-intersect-key.php new file mode 100644 index 0000000000..5d17736266 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-intersect-key.php @@ -0,0 +1,91 @@ + $arr + * @param non-empty-array $arr2 + */ + public function nonEmpty(array $arr, array $arr2): void + { + assertType('non-empty-array', array_intersect_key($arr)); + assertType('non-empty-array', array_intersect_key($arr, $arr)); + assertType('array', array_intersect_key($arr, $arr2)); + assertType('non-empty-array', array_intersect_key($arr2, $arr)); + assertType('array{}', array_intersect_key($arr, [])); + assertType("array<'foo', string>", array_intersect_key($arr, ['foo' => 17])); + } + + + /** + * @param array $arr + * @param array $arr2 + */ + public function normalArrays(array $arr, array $arr2, array $otherArrs): void + { + assertType('array', array_intersect_key($arr)); + assertType('array', array_intersect_key($arr, $arr)); + assertType('array', array_intersect_key($arr, $arr2)); + assertType('array', array_intersect_key($arr2, $arr)); + assertType('array{}', array_intersect_key($arr, [])); + assertType("array<'foo', string>", array_intersect_key($arr, ['foo' => 17])); + + /** @var array $otherArrs */ + assertType('array', array_intersect_key($arr, $otherArrs)); + /** @var array $otherArrs */ + assertType('array', array_intersect_key($arr, $otherArrs)); + /** @var array $otherArrs */ + assertType('array', array_intersect_key($arr, $otherArrs)); + /** @var array<17, int> $otherArrs */ + assertType('array<17, string>', array_intersect_key($arr, $otherArrs)); + /** @var array $otherArrs */ + assertType('array{}', array_intersect_key($arr, $otherArrs)); + + if (array_key_exists(17, $arr2)) { + assertType('array<17, string>&hasOffset(17)', array_intersect_key($arr2, [17 => 'bar'])); + /** @var array $otherArrs */ + assertType('array', array_intersect_key($arr2, $otherArrs)); + /** @var array $otherArrs */ + assertType('array{}', array_intersect_key($arr2, $otherArrs)); + } + + if (array_key_exists(17, $arr2) && $arr2[17] === 'foo') { + assertType("array<17, string>&hasOffsetValue(17, 'foo')", array_intersect_key($arr2, [17 => 'bar'])); + /** @var array $otherArrs */ + assertType('array', array_intersect_key($arr2, $otherArrs)); + /** @var array $otherArrs */ + assertType('array{}', array_intersect_key($arr2, $otherArrs)); + } + } + + /** + * @param list> $arrs + * @param list> $arrs2 + */ + public function arrayUnpacking(array $arrs, array $arrs2): void + { + assertType('array', array_intersect_key(...$arrs)); + assertType('array', array_intersect_key(...$arrs2)); + assertType('array', array_intersect_key(...$arrs, ...$arrs2)); + assertType('array', array_intersect_key(...$arrs2, ...$arrs)); + } + + /** @param list $arr */ + public function list(array $arr, array $otherArrs): void + { + assertType('array<0|1, string>&list', array_intersect_key($arr, ['foo', 'bar'])); + /** @var array $otherArrs */ + assertType('array, string>', array_intersect_key($arr, $otherArrs)); + /** @var array $otherArrs */ + assertType('array{}', array_intersect_key($arr, $otherArrs)); + /** @var list $otherArrs */ + assertType('list', array_intersect_key($arr, $otherArrs)); + } + +} diff --git a/tests/PHPStan/Analyser/data/array-is-list-type-specifying.php b/tests/PHPStan/Analyser/data/array-is-list-type-specifying.php index d3779c9f66..aa21248ec6 100644 --- a/tests/PHPStan/Analyser/data/array-is-list-type-specifying.php +++ b/tests/PHPStan/Analyser/data/array-is-list-type-specifying.php @@ -6,12 +6,38 @@ function foo(array $foo) { if (array_is_list($foo)) { - assertType('array', $foo); + assertType('list', $foo); } else { assertType('array', $foo); } } +function foo2($foo) { + if (array_is_list($foo)) { + assertType('list', $foo); + } else { + assertType('mixed~list', $foo); + } +} + +/** @param array{'foo', 'bar'}|array{'baz', 'foobar'} $foo */ +function foo3(array $foo) { + if (array_is_list($foo)) { + assertType("array{'baz', 'foobar'}|array{'foo', 'bar'}", $foo); + } else { + assertType('*NEVER*', $foo); + } +} + +/** @param array{foo: 0, bar: 1}|array{baz: 3, foobar: 4} $foo */ +function foo4(array $foo) { + if (array_is_list($foo)) { + assertType('*NEVER*', $foo); + } else { + assertType('array{baz: 3, foobar: 4}|array{foo: 0, bar: 1}', $foo); + } +} + $bar = [1, 2, 3]; if (array_is_list($bar)) { @@ -23,7 +49,7 @@ function foo(array $foo) { /** @var array $foo */ if (array_is_list($foo)) { - assertType('array', $foo); + assertType('list', $foo); } else { assertType('array', $foo); } diff --git a/tests/PHPStan/Analyser/data/array-is-list-unset.php b/tests/PHPStan/Analyser/data/array-is-list-unset.php new file mode 100644 index 0000000000..b14b8c97df --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-is-list-unset.php @@ -0,0 +1,26 @@ + $a * @return void */ - public function doFoo(array $a, string $key): void + public function doFoo(array $a, string $key, int $anotherKey): void { assertType('false', array_key_exists(2, $a)); assertType('bool', array_key_exists('foo', $a)); @@ -20,12 +20,95 @@ public function doFoo(array $a, string $key): void $a = ['foo' => 2, 3 => 'bar']; assertType('true', array_key_exists('foo', $a)); + assertType('true', array_key_exists(3, $a)); assertType('true', array_key_exists('3', $a)); assertType('false', array_key_exists(4, $a)); + if (array_key_exists($key, $a)) { + assertType("'3'|'foo'", $key); + } + if (array_key_exists($anotherKey, $a)) { + assertType('3', $anotherKey); + } + $empty = []; assertType('false', array_key_exists('foo', $empty)); assertType('false', array_key_exists($key, $empty)); } + /** + * @param array $a + * @param array $b + * @param array $c + * @param array-key $key4 + * + * @return void + */ + public function doBar(array $a, array $b, array $c, int $key1, string $key2, int|string $key3, $key4, mixed $key5): void + { + if (array_key_exists($key1, $a)) { + assertType('int', $key1); + } + if (array_key_exists($key2, $a)) { + assertType('numeric-string', $key2); + } + if (array_key_exists($key3, $a)) { + assertType('int|numeric-string', $key3); + } + if (array_key_exists($key4, $a)) { + assertType('(int|numeric-string)', $key4); + } + if (array_key_exists($key5, $a)) { + assertType('int|numeric-string', $key5); + } + + if (array_key_exists($key1, $b)) { + assertType('*NEVER*', $key1); + } + if (array_key_exists($key2, $b)) { + assertType('string', $key2); + } + if (array_key_exists($key3, $b)) { + assertType('string', $key3); + } + if (array_key_exists($key4, $b)) { + assertType('string', $key4); + } + if (array_key_exists($key5, $b)) { + assertType('string', $key5); + } + + if (array_key_exists($key1, $c)) { + assertType('int', $key1); + } + if (array_key_exists($key2, $c)) { + assertType('string', $key2); + } + if (array_key_exists($key3, $c)) { + assertType('(int|string)', $key3); + } + if (array_key_exists($key4, $c)) { + assertType('(int|string)', $key4); + } + if (array_key_exists($key5, $c)) { + assertType('(int|string)', $key5); + } + + if (array_key_exists($key1, [3 => 'foo', 4 => 'bar'])) { + assertType('3|4', $key1); + } + if (array_key_exists($key2, [3 => 'foo', 4 => 'bar'])) { + assertType("'3'|'4'", $key2); + } + if (array_key_exists($key3, [3 => 'foo', 4 => 'bar'])) { + assertType("3|4|'3'|'4'", $key3); + } + if (array_key_exists($key4, [3 => 'foo', 4 => 'bar'])) { + assertType("(3|4|'3'|'4')", $key4); + } + if (array_key_exists($key5, [3 => 'foo', 4 => 'bar'])) { + assertType("3|4|'3'|'4'", $key5); + } + } + } diff --git a/tests/PHPStan/Analyser/data/array-map.php b/tests/PHPStan/Analyser/data/array-map.php index 88f70db4ad..75a68ab490 100644 --- a/tests/PHPStan/Analyser/data/array-map.php +++ b/tests/PHPStan/Analyser/data/array-map.php @@ -44,7 +44,7 @@ static function(string $string): string { $array ); - assertType('array', $mapped); + assertType('list', $mapped); } /** @@ -58,7 +58,7 @@ static function(string $string): string { $array ); - assertType('non-empty-array', $mapped); + assertType('non-empty-list', $mapped); } /** @param array{foo?: 0, bar?: 1, baz?: 2} $array */ diff --git a/tests/PHPStan/Analyser/data/array-merge.php b/tests/PHPStan/Analyser/data/array-merge.php index 21905ece2c..6dd7a13005 100644 --- a/tests/PHPStan/Analyser/data/array-merge.php +++ b/tests/PHPStan/Analyser/data/array-merge.php @@ -36,3 +36,19 @@ function unpackingConstantArrays(): void assertType('array{foo: \'bar\', bar: \'baz2\', 0: 17}', array_merge(['foo' => 'bar', 'bar' => 'baz1'], ['bar' => 'baz2', 17])); assertType('array{foo: \'bar\', bar: \'baz2\', 0: 17}', array_merge(['foo' => 'bar', 'bar' => 'baz1'], ...[['bar' => 'baz2', 17]])); } + +/** + * @param list $a + * @param list $b + * @return void + */ +function listIsStillList(array $a, array $b): void +{ + assertType('list', array_merge($a, $b)); + + $c = []; + foreach ($a as $v) { + $c = array_merge($a, $c); + } + assertType('list', $c); +} diff --git a/tests/PHPStan/Analyser/data/array-merge2.php b/tests/PHPStan/Analyser/data/array-merge2.php index db51127a6f..a52f640ef2 100644 --- a/tests/PHPStan/Analyser/data/array-merge2.php +++ b/tests/PHPStan/Analyser/data/array-merge2.php @@ -40,9 +40,9 @@ public function arrayMergeSimple($array1, $array2): void */ public function arrayMergeUnionType($array1, $array2): void { - assertType("array", array_merge($array1, $array1)); - assertType("array", array_merge($array1, $array2)); - assertType("array", array_merge($array2, $array1)); + assertType("list", array_merge($array1, $array1)); + assertType("list", array_merge($array1, $array2)); + assertType("list", array_merge($array2, $array1)); } /** @@ -51,8 +51,8 @@ public function arrayMergeUnionType($array1, $array2): void */ public function arrayMergeUnionTypeArrayShapes($array1, $array2): void { - assertType("array", array_merge($array1, $array1)); - assertType("array", array_merge($array1, $array2)); - assertType("array", array_merge($array2, $array1)); + assertType("list", array_merge($array1, $array1)); + assertType("list", array_merge($array1, $array2)); + assertType("list", array_merge($array2, $array1)); } } diff --git a/tests/PHPStan/Analyser/data/array-pop.php b/tests/PHPStan/Analyser/data/array-pop.php index 9cce1e6c7c..04494a96df 100644 --- a/tests/PHPStan/Analyser/data/array-pop.php +++ b/tests/PHPStan/Analyser/data/array-pop.php @@ -2,6 +2,7 @@ namespace ArrayPop; +use function PHPStan\Testing\assertNativeType; use function PHPStan\Testing\assertType; class Foo @@ -33,6 +34,10 @@ public function constantArrays(array $arr): void /** @var array{a: 0, b: 1, c: 2} $arr */ assertType('2', array_pop($arr)); assertType('array{a: 0, b: 1}', $arr); + + /** @var array{} $arr */ + assertType('null', array_pop($arr)); + assertType('array{}', $arr); } public function constantArraysWithOptionalKeys(array $arr): void @@ -54,4 +59,38 @@ public function constantArraysWithOptionalKeys(array $arr): void assertType('array{a?: 0, b?: 1}', $arr); } + public function list(array $arr): void + { + /** @var list $arr */ + assertType('string|null', array_pop($arr)); + assertType('list', $arr); + } + + public function mixed($mixed): void + { + assertType('mixed', array_pop($mixed)); + assertType('array', $mixed); + } + + public function foo1($mixed): void + { + if(is_array($mixed)) { + assertType('mixed', array_pop($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('mixed', array_pop($mixed)); + assertType('*ERROR*', $mixed); + } + } + + /** @param non-empty-array $arr1 */ + public function nativeTypes(array $arr1, array $arr2): void + { + assertType('string', array_pop($arr1)); + assertType('array', $arr1); + + assertNativeType('mixed', array_pop($arr2)); + assertNativeType('array', $arr2); + } + } diff --git a/tests/PHPStan/Analyser/data/array-push.php b/tests/PHPStan/Analyser/data/array-push.php index 8aaaf3326d..4e2e235530 100644 --- a/tests/PHPStan/Analyser/data/array-push.php +++ b/tests/PHPStan/Analyser/data/array-push.php @@ -12,8 +12,9 @@ * @param int[] $b * @param non-empty-array $c * @param array $d + * @param list $e */ -function arrayPush(array $a, array $b, array $c, array $d, array $arr): void +function arrayPush(array $a, array $b, array $c, array $d, array $e, array $arr): void { array_push($a, ...$b); assertType('array', $a); @@ -32,6 +33,11 @@ function arrayPush(array $a, array $b, array $c, array $d, array $arr): void $d1 = []; array_push($d, ...$d1); assertType('array', $d); + + /** @var list $e1 */ + $e1 = []; + array_push($e, ...$e1); + assertType('list', $e); } function arrayPushConstantArray(): void @@ -60,7 +66,7 @@ function arrayPushConstantArray(): void /** @var array $f1 */ $f1 = []; array_push($f, ...$f1); - assertType('non-empty-array', $f); + assertType('non-empty-list<17|bool|null>', $f); $g = [new stdClass()]; array_push($g, ...[new stdClass(), new stdClass()]); diff --git a/tests/PHPStan/Analyser/data/array-search-php7.php b/tests/PHPStan/Analyser/data/array-search-php7.php new file mode 100644 index 0000000000..b6f9bbae4d --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-search-php7.php @@ -0,0 +1,24 @@ + $arr */ + assertType('int|string|false', array_search('foo', $arr, true)); + assertType('int|string|false', array_search('foo', $arr)); + assertType('int|string|false', array_search($string, $arr, true)); + } + + public function normalArrays(array $arr, string $string): void + { + /** @var array $arr */ + assertType('int|false', array_search('foo', $arr, true)); + assertType('int|false', array_search('foo', $arr)); + assertType('int|false', array_search($string, $arr, true)); + + if (array_key_exists(17, $arr)) { + assertType('int|false', array_search('foo', $arr, true)); + assertType('int|false', array_search('foo', $arr)); + assertType('int|false', array_search($string, $arr, true)); + } + + if (array_key_exists(17, $arr) && $arr[17] === 'foo') { + assertType('17', array_search('foo', $arr, true)); + assertType('int|false', array_search('foo', $arr)); + assertType('int|false', array_search($string, $arr, true)); + } + } + + public function constantArrays(array $arr, string $string): void + { + /** @var array{'a', 'b', 'c'} $arr */ + assertType('1', array_search('b', $arr, true)); + assertType('0|1|2|false', array_search('b', $arr)); + assertType('0|1|2|false', array_search($string, $arr, true)); + + /** @var array{} $arr */ + assertType('false', array_search('b', $arr, true)); + assertType('false', array_search('b', $arr)); + assertType('false', array_search($string, $arr, true)); + } + + public function constantArraysWithOptionalKeys(array $arr, string $string): void + { + /** @var array{0: 'a', 1?: 'b', 2: 'c'} $arr */ + assertType('1|false', array_search('b', $arr, true)); + assertType('0|1|2|false', array_search('b', $arr)); + assertType('0|1|2|false', array_search($string, $arr, true)); + + /** @var array{0: 'a', 1?: 'b', 2: 'b'} $arr */ + assertType('1|2', array_search('b', $arr, true)); + assertType('0|1|2|false', array_search('b', $arr)); + assertType('0|1|2|false', array_search($string, $arr, true)); + } + + public function list(array $arr, string $string): void + { + /** @var list $arr */ + assertType('int<0, max>|false', array_search('foo', $arr, true)); + assertType('int<0, max>|false', array_search('foo', $arr)); + assertType('int<0, max>|false', array_search($string, $arr, true)); + } + +} diff --git a/tests/PHPStan/Analyser/data/array-shape-list-optional.php b/tests/PHPStan/Analyser/data/array-shape-list-optional.php new file mode 100644 index 0000000000..0eaa4471d2 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-shape-list-optional.php @@ -0,0 +1,26 @@ + $arr */ + assertType('string|null', array_shift($arr)); + assertType('list', $arr); + } + + public function mixed($mixed): void + { + assertType('mixed', array_shift($mixed)); + assertType('array', $mixed); + } + + public function foo1($mixed): void + { + if(is_array($mixed)) { + assertType('mixed', array_shift($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('mixed', array_shift($mixed)); + assertType('*ERROR*', $mixed); + } + } + + /** @param non-empty-array $arr1 */ + public function nativeTypes(array $arr1, array $arr2): void + { + assertType('string', array_shift($arr1)); + assertType('array', $arr1); + + assertNativeType('mixed', array_shift($arr2)); + assertNativeType('array', $arr2); + } } diff --git a/tests/PHPStan/Analyser/data/array-slice.php b/tests/PHPStan/Analyser/data/array-slice.php index 7a39157d91..b28c660786 100644 --- a/tests/PHPStan/Analyser/data/array-slice.php +++ b/tests/PHPStan/Analyser/data/array-slice.php @@ -9,10 +9,14 @@ class Foo /** * @param non-empty-array $a + * @param non-empty-list $b + * @param non-empty-array|non-empty-list $c */ - public function nonEmpty(array $a): void + public function nonEmpty(array $a, array $b, array $c): void { assertType('array', array_slice($a, 1)); + assertType('list', array_slice($b, 1)); + assertType('array', array_slice($c, 1)); } /** diff --git a/tests/PHPStan/Analyser/data/array-sum.php b/tests/PHPStan/Analyser/data/array-sum.php index 7e2f17fe7e..3d53b450e3 100644 --- a/tests/PHPStan/Analyser/data/array-sum.php +++ b/tests/PHPStan/Analyser/data/array-sum.php @@ -37,7 +37,7 @@ function foo3($floatList) function foo4($list) { $sum = array_sum($list); - assertType('float|int', $sum); + assertType('(float|int)', $sum); } /** @@ -48,3 +48,219 @@ function foo5($list) $sum = array_sum($list); assertType('float|int', $sum); } + +/** + * @param list<0> $list + */ +function foo6($list) +{ + assertType('0', array_sum($list)); +} +/** + * @param list<1> $list + */ +function foo7($list) +{ + assertType('int<0, max>', array_sum($list)); +} + +/** + * @param non-empty-list<1> $list + */ +function foo8($list) +{ + assertType('int<1, max>', array_sum($list)); +} + +/** + * @param list<-1> $list + */ +function foo9($list) +{ + assertType('int', array_sum($list)); +} + +/** + * @param list<1|2|3> $list + */ +function foo10($list) +{ + assertType('int<0, max>', array_sum($list)); +} + +/** + * @param non-empty-list<1|2|3> $list + */ +function foo11($list) +{ + assertType('int<1, max>', array_sum($list)); +} + +/** + * @param list<1|-1> $list + */ +function foo12($list) +{ + assertType('int', array_sum($list)); +} +/** + * @param non-empty-list<1|-1> $list + */ +function foo13($list) +{ + assertType('int', array_sum($list)); +} + +/** + * @param array{0} $list + */ +function foo14($list) +{ + assertType('0', array_sum($list)); +} +/** + * @param array{1} $list + */ +function foo15($list) +{ + assertType('1', array_sum($list)); +} + +/** + * @param array{1, 2, 3} $list + */ +function foo16($list) +{ + assertType('6', array_sum($list)); +} + +/** + * @param array{1, int} $list + */ +function foo17($list) +{ + assertType('int', array_sum($list)); +} + +/** + * @param array{1, float} $list + */ +function foo18($list) +{ + assertType('float', array_sum($list)); +} + +/** + * @param array{} $list + */ +function foo19($list) +{ + assertType('0', array_sum($list)); +} + + +/** + * @param list<1|float> $list + */ +function foo20($list) +{ + assertType('float|int<0, max>', array_sum($list)); +} + +/** + * @param array{1, int|float} $list + */ +function foo21($list) +{ + assertType('float|int', array_sum($list)); +} + +/** + * @param array{1, string} $list + */ +function foo22($list) +{ + assertType('float|int', array_sum($list)); +} + + +/** + * @param array{1, 3.2} $list + */ +function foo23($list) +{ + assertType('4.2', array_sum($list)); +} + +/** + * @param array{1, float|4} $list + */ +function foo24($list) +{ + assertType('5|float', array_sum($list)); +} + +/** + * @param array{1, 2|3.4} $list + */ +function foo25($list) +{ + assertType('3|4.4', array_sum($list)); +} + +/** + * @param array{1, 2.4|3.4} $list + */ +function foo26($list) +{ + assertType('3.4|4.4', array_sum($list)); +} + + +/** + * @param array{1}|array{2, 3} $list + */ +function foo27($list) +{ + assertType('1|5', array_sum($list)); +} + +/** + * @param array{1}|list<1>|array{float} $list + */ +function foo28($list) +{ + assertType('float|int<0, max>', array_sum($list)); +} + +/** + * @param non-empty-list|int<1, max>> $list + */ +function foo29($list) +{ + assertType('int', array_sum($list)); +} + +/** + * @param array{'133', 3} $list + */ +function foo30($list) +{ + assertType('136', array_sum($list)); +} + +/** + * @param array{0: 1, 1?: 2, 2?: 3} $list + */ +function foo31($list) +{ + assertType('1|3|4|6', array_sum($list)); +} + +/** + * @param mixed $list + */ +function foo32($list) +{ + assertType('(float|int)', array_sum($list)); +} diff --git a/tests/PHPStan/Analyser/data/array-unshift.php b/tests/PHPStan/Analyser/data/array-unshift.php index 59d7819819..933aad522d 100644 --- a/tests/PHPStan/Analyser/data/array-unshift.php +++ b/tests/PHPStan/Analyser/data/array-unshift.php @@ -12,8 +12,9 @@ * @param int[] $b * @param non-empty-array $c * @param array $d + * @param list $e */ -function arrayUnshift(array $a, array $b, array $c, array $d, array $arr): void +function arrayUnshift(array $a, array $b, array $c, array $d, array $e, array $arr): void { array_unshift($a, ...$b); assertType('array', $a); @@ -32,6 +33,11 @@ function arrayUnshift(array $a, array $b, array $c, array $d, array $arr): void $d1 = []; array_unshift($d, ...$d1); assertType('array', $d); + + /** @var list $e1 */ + $e1 = []; + array_unshift($e, ...$e1); + assertType('list', $e); } function arrayUnshiftConstantArray(): void @@ -60,7 +66,7 @@ function arrayUnshiftConstantArray(): void /** @var array $f1 */ $f1 = []; array_unshift($f, ...$f1); - assertType('non-empty-array', $f); + assertType('non-empty-list<17|bool|null>', $f); $g = [new stdClass()]; array_unshift($g, ...[new stdClass(), new stdClass()]); diff --git a/tests/PHPStan/Analyser/data/array_keys-php7.php b/tests/PHPStan/Analyser/data/array_keys-php7.php new file mode 100644 index 0000000000..bf83b41e99 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array_keys-php7.php @@ -0,0 +1,20 @@ +', array_keys($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('null', array_keys($mixed)); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/array_keys.php b/tests/PHPStan/Analyser/data/array_keys.php new file mode 100644 index 0000000000..e35f3894d4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array_keys.php @@ -0,0 +1,27 @@ +', array_keys($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('*NEVER*', array_keys($mixed)); + } + } + + public function constantArrayType(): void + { + $numbers = array_filter( + [1 => 'a', 2 => 'b', 3 => 'c'], + static fn ($value) => mt_rand(0, 1) === 0, + ); + assertType("array{0?: 1|2|3, 1?: 2|3, 2?: 3}", array_keys($numbers)); + } +} diff --git a/tests/PHPStan/Analyser/data/array_splice.php b/tests/PHPStan/Analyser/data/array_splice.php new file mode 100644 index 0000000000..7075c0fb8b --- /dev/null +++ b/tests/PHPStan/Analyser/data/array_splice.php @@ -0,0 +1,61 @@ + $arr + * @return void + */ +function insertViaArraySplice(array $arr): void +{ + $brr = $arr; + array_splice($brr, 0, 0, 1); + assertType('array', $brr); + + $brr = $arr; + array_splice($brr, 0, 0, [1]); + assertType('array', $brr); + + $brr = $arr; + array_splice($brr, 0, 0, ''); + assertType('array', $brr); + + $brr = $arr; + array_splice($brr, 0, 0, ['']); + assertType('array', $brr); + + $brr = $arr; + array_splice($brr, 0, 0, null); + assertType('array', $brr); + + $brr = $arr; + array_splice($brr, 0, 0, [null]); + assertType('array', $brr); + + $brr = $arr; + array_splice($brr, 0, 0, new Foo()); + assertType('array', $brr); + + $brr = $arr; + array_splice($brr, 0, 0, [new \stdClass()]); + assertType('array', $brr); + + $brr = $arr; + array_splice($brr, 0, 0, false); + assertType('array', $brr); + + $brr = $arr; + array_splice($brr, 0, 0, [false]); + assertType('array', $brr); +} diff --git a/tests/PHPStan/Analyser/data/array_values-php7.php b/tests/PHPStan/Analyser/data/array_values-php7.php new file mode 100644 index 0000000000..4e6e93d33f --- /dev/null +++ b/tests/PHPStan/Analyser/data/array_values-php7.php @@ -0,0 +1,20 @@ +', array_values($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('null', array_values($mixed)); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/array_values.php b/tests/PHPStan/Analyser/data/array_values.php new file mode 100644 index 0000000000..acad5d6abe --- /dev/null +++ b/tests/PHPStan/Analyser/data/array_values.php @@ -0,0 +1,40 @@ +', array_values($mixed)); + } else { + assertType('mixed~array', $mixed); + assertType('*NEVER*', array_values($mixed)); + } + } + + /** + * @param list $list + */ + public function foo2($list): void + { + if(is_array($list)) { + assertType('list', array_values($list)); + } else { + assertType('*NEVER*', $list); + assertType('*NEVER*', array_values($list)); + } + } + + public function constantArrayType(): void + { + $numbers = array_filter( + [1 => 'a', 2 => 'b', 3 => 'c'], + static fn ($value) => mt_rand(0, 1) === 0, + ); + assertType("array{0?: 'a'|'b'|'c', 1?: 'b'|'c', 2?: 'c'}", array_values($numbers)); + } +} diff --git a/tests/PHPStan/Analyser/data/assert-class-type.php b/tests/PHPStan/Analyser/data/assert-class-type.php new file mode 100644 index 0000000000..5bb97f4fce --- /dev/null +++ b/tests/PHPStan/Analyser/data/assert-class-type.php @@ -0,0 +1,42 @@ +value = $value; + } + + /** + * @template K + * @param K $data + * @phpstan-assert T $data + */ + public function assert($data): void + { + if ($data !== $this->value) { + throw new Exception(); + } + } +} + +function () { + $a = new HelloWorld(123); + assertType('AssertClassType\\HelloWorld', $a); + + $b = $_GET['value']; + $a->assert($b); + + assertType('int', $b); +}; diff --git a/tests/PHPStan/Analyser/data/assert-conditional.php b/tests/PHPStan/Analyser/data/assert-conditional.php new file mode 100644 index 0000000000..4e52490066 --- /dev/null +++ b/tests/PHPStan/Analyser/data/assert-conditional.php @@ -0,0 +1,37 @@ +', $arr); +} + +/** + * @param mixed[] $arr + */ +function takesArrayIfTrue(array $arr) : void { + assertType('array', $arr); + + if (validateStringArrayIfTrue($arr)) { + assertType('array', $arr); + } else { + assertType('array', $arr); + } +} +/** + * @param mixed[] $arr + */ +function takesArrayIfTrue1(array $arr) : void { + assertType('array', $arr); + + if (!validateStringArrayIfTrue($arr)) { + assertType('array', $arr); + } else { + assertType('array', $arr); + } +} + +/** + * @param mixed[] $arr + */ +function takesArrayIfFalse(array $arr) : void { + assertType('array', $arr); + + if (!validateStringArrayIfFalse($arr)) { + assertType('array', $arr); + } else { + assertType('array', $arr); + } +} + +/** + * @param mixed[] $arr + */ +function takesArrayIfFalse1(array $arr) : void { + assertType('array', $arr); + + if (validateStringArrayIfFalse($arr)) { + assertType('array', $arr); + } else { + assertType('array', $arr); + } +} + +/** + * @param mixed[] $arr + */ +function takesStringOrIntArray(array $arr) : void { + assertType('array', $arr); + + if (validateStringOrIntArray($arr)) { + assertType('array', $arr); + } else { + assertType('array', $arr); + } +} + +/** + * @param mixed[] $arr + */ +function takesStringOrNonEmptyIntArray(array $arr) : void { + assertType('array', $arr); + + if (validateStringOrNonEmptyIntArray($arr)) { + assertType('array', $arr); + } else { + assertType('non-empty-array', $arr); + } +} + +/** + * @param mixed $value + */ +function takesNotNull($value) : void { + assertType('mixed', $value); + + validateNotNull($value); + assertType('mixed~null', $value); +} + + +/** + * @template T of object + * @param object $object + * @param class-string $class + * @phpstan-assert T $object + */ +function validateClassType(object $object, string $class): void {} + +class ClassToValidate {} + +function (object $object) { + validateClassType($object, ClassToValidate::class); + assertType('AssertDocblock\ClassToValidate', $object); +}; + + +class A { + /** + * @phpstan-assert-if-true int $x + */ + public function testInt(mixed $x): bool + { + return is_int($x); + } + + /** + * @phpstan-assert-if-true !int $x + */ + public function testNotInt(mixed $x): bool + { + return !is_int($x); + } +} + +class B extends A +{ + public function testInt(mixed $y): bool + { + return parent::testInt($y); + } +} + +function (A $a, $i) { + if ($a->testInt($i)) { + assertType('int', $i); + } else { + assertType('mixed~int', $i); + } + + if ($a->testNotInt($i)) { + assertType('mixed~int', $i); + } else { + assertType('int', $i); + } +}; + +function (B $b, $i) { + if ($b->testInt($i)) { + assertType('int', $i); + } else { + assertType('mixed~int', $i); + } +}; + +function (A $a, string $i) { + if ($a->testInt($i)) { + assertType('*NEVER*', $i); + } else { + assertType('string', $i); + } + + if ($a->testNotInt($i)) { + assertType('string', $i); + } else { + assertType('*NEVER*', $i); + } +}; + +function (A $a, int $i) { + if ($a->testInt($i)) { + assertType('int', $i); + } else { + assertType('*NEVER*', $i); + } + + if ($a->testNotInt($i)) { + assertType('*NEVER*', $i); + } else { + assertType('int', $i); + } +}; diff --git a/tests/PHPStan/Analyser/data/assert-empty.php b/tests/PHPStan/Analyser/data/assert-empty.php new file mode 100644 index 0000000000..12176791a3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/assert-empty.php @@ -0,0 +1,29 @@ + + */ +class IntWrapper implements WrapperInterface +{ + public function assert(mixed $param): void + { + } + + public function supports(mixed $param): bool + { + return is_int($param); + } + + public function notSupports(mixed $param): bool + { + return !is_int($param); + } +} + +/** + * @template T of object + * @implements WrapperInterface + */ +abstract class ObjectWrapper implements WrapperInterface +{ +} + +/** + * @extends ObjectWrapper<\DateTimeInterface> + */ +class DateTimeInterfaceWrapper extends ObjectWrapper +{ + public function assert(mixed $param): void + { + } + + public function supports(mixed $param): bool + { + return $param instanceof \DateTimeInterface; + } + + public function notSupports(mixed $param): bool + { + return !$param instanceof \DateTimeInterface; + } +} + +function (IntWrapper $test, $val) { + if ($test->supports($val)) { + assertType('int', $val); + } else { + assertType('mixed~int', $val); + } + + if ($test->notSupports($val)) { + assertType('mixed~int', $val); + } else { + assertType('int', $val); + } + + assertType('mixed', $val); + $test->assert($val); + assertType('int', $val); +}; + +function (DateTimeInterfaceWrapper $test, $val) { + if ($test->supports($val)) { + assertType('DateTimeInterface', $val); + } else { + assertType('mixed~DateTimeInterface', $val); + } + + if ($test->notSupports($val)) { + assertType('mixed~DateTimeInterface', $val); + } else { + assertType('DateTimeInterface', $val); + } + + assertType('mixed', $val); + $test->assert($val); + assertType('DateTimeInterface', $val); +}; diff --git a/tests/PHPStan/Analyser/data/assert-intersected.php b/tests/PHPStan/Analyser/data/assert-intersected.php new file mode 100644 index 0000000000..a39ffe1436 --- /dev/null +++ b/tests/PHPStan/Analyser/data/assert-intersected.php @@ -0,0 +1,30 @@ +assert($value); + assertType('non-empty-list', $value); +} diff --git a/tests/PHPStan/Analyser/data/assert-invariant.php b/tests/PHPStan/Analyser/data/assert-invariant.php new file mode 100644 index 0000000000..b7368f06e9 --- /dev/null +++ b/tests/PHPStan/Analyser/data/assert-invariant.php @@ -0,0 +1,27 @@ +getId() + * @phpstan-assert-if-false null $this->getId() + */ + public function hasId(): bool; +} + +function (Identity $identity) { + assertType('int|null', $identity->getId()); + + if ($identity->hasId()) { + assertType('int', $identity->getId()); + } else { + assertType('null', $identity->getId()); + } +}; diff --git a/tests/PHPStan/Analyser/data/assert-methods.php b/tests/PHPStan/Analyser/data/assert-methods.php new file mode 100644 index 0000000000..f6609f3f8f --- /dev/null +++ b/tests/PHPStan/Analyser/data/assert-methods.php @@ -0,0 +1,99 @@ + $class + * @phpstan-assert iterable> $value + * + * @param iterable $value + * + * @throws \InvalidArgumentException + * + * @return void + */ + public static function doFoo($value, string $class): void + { + + } + + public function doBar($mixed) + { + self::doFoo($mixed, stdClass::class); + assertType('iterable|stdClass>', $mixed); + } + + /** + * @param array $objects + * @return void + */ + public function doBar2(array $objects) + { + self::doFoo($objects, stdClass::class); + assertType('array', $objects); + } + + /** + * @param array $strings + * @return void + */ + public function doBar3(array $strings) + { + self::doFoo($strings, stdClass::class); + assertType('array>', $strings); + } + + /** + * @template ExpectedType of object + * @param class-string $class + * @phpstan-assert iterable> $value + * + * @param iterable $value + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function doBaz($value, string $class): void + { + + } + + public function doLorem($mixed) + { + $this->doBaz($mixed, stdClass::class); + assertType('iterable|stdClass>', $mixed); + } + +} + +/** @template T */ +class Bar +{ + + /** + * @phpstan-assert T $arg + */ + public function doFoo($arg): void + { + + } + + /** + * @param Bar $bar + */ + public function doBar(Bar $bar, object $object): void + { + assertType('object', $object); + $bar->doFoo($object); + assertType(stdClass::class, $object); + } + +} diff --git a/tests/PHPStan/Analyser/data/assert-property.php b/tests/PHPStan/Analyser/data/assert-property.php new file mode 100644 index 0000000000..a8aaac79d2 --- /dev/null +++ b/tests/PHPStan/Analyser/data/assert-property.php @@ -0,0 +1,30 @@ +id + * @phpstan-assert-if-false null $this->id + */ + public function hasId(): bool + { + return $this->id !== null; + } +} + +function (Identity $identity) { + assertType('int|null', $identity->id); + + if ($identity->hasId()) { + assertType('int', $identity->id); + } else { + assertType('null', $identity->id); + } +}; diff --git a/tests/PHPStan/Analyser/data/assert-stub.php b/tests/PHPStan/Analyser/data/assert-stub.php new file mode 100644 index 0000000000..f4e3d6b353 --- /dev/null +++ b/tests/PHPStan/Analyser/data/assert-stub.php @@ -0,0 +1,42 @@ +doFoo($x)) { + assertType('int', $x); + } else { + assertType('mixed~int', $x); + } +}; + + +function (Bar $b, $x): void { + if ($b->doFoo($x)) { + assertType('int', $x); + } else { + assertType('mixed~int', $x); + } +}; diff --git a/tests/PHPStan/Analyser/data/assert-this.php b/tests/PHPStan/Analyser/data/assert-this.php new file mode 100644 index 0000000000..4b29fa697c --- /dev/null +++ b/tests/PHPStan/Analyser/data/assert-this.php @@ -0,0 +1,84 @@ + $this + * @phpstan-assert-if-false Err $this + */ + public function isOk(): bool; + + /** + * @return TOk|never + */ + public function unwrap(); +} + +/** + * @template TOk + * @template-implements Result + */ +class Ok implements Result { + public function __construct(private $value) { + } + + /** + * @return true + */ + public function isOk(): bool { + return true; + } + + /** + * @return TOk + */ + public function unwrap() { + return $this->value; + } +} + +/** + * @template TErr + * @template-implements Result + */ +class Err implements Result { + public function __construct(private $value) { + } + + /** + * @return false + */ + public function isOk(): bool { + return false; + } + + /** + * @return never + */ + public function unwrap() { + throw new RuntimeException('Tried to unwrap() an Err value'); + } +} + +function () { + /** @var Result $result */ + $result = new Ok(123); + assertType('AssertThis\\Result', $result); + + if ($result->isOk()) { + assertType('AssertThis\\Ok', $result); + assertType('int', $result->unwrap()); + } else { + assertType('AssertThis\\Err', $result); + assertType('never', $result->unwrap()); + } +}; \ No newline at end of file diff --git a/tests/PHPStan/Analyser/data/assert.stub b/tests/PHPStan/Analyser/data/assert.stub new file mode 100644 index 0000000000..eeb68738cb --- /dev/null +++ b/tests/PHPStan/Analyser/data/assert.stub @@ -0,0 +1,16 @@ +asymmetricPropertyRw); + assertType('int', $this->asymmetricPropertyXw); + assertType('int', $this->asymmetricPropertyRx); + } + +} diff --git a/tests/PHPStan/Analyser/data/base64_decode.php b/tests/PHPStan/Analyser/data/base64_decode.php new file mode 100644 index 0000000000..34de145d9a --- /dev/null +++ b/tests/PHPStan/Analyser/data/base64_decode.php @@ -0,0 +1,21 @@ +getBenevolent(); + assertType('array|null', $dbresponse); + + if ($dbresponse === null) {return;} + + assertType('array', $dbresponse); + assertType('(float|int|string|null)', $dbresponse['Value']); + assertType('int<0, max>', strlen($dbresponse['Value'])); + } + + /** + * @return array>|null + */ + private function getBenevolent(): ?array{ + return rand(10) > 1 ? null : []; + } +} diff --git a/tests/PHPStan/Analyser/data/binary.php b/tests/PHPStan/Analyser/data/binary.php index 352ff65141..cf6f245f5b 100644 --- a/tests/PHPStan/Analyser/data/binary.php +++ b/tests/PHPStan/Analyser/data/binary.php @@ -137,6 +137,12 @@ public function doFoo(array $generalArray) $arrayToBeUnset2 = $arrayToBeUnset; unset($arrayToBeUnset2[$string]); + $arrayToBeUnset3 = $array; + unset($arrayToBeUnset3[$integer]); + + $arrayToBeUnset4 = $arrayToBeUnset3; + unset($arrayToBeUnset4[$integer]); + /** @var array $shiftedNonEmptyArray */ $shiftedNonEmptyArray = doFoo(); diff --git a/tests/PHPStan/Analyser/data/bug-10002.php b/tests/PHPStan/Analyser/data/bug-10002.php new file mode 100644 index 0000000000..baff0c56fa --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10002.php @@ -0,0 +1,44 @@ +format('n'); + $monthDay2 = (int) $day2->format('n'); + + + if($monthDay1 === $month){ + return true; + } + + assertType('bool', $monthDay2 === $month); + + return $monthDay2 === $month; + } + + public function bar1(int $month): bool + { + $day1 = new \DateTime('2022-01-01'); + $day2 = new \DateTime('2022-05-01'); + + $monthDay1 = (int) $day1->format('n'); + $monthDay2 = (int) $day2->format('n'); + + + if($month === $monthDay1){ + return true; + } + + assertType('bool', $month === $monthDay2); + + return $monthDay2 === $month; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10037.php b/tests/PHPStan/Analyser/data/bug-10037.php new file mode 100644 index 0000000000..58adb961c1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10037.php @@ -0,0 +1,96 @@ + */ +final readonly class PostFetcher implements Fetcher +{ + public function supports(Identifier $identifier): bool + { + return $identifier instanceof PostIdentifier; + } + + public function fetch(Identifier $identifier): Document + { + // SA knows $identifier is instance of PostIdentifier here + return $identifier->foo(); + } +} + +class PostIdentifier implements Identifier +{ + public function foo(): Document + { + return new class implements Document{}; + } +} + +function (Identifier $i): void { + $fetcher = new PostFetcher(); + \PHPStan\Testing\assertType('Bug10037\Identifier', $i); + if ($fetcher->supports($i)) { + \PHPStan\Testing\assertType('Bug10037\PostIdentifier', $i); + $fetcher->fetch($i); + } else { + $fetcher->fetch($i); + } +}; + +class Post +{ +} + +/** @template T */ +abstract class Voter +{ + + /** @phpstan-assert-if-true T $subject */ + abstract function supports(string $attribute, mixed $subject): bool; + + /** @param T $subject */ + abstract function voteOnAttribute(string $attribute, mixed $subject): bool; + +} + +/** @extends Voter */ +class PostVoter extends Voter +{ + + /** @phpstan-assert-if-true Post $subject */ + function supports(string $attribute, mixed $subject): bool + { + + } + + function voteOnAttribute(string $attribute, mixed $subject): bool + { + \PHPStan\Testing\assertType('Bug10037\Post', $subject); + } +} + +function ($subject): void { + $voter = new PostVoter(); + \PHPStan\Testing\assertType('mixed', $subject); + if ($voter->supports('aaa', $subject)) { + \PHPStan\Testing\assertType('Bug10037\Post', $subject); + $voter->voteOnAttribute('aaa', $subject); + } else { + $voter->voteOnAttribute('aaa', $subject); + } +}; diff --git a/tests/PHPStan/Analyser/data/bug-10049-recursive.php b/tests/PHPStan/Analyser/data/bug-10049-recursive.php new file mode 100644 index 0000000000..b0887157ba --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10049-recursive.php @@ -0,0 +1,66 @@ + + */ +abstract class SimpleEntity +{ + /** + * @param SimpleTable $table + */ + public function __construct(protected readonly SimpleTable $table) + { + } +} + +/** + * @template-covariant E of SimpleEntity + */ +class SimpleTable +{ + /** + * @template ENTITY of SimpleEntity + * + * @param class-string $className + * + * @return SimpleTable + */ + public static function table(string $className, string $name): SimpleTable + { + return new SimpleTable($className, $name); + } + + /** + * @param class-string $className + */ + private function __construct(readonly string $className, readonly string $table) + { + } +} + +/** + * @template-extends SimpleEntity + */ +class TestEntity extends SimpleEntity +{ + public function __construct() + { + $table = SimpleTable::table(TestEntity::class, 'testentity'); + parent::__construct($table); + } +} + + +/** + * @template-extends SimpleEntity + */ +class AnotherEntity extends SimpleEntity +{ + public function __construct() + { + $table = SimpleTable::table(AnotherEntity::class, 'anotherentity'); + parent::__construct($table); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10071.php b/tests/PHPStan/Analyser/data/bug-10071.php new file mode 100644 index 0000000000..ecb298adb3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10071.php @@ -0,0 +1,22 @@ += 8.0 + +namespace Bug10071; + +use function PHPStan\Testing\assertType; + +class Foo +{ + public ?bool $bar = null; +} + + +function okIfBar(?Foo $foo = null): void +{ + if ($foo?->bar !== false) { + assertType(Foo::class . '|null', $foo); + } else { + assertType(Foo::class, $foo); + } + + assertType(Foo::class . '|null', $foo); +} diff --git a/tests/PHPStan/Analyser/data/bug-10080.php b/tests/PHPStan/Analyser/data/bug-10080.php new file mode 100644 index 0000000000..1875d50dfd --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10080.php @@ -0,0 +1,76 @@ + assertType('null', $val), + default => assertType('int', $val), + }; +}; + +function (?int $val) { + match ($foo = $val) { + null => assertType('null', $val), + default => assertType('int', $val), + }; +}; + +function (?int $val) { + match ($foo = $val) { + null => assertType('null', $foo), + default => assertType('int', $foo), + }; +}; diff --git a/tests/PHPStan/Analyser/data/bug-10086.php b/tests/PHPStan/Analyser/data/bug-10086.php new file mode 100644 index 0000000000..5b1fc7c235 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10086.php @@ -0,0 +1,12 @@ + +match($_GET['x']) { + 'x' => 'y', + default => 'z', +} +)(); + +define('x', $a); diff --git a/tests/PHPStan/Analyser/data/bug-10088.php b/tests/PHPStan/Analyser/data/bug-10088.php new file mode 100644 index 0000000000..df9bd2b6c0 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10088.php @@ -0,0 +1,135 @@ +assertInstanceOfStdClass($date ?? null); + assertVariableCertainty(TrinaryLogic::createYes(), $date); + } + + /** + * @param mixed $m + * @phpstan-assert stdClass $m + */ + private function assertInstanceOfStdClass($m): void + { + if (!$m instanceof stdClass) { + throw new \Exception(); + } + } + + /** + * @param mixed[] $period + */ + public function testCarbon2(array $period): void + { + foreach ($period as $date) { + break; + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $date); + assert(($date ?? null) instanceof stdClass); + assertVariableCertainty(TrinaryLogic::createYes(), $date); + } + + function constantIfElse(int $x): void { + $link_mode = $x > 10 ? "remove" : "add"; + + assertType('int', $x); + if ($link_mode === "add") { + assertType('int', $x); + } else { + assertType('int<11, max>', $x); + } + assertType('int', $x); + } + + function constantIfElseShort(int $x): void { + $link_mode = $x > 10 ?: "remove"; + + assertType('int', $x); + if ($link_mode === "remove") { + assertType('int', $x); + } else { + assertType('int<11, max>', $x); + } + assertType('int', $x); + } + + function nonEmptyArray(array $arr): void { + $link_mode = $arr ? "truethy-arr" : "falsey-arr"; + assertType('array', $arr); + if ($link_mode === "truethy-arr") { + assertType('non-empty-array', $arr); + } else { + assertType('array{}', $arr); + } + assertType('array', $arr); + } + + /** + * @param array $arr + * @param 0|positive-int $intRange + */ + function nonEmptyArrayViaInt(array $arr, $intRange): void { + $link_mode = $arr ? $intRange : -10; + assertType('array', $arr); + if ($link_mode >= 0) { + assertType('non-empty-array', $arr); + } else { + assertType('array{}', $arr); + } + assertType('array', $arr); + } + + /** + * @param string[] $arr + * @param 0|positive-int $posInt + */ + function overlappingIfElseType($arr, int $x, int $posInt): void { + $link_mode = $arr ? $posInt : $x; + assert($link_mode >= 0); + + assertType('array', $arr); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-10092.php b/tests/PHPStan/Analyser/data/bug-10092.php new file mode 100644 index 0000000000..aa5bacc049 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10092.php @@ -0,0 +1,43 @@ + */ +function int() { } + +/** @return TypeInterface<0> */ +function zero() { } + +/** @return TypeInterface> */ +function positive_int() { } + +/** @return TypeInterface */ +function numeric_string() { } + + +/** + * @template T + * + * @param TypeInterface $first + * @param TypeInterface $second + * @param TypeInterface ...$rest + * + * @return TypeInterface + */ +function union( + TypeInterface $first, + TypeInterface $second, + TypeInterface ...$rest +) { + +} + +function (): void { + assertType('Bug10092\TypeInterface', union(int(), numeric_string())); + assertType('Bug10092\TypeInterface>', union(positive_int(), zero())); +}; diff --git a/tests/PHPStan/Analyser/data/bug-10122.php b/tests/PHPStan/Analyser/data/bug-10122.php new file mode 100644 index 0000000000..b945f83075 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10122.php @@ -0,0 +1,54 @@ +a ?? throw new \Exception(); + + assertType(A::class, $a); + assertType(B::class . '|null', $b); + + return [$a, $b]; +} diff --git a/tests/PHPStan/Analyser/data/bug-1014.php b/tests/PHPStan/Analyser/data/bug-1014.php index 9d0f0567b0..d146c3341f 100644 --- a/tests/PHPStan/Analyser/data/bug-1014.php +++ b/tests/PHPStan/Analyser/data/bug-1014.php @@ -1,5 +1,7 @@ ", $files); + + return empty($files) ? [] : [1,2]; +} diff --git a/tests/PHPStan/Analyser/data/bug-10201.php b/tests/PHPStan/Analyser/data/bug-10201.php new file mode 100644 index 0000000000..a5cae6e11e --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10201.php @@ -0,0 +1,22 @@ += 8.1 + +namespace Bug10201; + +use function PHPStan\Testing\assertType; + +enum Hello +{ + case Hi; +} + +function bla(null|string|Hello $hello): void { + if (!in_array($hello, [Hello::Hi, null], true) && !($hello instanceof Hello)) { + assertType('string', $hello); + } +} + +function bla2(null|string|Hello $hello): void { + if (!in_array($hello, [Hello::Hi, null], true)) { + assertType('string', $hello); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10224.php b/tests/PHPStan/Analyser/data/bug-10224.php new file mode 100644 index 0000000000..3158734620 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10224.php @@ -0,0 +1,33 @@ + $list */ + $list = []; + + assertType('list', $list); + + assert((count($list) <= 1) === true); + assertType('list', $list); + } + + /** @param list $c */ + public function sayHello(array $c): void + { + assertType('list', $c); + if (count($c) > 0) { + $c = array_map(fn () => new stdClass(), $c); + assertType('non-empty-list', $c); + } else { + assertType('array{}', $c); + } + + assertType('list', $c); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10283.php b/tests/PHPStan/Analyser/data/bug-10283.php new file mode 100644 index 0000000000..e2cb63e31e --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10283.php @@ -0,0 +1,25 @@ +x); + assertType('string', $test->y); + assertType('array', $test->z); + + assertType('int', $test->doFoo()); +} + +function testExtendedInterface(AnotherInterface $test): void +{ + assertType('int', $test->x); + assertType('string', $test->y); + assertType('array', $test->z); + + assertType('int', $test->doFoo()); +} + +interface AnotherInterface extends SampleInterface +{ +} + +class SomeSubClass extends SomeClass {} + +class ValidClass extends SomeClass implements SampleInterface {} + +class ValidSubClass extends SomeSubClass implements SampleInterface {} + +class InvalidClass implements SampleInterface {} diff --git a/tests/PHPStan/Analyser/data/bug-10302-trait-extends.php b/tests/PHPStan/Analyser/data/bug-10302-trait-extends.php new file mode 100644 index 0000000000..6489de2dcc --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10302-trait-extends.php @@ -0,0 +1,52 @@ +x); + assertType('string', $this->y); + assertType('*ERROR*', $this->z); + } +} + +/** + * @phpstan-require-extends SomeClass + */ +trait anotherTrait +{ +} + +class SomeClass { + public int $x = 1; + protected string $y = 'foo'; + private array $z = []; +} + +function test(SomeClass $test): void +{ + assertType('int', $test->x); + assertType('string', $test->y); + assertType('array', $test->z); +} + +class SomeSubClass extends SomeClass {} + +class ValidClass extends SomeClass { + use myTrait; +} + +class ValidSubClass extends SomeSubClass { + use myTrait; +} + +class InvalidClass { + use anotherTrait; +} diff --git a/tests/PHPStan/Analyser/data/bug-10302-trait-implements.php b/tests/PHPStan/Analyser/data/bug-10302-trait-implements.php new file mode 100644 index 0000000000..f4b26cea11 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10302-trait-implements.php @@ -0,0 +1,54 @@ +foo(); + $this->doesNotExist(); + } +} + +interface SomeInterface +{ + public function foo(): string; +} + +class SomeClass implements SomeInterface { + use myTrait; + + public int $x; + protected string $y; + private array $z = []; + + public function foo(): string + { + return "hallo"; + } +} + +function test(SomeClass $test): void +{ + assertType('int', $test->x); + assertType('string', $test->y); + assertType('array', $test->z); + + assertType('string', $test->foo()); +} + +class ValidImplements implements SomeInterface { + public function foo(): string + { + return "hallo"; + } +} + +class InvalidClass { + use myTrait; +} diff --git a/tests/PHPStan/Analyser/data/bug-10302.php b/tests/PHPStan/Analyser/data/bug-10302.php new file mode 100644 index 0000000000..9cb45b51b6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10302.php @@ -0,0 +1,55 @@ +busy); + assertType('bool', $b->busy2); +}; + +class ModelWithoutAllowDynamicProperties +{ + +} + +/** + * @property-read bool $busy + * @phpstan-require-extends ModelWithoutAllowDynamicProperties + */ +interface BatchAwareWithoutAllowDynamicProperties +{ + +} + +function (BatchAwareWithoutAllowDynamicProperties $b): void +{ + $result = $b->busy; // @phpstan-ignore-line + + assertType('*ERROR*', $result); +}; diff --git a/tests/PHPStan/Analyser/data/bug-10317.php b/tests/PHPStan/Analyser/data/bug-10317.php new file mode 100644 index 0000000000..1ccd87d41a --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10317.php @@ -0,0 +1,14 @@ +optionalKey ?? null; + + assertType('int|null', $valueObject); +} diff --git a/tests/PHPStan/Analyser/data/bug-10358.php b/tests/PHPStan/Analyser/data/bug-10358.php new file mode 100644 index 0000000000..fc8a94f01c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10358.php @@ -0,0 +1,8 @@ + $options Array of options. Possible keys are: + * + * - `cache` - Can either be `true`, to enable caching using the config in View::$elementCache. Or an array + * If an array, the following keys can be used: + * + * - `config` - Used to store the cached element in a custom cache configuration. + * - `key` - Used to define the key used in the Cache::write(). It will be prefixed with `element_` + * + * - `callbacks` - Set to true to fire beforeRender and afterRender helper callbacks for this element. + * Defaults to false. + * - `ignoreMissing` - Used to allow missing elements. Set to true to not throw exceptions. + * - `plugin` - setting to false will force to use the application's element from plugin templates, when the + * plugin has element with same name. Defaults to true + * @return string Rendered Element + * @psalm-param array{cache?:array|true, callbacks?:bool, plugin?:string|false, ignoreMissing?:bool} $options + */ + public function element(string $name, array $data = [], array $options = []): string + { + assertType('array|true', $options['cache']); + $options += ['callbacks' => false, 'cache' => null, 'plugin' => null, 'ignoreMissing' => false]; + assertType('array|true|null', $options['cache']); + if (isset($options['cache'])) { + $options['cache'] = $this->_elementCache( + $name, + $data, + array_diff_key($options, ['callbacks' => false, 'plugin' => null, 'ignoreMissing' => null]) + ); + assertType('array{key: string, config: string}', $options['cache']); + } else { + assertType('null', $options['cache']); + } + assertType('array{key: string, config: string}|null', $options['cache']); + + $pluginCheck = $options['plugin'] !== false; + $file = $this->_getElementFileName($name, $pluginCheck); + if ($file && $options['cache']) { + assertType('array{key: string, config: string}', $options['cache']); + return $this->cache(function (): void { + echo ''; + }, $options['cache']); + } + + return $file; + } + + /** + * @param string $name + * @param bool $pluginCheck + */ + protected function _getElementFileName(string $name, bool $pluginCheck): string + { + return $name; + } + + /** + * @param callable $block The block of code that you want to cache the output of. + * @param array $options The options defining the cache key etc. + * @return string The rendered content. + * @throws \InvalidArgumentException When $options is lacking a 'key' option. + */ + public function cache(callable $block, array $options = []): string + { + $options += ['key' => '', 'config' => []]; + if (empty($options['key'])) { + throw new \InvalidArgumentException('Cannot cache content with an empty key'); + } + /** @var string $result */ + $result = $options['key']; + if ($result) { + return $result; + } + + $bufferLevel = ob_get_level(); + ob_start(); + + try { + $block(); + } catch (\Throwable $exception) { + while (ob_get_level() > $bufferLevel) { + ob_end_clean(); + } + + throw $exception; + } + + $result = (string)ob_get_clean(); + + return $result; + } + + /** + * Generate the cache configuration options for an element. + * + * @param string $name Element name + * @param array $data Data + * @param array $options Element options + * @return array Element Cache configuration. + * @psalm-return array{key:string, config:string} + */ + protected function _elementCache(string $name, array $data, array $options): array + { + [$plugin, $name] = explode(':', $name, 2); + + $pluginKey = null; + if ($plugin) { + $pluginKey = str_replace('/', '_', $plugin); + } + $elementKey = str_replace(['\\', '/'], '_', $name); + + $cache = $options['cache']; + unset($options['cache']); + $keys = array_merge( + [$pluginKey, $elementKey], + array_keys($options), + array_keys($data) + ); + $config = [ + 'config' => [], + 'key' => implode('_', array_keys($keys)), + ]; + if (is_array($cache)) { + $config = $cache + $config; + } + $config['key'] = 'element_' . $config['key']; + + /** @var array{config: string, key: string} */ + return $config; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10442.php b/tests/PHPStan/Analyser/data/bug-10442.php new file mode 100644 index 0000000000..d8e2f7612c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10442.php @@ -0,0 +1,15 @@ += 8.1 + +namespace Bug10445; + +use function PHPStan\Testing\assertType; + +enum Error { + case A; + case B; + case C; +} + +/** + * @template T of array + */ +class Response +{ + /** + * @param ?T $data + * @param Error[] $errors + */ + public function __construct( + public ?array $data, + public array $errors = [], + ) { + } + + /** + * @return array{ + * result: ?T, + * errors?: string[], + * } + */ + public function format(): array + { + $output = [ + 'result' => $this->data, + ]; + assertType('array{result: T of array (class Bug10445\Response, argument)|null}', $output); + if (count($this->errors) > 0) { + $output['errors'] = array_map(fn ($e) => $e->name, $this->errors); + assertType("array{result: T of array (class Bug10445\Response, argument)|null, errors: non-empty-array<'A'|'B'|'C'>}", $output); + } else { + assertType('array{result: T of array (class Bug10445\Response, argument)|null}', $output); + } + assertType("array{result: T of array (class Bug10445\Response, argument)|null, errors?: non-empty-array<'A'|'B'|'C'>}", $output); + return $output; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10468.php b/tests/PHPStan/Analyser/data/bug-10468.php new file mode 100644 index 0000000000..8a3e30e970 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10468.php @@ -0,0 +1,14 @@ + + */ +class Rows +{ + + /** + * @param list $rowsData + */ + public function __construct(private array $rowsData) + {} + + /** + * @return Row|NULL + */ + public function getByIndex(int $index): ?Row + { + return isset($this->rowsData[$index]) + ? new Row($this->rowsData[$index]) + : NULL; + } +} + +/** + * @template TRow of array + * @implements ArrayAccess, value-of> + */ +class Row implements ArrayAccess +{ + + /** + * @param TRow $data + */ + public function __construct(private array $data) + {} + + /** + * @param key-of $key + */ + public function offsetExists($key): bool + { + return isset($this->data[$key]); + } + + /** + * @template TKey of key-of + * @param TKey $key + * @return TRow[TKey] + */ + public function offsetGet($key): mixed + { + return $this->data[$key]; + } + + public function offsetSet($key, mixed $value): void + { + $this->data[$key] = $value; + } + + public function offsetUnset($key): void + { + unset($this->data[$key]); + } + + /** + * @return TRow + */ + public function toArray(): array + { + return $this->data; + } + +} + +class Foo +{ + + /** @param Rows}> $rows */ + public function doFoo(Rows $rows): void + { + assertType('Bug10473\Rows}>', $rows); + + $row = $rows->getByIndex(0); + + if ($row !== NULL) { + assertType('Bug10473\Row}>', $row); + $fooFromRow = $row['foo']; + + assertType('int<0, max>', $fooFromRow); + assertType('array{foo: int<0, max>}', $row->toArray()); + } + + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-10509.php b/tests/PHPStan/Analyser/data/bug-10509.php new file mode 100644 index 0000000000..cdbf76bd53 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10509.php @@ -0,0 +1,31 @@ + + */ + public function doFoo() + { + + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-10538.php b/tests/PHPStan/Analyser/data/bug-10538.php new file mode 100644 index 0000000000..24fc1f1be2 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10538.php @@ -0,0 +1,44 @@ + 1, + 'BY' => 2, + 'BE' => 3, + 'BB' => 4, + 'HB' => 5, + 'HH' => 6, + 'HE' => 7, + 'MV' => 8, + 'NI' => 9, + 'NW' => 10, + 'RP' => 11, + 'SL' => 12, + 'ST' => 13, + 'SN' => 14, + 'SH' => 15, + 'TH' => 16, + ]; + + protected static function test(): void + { + for ($i = 0; $i < 10; $i++) { + foreach (self::CHANGESET as $stateCode => $changesets) { + $stateId = self::STATES[$stateCode]; + foreach ($changesets as $changeset) { + echo sprintf( + '%s %s %s %s', + $changeset['new']['Gemarkung'], + $changeset['old']['Gemeinde'], + $changeset['old']['Gemarkung'], + $stateId + ); + } + } + } + } + + protected const CHANGESET = ['BB' => [['old' => ['Gemeinde' => 'Alt Zauche - Wußmerk', 'Gemarkung' => 'Alt Zauche'],'new' => ['Gemeinde' => 'Alt Zauche - Wußwerk', 'Gemarkung' => 'Alt Zauche'],],['old' => ['Gemeinde' => 'Alt Zauche - Wußmerk', 'Gemarkung' => 'Wußwerk'],'new' => ['Gemeinde' => 'Alt Zauche - Wußwerk', 'Gemarkung' => 'Wußwerk'],],['old' => ['Gemeinde' => 'Doberlug - Kirchhain', 'Gemarkung' => 'Doberlug-Kirchhainrchhain'],'new' => ['Gemeinde' => 'Doberlug - Kirchhain', 'Gemarkung' => 'Doberlug-Kirchhain'],],],'BE' => [['old' => ['Gemeindeschluessel' => '11000004', 'Gemeinde' => 'Charlottenburg-Wilmersdorf', 'Gemarkung' => 'Charlottenburg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Charlottenburg'],],['old' => ['Gemeindeschluessel' => '11000004', 'Gemeinde' => 'Charlottenburg-Wilmersdorf', 'Gemarkung' => 'Grunewald-Forst'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Grunewald-Forst'],],['old' => ['Gemeindeschluessel' => '11000004', 'Gemeinde' => 'Charlottenburg-Wilmersdorf', 'Gemarkung' => 'Schmargendorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Schmargendorf'],],['old' => ['Gemeindeschluessel' => '11000004', 'Gemeinde' => 'Charlottenburg-Wilmersdorf', 'Gemarkung' => 'Wilmersdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wilmersdorf'],],['old' => ['Gemeindeschluessel' => '11000002', 'Gemeinde' => 'Friedrichshain-Kreuzberg', 'Gemarkung' => 'Friedrichshain'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Friedrichshain'],],['old' => ['Gemeindeschluessel' => '11000002', 'Gemeinde' => 'Friedrichshain-Kreuzberg', 'Gemarkung' => 'Kreuzberg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Kreuzberg'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Falkenberg Gemeinde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Falkenberg Gemeinde'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Falkenberg Gut'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Falkenberg Gut'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Hohenschönhausen'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Hohenschönhausen'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Lichtenberg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Lichtenberg'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Malchow Gemeinde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Malchow Gemeinde'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Malchow Gut'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Malchow Gut'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Wartenberg Gemeinde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wartenberg Gemeinde'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Wartenberg Gut'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wartenberg Gut'],],['old' => ['Gemeindeschluessel' => '11000011', 'Gemeinde' => 'Lichtenberg', 'Gemarkung' => 'Weißensee'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Weißensee'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Ahrensfelde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Ahrensfelde'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Biesdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Biesdorf'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Dahlwitz'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Dahlwitz'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Falkenberg Gemeinde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Falkenberg Gemeinde'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Falkenberg Gut'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Falkenberg Gut'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Friedrichsfelde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Friedrichsfelde'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Hellersdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Hellersdorf'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Kaulsdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Kaulsdorf'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Köpenick'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Köpenick'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Mahlsdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Mahlsdorf'],],['old' => ['Gemeindeschluessel' => '11000010', 'Gemeinde' => 'Marzahn-Hellersdorf', 'Gemarkung' => 'Marzahn'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Marzahn'],],['old' => ['Gemeindeschluessel' => '11000001', 'Gemeinde' => 'Mitte', 'Gemarkung' => 'Mitte'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Mitte'],],['old' => ['Gemeindeschluessel' => '11000001', 'Gemeinde' => 'Mitte', 'Gemarkung' => 'Tiergarten'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Tiergarten'],],['old' => ['Gemeindeschluessel' => '11000001', 'Gemeinde' => 'Mitte', 'Gemarkung' => 'Wedding'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wedding'],],['old' => ['Gemeindeschluessel' => '11000008', 'Gemeinde' => 'Neukölln', 'Gemarkung' => 'Britz'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Britz'],],['old' => ['Gemeindeschluessel' => '11000008', 'Gemeinde' => 'Neukölln', 'Gemarkung' => 'Buckow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Buckow'],],['old' => ['Gemeindeschluessel' => '11000008', 'Gemeinde' => 'Neukölln', 'Gemarkung' => 'Neukölln'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Neukölln'],],['old' => ['Gemeindeschluessel' => '11000008', 'Gemeinde' => 'Neukölln', 'Gemarkung' => 'Rudow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Rudow'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Pankow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Pankow'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Pankow 01'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Pankow 01'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Pankow 02'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Pankow 02'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Pankow 03'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Pankow 03'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Prenzlauer Berg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Prenzlauer Berg'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Weißensee'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Weißensee'],],['old' => ['Gemeindeschluessel' => '11000003', 'Gemeinde' => 'Pankow', 'Gemarkung' => 'Weißensee 01'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Weißensee 01'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Frohnau'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Frohnau'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Heiligensee'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Heiligensee'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Hermsdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Hermsdorf'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Lübars'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Lübars'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Reinickendorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Reinickendorf'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Schulzendorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Schulzendorf'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Tegel-Forst'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Tegel-Forst'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Tegel-Gemeinde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Tegel-Gemeinde'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Tegel-Gut'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Tegel-Gut'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Valentinswerder'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Valentinswerder'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Wilhelmsruh'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wilhelmsruh'],],['old' => ['Gemeindeschluessel' => '11000012', 'Gemeinde' => 'Reinickendorf', 'Gemarkung' => 'Wittenau'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wittenau'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Eiswerder'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Eiswerder'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Gatow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Gatow'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Gewehrplan u. Pulverfabrik'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Gewehrplan u. Pulverfabrik'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Groß-Glienicke'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Groß-Glienicke'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Haselhorst'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Haselhorst'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Heerstraße'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Heerstraße'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Kladow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Kladow'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Klosterfelde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Klosterfelde'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Pichelsdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Pichelsdorf'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Pichelswerder'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Pichelswerder'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Seeburg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Seeburg'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Spandau'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Spandau'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Staaken'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Staaken'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Teufelsbruch'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Teufelsbruch'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Tiefwerder'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Tiefwerder'],],['old' => ['Gemeindeschluessel' => '11000005', 'Gemeinde' => 'Spandau', 'Gemarkung' => 'Zitadelle'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Zitadelle'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Dahlem'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Dahlem'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Düppel'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Düppel'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Lankwitz'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Lankwitz'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Lichterfelde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Lichterfelde'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Nikolassee'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Nikolassee'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Schwanenwerder'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Schwanenwerder'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Steglitz'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Steglitz'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Wannsee'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Wannsee'],],['old' => ['Gemeindeschluessel' => '11000006', 'Gemeinde' => 'Steglitz-Zehlendorf', 'Gemarkung' => 'Zehlendorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Zehlendorf'],],['old' => ['Gemeindeschluessel' => '11000007', 'Gemeinde' => 'Tempelhof-Schöneberg', 'Gemarkung' => 'Friedenau'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Friedenau'],],['old' => ['Gemeindeschluessel' => '11000007', 'Gemeinde' => 'Tempelhof-Schöneberg', 'Gemarkung' => 'Lichtenrade'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Lichtenrade'],],['old' => ['Gemeindeschluessel' => '11000007', 'Gemeinde' => 'Tempelhof-Schöneberg', 'Gemarkung' => 'Mariendorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Mariendorf'],],['old' => ['Gemeindeschluessel' => '11000007', 'Gemeinde' => 'Tempelhof-Schöneberg', 'Gemarkung' => 'Marienfelde'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Marienfelde'],],['old' => ['Gemeindeschluessel' => '11000007', 'Gemeinde' => 'Tempelhof-Schöneberg', 'Gemarkung' => 'Schöneberg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Schöneberg'],],['old' => ['Gemeindeschluessel' => '11000007', 'Gemeinde' => 'Tempelhof-Schöneberg', 'Gemarkung' => 'Tempelhof'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Tempelhof'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Bohnsdorf'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Bohnsdorf'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Britz'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Britz'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Buckow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Buckow'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Fahlenberg'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Fahlenberg'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Glienicke'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Glienicke'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Grünau'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Grünau'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Johannisthal'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Johannisthal'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Kanne'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Kanne'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Köpenick'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Köpenick'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Neukölln'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Neukölln'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Oberschöneweide'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Oberschöneweide'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Rudow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Rudow'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Schmöckwitz'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Schmöckwitz'],],['old' => ['Gemeindeschluessel' => '11000009', 'Gemeinde' => 'Treptow-Köpenick', 'Gemarkung' => 'Treptow'],'new' => ['Gemeindeschluessel' => '11000000', 'Gemeinde' => 'Berlin', 'Gemarkung' => 'Treptow'],],],'HB' => [['old' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Neustadt1'],'new' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Neustadt 1'],],['old' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Neustadt3'],'new' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Neustadt 3'],],['old' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Neustadt4'],'new' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Neustadt 4'],],['old' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Überseehafen', 'Gemarkungsnummer' => '040008'],'new' => ['Gemeinde' => 'Bremen', 'Gemarkung' => 'Überseehafen', 'Gemarkungsnummer' => '040009'],],],'HE' => [['old' => ['Gemeindeschluessel' => '06635005', 'Gemeinde' => 'Bromskirchen', 'Gemarkung' => 'Bromskirchen'],'new' => ['Gemeindeschluessel' => '06635001', 'Gemeinde' => 'Allendorf (Eder)', 'Gemarkung' => 'Bromskirchen'],],['old' => ['Gemeindeschluessel' => '06635005', 'Gemeinde' => 'Bromskirchen', 'Gemarkung' => 'Somplar'],'new' => ['Gemeindeschluessel' => '06635001', 'Gemeinde' => 'Allendorf (Eder)', 'Gemarkung' => 'Somplar'],],['old' => ['Gemeinde' => 'Gelnhausen', 'Gemarkung' => 'Gelnhausen'],'new' => ['Gemeinde' => 'Gelnhausen, Barbarossastadt', 'Gemarkung' => 'Gelnhausen'],],['old' => ['Gemeinde' => 'Gelnhausen', 'Gemarkung' => 'Hailer'],'new' => ['Gemeinde' => 'Gelnhausen, Barbarossastadt', 'Gemarkung' => 'Hailer'],],['old' => ['Gemeinde' => 'Gelnhausen', 'Gemarkung' => 'Haitz'],'new' => ['Gemeinde' => 'Gelnhausen, Barbarossastadt', 'Gemarkung' => 'Haitz'],],['old' => ['Gemeinde' => 'Gelnhausen', 'Gemarkung' => 'Höchst'],'new' => ['Gemeinde' => 'Gelnhausen, Barbarossastadt', 'Gemarkung' => 'Höchst'],],['old' => ['Gemeinde' => 'Gelnhausen', 'Gemarkung' => 'Meerholz'],'new' => ['Gemeinde' => 'Gelnhausen, Barbarossastadt', 'Gemarkung' => 'Meerholz'],],['old' => ['Gemeinde' => 'Gelnhausen', 'Gemarkung' => 'Roth'],'new' => ['Gemeinde' => 'Gelnhausen, Barbarossastadt', 'Gemarkung' => 'Roth'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Ahlbach'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Ahlbach'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Dietkirchen'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Dietkirchen'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Eschhofen'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Eschhofen'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Limburg'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Limburg'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Lindenholzhausen'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Lindenholzhausen'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Linter'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Linter'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Offheim'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Offheim'],],['old' => ['Gemeinde' => 'Limburg a.d. Lahn', 'Gemarkung' => 'Staffel'],'new' => ['Gemeinde' => 'Limburg a. d. Lahn', 'Gemarkung' => 'Staffel'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Arnoldshain'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Arnoldshain'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Brombach'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Brombach'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Dorfweil'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Dorfweil'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Hunoldstal'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Hunoldstal'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Niederreifenberg'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Niederreifenberg'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Oberreifenberg'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Oberreifenberg'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Schmitten'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Schmitten'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Seelenberg'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Seelenberg'],],['old' => ['Gemeinde' => 'Schmitten', 'Gemarkung' => 'Treisberg'],'new' => ['Gemeinde' => 'Schmitten im Taunus', 'Gemarkung' => 'Treisberg'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Alraft'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Alraft'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Dehringhausen'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Dehringhausen'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Freienhagen'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Freienhagen'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Höringhausen'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Höringhausen'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Netze'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Netze'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Nieder-Werbe'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Nieder-Werbe'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Ober-Werbe'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Ober-Werbe'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Oberwerba'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Oberwerba'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Sachsenhausen'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Sachsenhausen'],],['old' => ['Gemeinde' => 'Waldeck', 'Gemarkung' => 'Waldeck'],'new' => ['Gemeinde' => 'Waldeck, Nationalparkstadt', 'Gemarkung' => 'Waldeck'],],],'MV' => [['old' => ['Gemeinde' => 'Neubukow, Stadt', 'Gemarkung' => 'Buschmühlen'],'new' => ['Gemeinde' => 'Neubukow, Schliemannstadt', 'Gemarkung' => 'Buschmühlen'],],['old' => ['Gemeinde' => 'Neubukow, Stadt', 'Gemarkung' => 'Malpendorf'],'new' => ['Gemeinde' => 'Neubukow, Schliemannstadt', 'Gemarkung' => 'Malpendorf'],],['old' => ['Gemeinde' => 'Neubukow, Stadt', 'Gemarkung' => 'Neubukow'],'new' => ['Gemeinde' => 'Neubukow, Schliemannstadt', 'Gemarkung' => 'Neubukow'],],['old' => ['Gemeinde' => 'Neubukow, Stadt', 'Gemarkung' => 'Panzow'],'new' => ['Gemeinde' => 'Neubukow, Schliemannstadt', 'Gemarkung' => 'Panzow'],],['old' => ['Gemeinde' => 'Neubukow, Stadt', 'Gemarkung' => 'Spriehusen'],'new' => ['Gemeinde' => 'Neubukow, Schliemannstadt', 'Gemarkung' => 'Spriehusen'],],['old' => ['Gemeinde' => 'Tessin, Stadt', 'Gemarkung' => 'Helmstorf'],'new' => ['Gemeinde' => 'Tessin, Blumenstadt', 'Gemarkung' => 'Helmstorf'],],['old' => ['Gemeinde' => 'Tessin, Stadt', 'Gemarkung' => 'Klein Tessin'],'new' => ['Gemeinde' => 'Tessin, Blumenstadt', 'Gemarkung' => 'Klein Tessin'],],['old' => ['Gemeinde' => 'Tessin, Stadt', 'Gemarkung' => 'Tessin'],'new' => ['Gemeinde' => 'Tessin, Blumenstadt', 'Gemarkung' => 'Tessin'],],['old' => ['Gemeinde' => 'Tessin, Stadt', 'Gemarkung' => 'Vilz'],'new' => ['Gemeinde' => 'Tessin, Blumenstadt', 'Gemarkung' => 'Vilz'],],],'NI' => [['old' => ['Gemeindeschluessel' => '03153006', 'Gemeinde' => 'Hahausen', 'Gemarkung' => 'Hahausen'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Hahausen'],],['old' => ['Gemeindeschluessel' => '03153007', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Astfeld'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Astfeld'],],['old' => ['Gemeindeschluessel' => '03153007', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Bredelem'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Bredelem'],],['old' => ['Gemeindeschluessel' => '03153007', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Langelsheim'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Langelsheim'],],['old' => ['Gemeindeschluessel' => '03153007', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Lautenthal'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Lautenthal'],],['old' => ['Gemeindeschluessel' => '03153007', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Wolfshagen'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Wolfshagen'],],['old' => ['Gemeindeschluessel' => '03153007', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Wolfshagen-Mispliet'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Wolfshagen-Mispliet'],],['old' => ['Gemeindeschluessel' => '03153009', 'Gemeinde' => 'Lutter am Barenb., Flecken', 'Gemarkung' => 'Lutter am Barenberge'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Lutter am Barenberge'],],['old' => ['Gemeindeschluessel' => '03153009', 'Gemeinde' => 'Lutter am Barenb., Flecken', 'Gemarkung' => 'Lutter-Westerberg'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Lutter-Westerberg'],],['old' => ['Gemeindeschluessel' => '03153009', 'Gemeinde' => 'Lutter am Barenb., Flecken', 'Gemarkung' => 'Nauen'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Nauen'],],['old' => ['Gemeindeschluessel' => '03153009', 'Gemeinde' => 'Lutter am Barenb., Flecken', 'Gemarkung' => 'Ostlutter'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Ostlutter'],],['old' => ['Gemeindeschluessel' => '03153014', 'Gemeinde' => 'Wallmoden', 'Gemarkung' => 'Alt Wallmoden'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Alt Wallmoden'],],['old' => ['Gemeindeschluessel' => '03153014', 'Gemeinde' => 'Wallmoden', 'Gemarkung' => 'Bodenstein'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Bodenstein'],],['old' => ['Gemeindeschluessel' => '03153014', 'Gemeinde' => 'Wallmoden', 'Gemarkung' => 'Neuwallmoden'],'new' => ['Gemeindeschluessel' => '03153019', 'Gemeinde' => 'Langelsheim, Stadt', 'Gemarkung' => 'Neuwallmoden'],],],'ST' => [['old' => ['Gemeinde' => 'Bad Dürrenberg, Stadt', 'Gemarkung' => 'Nempitz'],'new' => ['Gemeinde' => 'Bad Dürrenberg, Solestadt', 'Gemarkung' => 'Nempitz'],],['old' => ['Gemeinde' => 'Bad Dürrenberg, Stadt', 'Gemarkung' => 'Oebles-Schlechtewitz'],'new' => ['Gemeinde' => 'Bad Dürrenberg, Solestadt', 'Gemarkung' => 'Oebles-Schlechtewitz'],],['old' => ['Gemeinde' => 'Bad Dürrenberg, Stadt', 'Gemarkung' => 'Tollwitz'],'new' => ['Gemeinde' => 'Bad Dürrenberg, Solestadt', 'Gemarkung' => 'Tollwitz'],],['old' => ['Gemeinde' => 'Harsleben', 'Gemarkung' => 'Harsleben'],'new' => ['Gemeinde' => 'Harsleben / Harschlewe', 'Gemarkung' => 'Harsleben'],],['old' => ['Gemeindeschluessel' => '15083575', 'Gemeinde' => 'Westheide', 'Gemarkung' => 'Born'],'new' => ['Gemeindeschluessel' => '15083557', 'Gemeinde' => 'Westheide', 'Gemarkung' => 'Born'],],],'TH' => [['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Berteroda'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Berteroda'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Eisenach'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Eisenach'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Frohnishof'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Frohnishof'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Göringen'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Göringen'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Hörschel'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Hörschel'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Hötzelsroda'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Hötzelsroda'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Madelungen'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Madelungen'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Neuenhof'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Neuenhof'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Neukirchen'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Neukirchen'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Stedtfeld'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Stedtfeld'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Stockhausen'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Stockhausen'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Stregda'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Stregda'],],['old' => ['Gemeindeschluessel' => '16056000', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Wartha'],'new' => ['Gemeindeschluessel' => '16063105', 'Gemeinde' => 'Eisenach', 'Gemarkung' => 'Wartha'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Bliederstedt'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Bliederstedt'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Feldengel'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Feldengel'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Großenehrich'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Großenehrich'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Holzengel'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Holzengel'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Kirchengel'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Kirchengel'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Niederspier'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Niederspier'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Otterstedt'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Otterstedt'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Rohnstedt'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Rohnstedt'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Wenigenehrich'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Wenigenehrich'],],['old' => ['Gemeinde' => 'Großenehrich', 'Gemarkung' => 'Westerengel'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Westerengel'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Etterwinden'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Etterwinden'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Gräfen-Nitzendorf'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Gräfen-Nitzendorf'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Gumpelstadt'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Gumpelstadt'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Kupfersuhl'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Kupfersuhl'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Möhra'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Möhra'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Neuendorf'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Neuendorf'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Wackenhof'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Wackenhof'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Waldfisch'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Waldfisch'],],['old' => ['Gemeindeschluessel' => '16063094', 'Gemeinde' => 'Moorgrund', 'Gemarkung' => 'Witzelroda'],'new' => ['Gemeindeschluessel' => '16063003', 'Gemeinde' => 'Bad Salzungen', 'Gemarkung' => 'Witzelroda'],],['old' => ['Gemeinde' => 'Roßleben-Wiehe, Stadt', 'Gemarkung' => 'Bottendorf'],'new' => ['Gemeinde' => 'Roßleben-Wiehe', 'Gemarkung' => 'Bottendorf'],],['old' => ['Gemeinde' => 'Wolferschwenda', 'Gemarkung' => 'Wolferschwenda'],'new' => ['Gemeinde' => 'Greußen', 'Gemarkung' => 'Wolferschwenda'],],],]; +} diff --git a/tests/PHPStan/Analyser/data/bug-10566.php b/tests/PHPStan/Analyser/data/bug-10566.php new file mode 100644 index 0000000000..2fb46c5a7f --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10566.php @@ -0,0 +1,171 @@ +running = true; + + while ($this->running) { + assertType('true', $this->running); + call_user_func(function () { + $this->stop(); + }); + assertType('bool', $this->running); + + if (!$this->running) { + $timeout = 0; + break; + } + } + } + + public function stop(): void + { + $this->running = false; + } + + public function run2(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + call_user_func(function () use ($s) { + $s->stop(); + }); + assertType('bool', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + + public function run3(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + call_user_func(function () { + $s = new self(); + $s->stop(); + }); + assertType('true', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + + public function run4(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + $cb = function () use ($s) { + $s = new self(); + $s->stop(); + }; + assertType('true', $s->running); + call_user_func($cb); + assertType('bool', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + + public function run5(): void + { + $this->running = true; + + while ($this->running) { + assertType('true', $this->running); + $cb = function () { + $this->stop(); + }; + assertType('true', $this->running); + call_user_func($cb); + assertType('bool', $this->running); + + if (!$this->running) { + $timeout = 0; + break; + } + } + } + + public function run6(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + (function () use ($s) { + $s = new self(); + $s->stop(); + })(); + assertType('bool', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + + public function run7(): void + { + $this->running = true; + + while ($this->running) { + assertType('true', $this->running); + (function () { + $this->stop(); + })(); + assertType('bool', $this->running); + + if (!$this->running) { + $timeout = 0; + break; + } + } + } + + function run8(): void + { + $s = new self(); + + while ($s->running) { + assertType('true', $s->running); + call_user_func(static function () use ($s) { + $s->stop(); + }); + assertType('bool', $s->running); + + if (!$s->running) { + $timeout = 0; + break; + } + } + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-10627.php b/tests/PHPStan/Analyser/data/bug-10627.php new file mode 100644 index 0000000000..17579ec52c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10627.php @@ -0,0 +1,95 @@ + $list + * @return void + */ + public function sayHello9(array $list): void + { + krsort($list); + assertType("array, string>", $list); + } + + public function sayHello10(): void + { + $list = ['a' => 'A', 'c' => 'C', 'b' => 'B']; + krsort($list); + assertType("array{a: 'A', c: 'C', b: 'B'}", $list); + assertType('false', array_is_list($list)); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10699.php b/tests/PHPStan/Analyser/data/bug-10699.php new file mode 100644 index 0000000000..de06986e90 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10699.php @@ -0,0 +1,49 @@ +getNameInLanguage(LanguageAlpha2::English); + + // part B, all right + // $value->toCountryAlpha2()->getNameInLanguage(LanguageAlpha2::English); +} + +enum CountryAlpha3: string +{ + case Afghanistan = 'AFG'; + case Aland_Islands = 'ALA'; + case Albania = 'ALB'; + case Algeria = 'DZA'; + case American_Samoa = 'ASM'; + case Andorra = 'AND'; + case Angola = 'AGO'; + case Anguilla = 'AIA'; + case Antarctica = 'ATA'; + case Antigua_and_Barbuda = 'ATG'; + case Argentina = 'ARG'; + case Armenia = 'ARM'; + case Aruba = 'ABW'; + case Australia = 'AUS'; + case Austria = 'AUT'; + case Azerbaijan = 'AZE'; + case Bahamas = 'BHS'; + case Bahrain = 'BHR'; + case Bangladesh = 'BGD'; + case Barbados = 'BRB'; + case Belarus = 'BLR'; + case Belgium = 'BEL'; + case Belize = 'BLZ'; + case Benin = 'BEN'; + case Bermuda = 'BMU'; + case Bhutan = 'BTN'; + case Bolivia = 'BOL'; + case Bonaire_Sint_Eustatius_and_Saba = 'BES'; + case Bosnia_and_Herzegovina = 'BIH'; + case Botswana = 'BWA'; + case Bouvet_Island = 'BVT'; + case Brazil = 'BRA'; + case British_Indian_Ocean_Territory = 'IOT'; + case Brunei_Darussalam = 'BRN'; + case Bulgaria = 'BGR'; + case Burkina_Faso = 'BFA'; + case Burundi = 'BDI'; + case Cabo_Verde = 'CPV'; + case Cambodia = 'KHM'; + case Cameroon = 'CMR'; + case Canada = 'CAN'; + case Cayman_Islands = 'CYM'; + case Central_African_Republic = 'CAF'; + case Chad = 'TCD'; + case Chile = 'CHL'; + case China = 'CHN'; + case Christmas_Island = 'CXR'; + case Cocos_Islands = 'CCK'; + case Colombia = 'COL'; + case Comoros = 'COM'; + case Congo = 'COG'; + case Congo_Democratic_Republic = 'COD'; + case Cook_Islands = 'COK'; + case Costa_Rica = 'CRI'; + case Cote_d_Ivoire = 'CIV'; + case Croatia = 'HRV'; + case Cuba = 'CUB'; + case Curacao = 'CUW'; + case Cyprus = 'CYP'; + case Czechia = 'CZE'; + case Denmark = 'DNK'; + case Djibouti = 'DJI'; + case Dominica = 'DMA'; + case Dominican_Republic = 'DOM'; + case Ecuador = 'ECU'; + case Egypt = 'EGY'; + case El_Salvador = 'SLV'; + case Equatorial_Guinea = 'GNQ'; + case Eritrea = 'ERI'; + case Estonia = 'EST'; + case Eswatini = 'SWZ'; + case Ethiopia = 'ETH'; + case Falkland_Islands = 'FLK'; + case Faroe_Islands = 'FRO'; + case Fiji = 'FJI'; + case Finland = 'FIN'; + case France = 'FRA'; + case French_Guiana = 'GUF'; + case French_Polynesia = 'PYF'; + case French_Southern_Territories = 'ATF'; + case Gabon = 'GAB'; + case Gambia = 'GMB'; + case Georgia = 'GEO'; + case Germany = 'DEU'; + case Ghana = 'GHA'; + case Gibraltar = 'GIB'; + case Greece = 'GRC'; + case Greenland = 'GRL'; + case Grenada = 'GRD'; + case Guadeloupe = 'GLP'; + case Guam = 'GUM'; + case Guatemala = 'GTM'; + case Guernsey = 'GGY'; + case Guinea = 'GIN'; + case Guinea_Bissau = 'GNB'; + case Guyana = 'GUY'; + case Haiti = 'HTI'; + case Heard_Island_and_McDonald_Islands = 'HMD'; + case Holy_See = 'VAT'; + case Honduras = 'HND'; + case Hong_Kong = 'HKG'; + case Hungary = 'HUN'; + case Iceland = 'ISL'; + case India = 'IND'; + case Indonesia = 'IDN'; + case Iran = 'IRN'; + case Iraq = 'IRQ'; + case Ireland = 'IRL'; + case Isle_of_Man = 'IMN'; + case Israel = 'ISR'; + case Italy = 'ITA'; + case Jamaica = 'JAM'; + case Japan = 'JPN'; + case Jersey = 'JEY'; + case Jordan = 'JOR'; + case Kazakhstan = 'KAZ'; + case Kenya = 'KEN'; + case Kiribati = 'KIR'; + case Korea_Democratic_Peoples_Republic = 'PRK'; + case Korea_Republic = 'KOR'; + case Kuwait = 'KWT'; + case Kyrgyzstan = 'KGZ'; + case Lao_Peoples_Democratic_Republic = 'LAO'; + case Latvia = 'LVA'; + case Lebanon = 'LBN'; + case Lesotho = 'LSO'; + case Liberia = 'LBR'; + case Libya = 'LBY'; + case Liechtenstein = 'LIE'; + case Lithuania = 'LTU'; + case Luxembourg = 'LUX'; + case Macao = 'MAC'; + case Madagascar = 'MDG'; + case Malawi = 'MWI'; + case Malaysia = 'MYS'; + case Maldives = 'MDV'; + case Mali = 'MLI'; + case Malta = 'MLT'; + case Marshall_Islands = 'MHL'; + case Martinique = 'MTQ'; + case Mauritania = 'MRT'; + case Mauritius = 'MUS'; + case Mayotte = 'MYT'; + case Mexico = 'MEX'; + case Micronesia = 'FSM'; + case Moldova = 'MDA'; + case Monaco = 'MCO'; + case Mongolia = 'MNG'; + case Montenegro = 'MNE'; + case Montserrat = 'MSR'; + case Morocco = 'MAR'; + case Mozambique = 'MOZ'; + case Myanmar = 'MMR'; + case Namibia = 'NAM'; + case Nauru = 'NRU'; + case Nepal = 'NPL'; + case Netherlands = 'NLD'; + case New_Caledonia = 'NCL'; + case New_Zealand = 'NZL'; + case Nicaragua = 'NIC'; + case Niger = 'NER'; + case Nigeria = 'NGA'; + case Niue = 'NIU'; + case Norfolk_Island = 'NFK'; + case North_Macedonia = 'MKD'; + case Northern_Mariana_Islands = 'MNP'; + case Norway = 'NOR'; + case Oman = 'OMN'; + case Pakistan = 'PAK'; + case Palau = 'PLW'; + case Palestine = 'PSE'; + case Panama = 'PAN'; + case Papua_New_Guinea = 'PNG'; + case Paraguay = 'PRY'; + case Peru = 'PER'; + case Philippines = 'PHL'; + case Pitcairn = 'PCN'; + case Poland = 'POL'; + case Portugal = 'PRT'; + case Puerto_Rico = 'PRI'; + case Qatar = 'QAT'; + case Reunion = 'REU'; + case Romania = 'ROU'; + case Russian_Federation = 'RUS'; + case Rwanda = 'RWA'; + case Saint_Barthelemy = 'BLM'; + case Saint_Helena_Ascension_Tristan_da_Cunha = 'SHN'; + case Saint_Kitts_and_Nevis = 'KNA'; + case Saint_Lucia = 'LCA'; + case Saint_Martin_French_part = 'MAF'; + case Saint_Pierre_and_Miquelon = 'SPM'; + case Saint_Vincent_and_the_Grenadines = 'VCT'; + case Samoa = 'WSM'; + case San_Marino = 'SMR'; + case Sao_Tome_and_Principe = 'STP'; + case Saudi_Arabia = 'SAU'; + case Senegal = 'SEN'; + case Serbia = 'SRB'; + case Seychelles = 'SYC'; + case Sierra_Leone = 'SLE'; + case Singapore = 'SGP'; + case Sint_Maarten_Dutch_part = 'SXM'; + case Slovakia = 'SVK'; + case Slovenia = 'SVN'; + case Solomon_Islands = 'SLB'; + case Somalia = 'SOM'; + case South_Africa = 'ZAF'; + case South_Georgia_South_Sandwich_Islands = 'SGS'; + case South_Sudan = 'SSD'; + case Spain = 'ESP'; + case Sri_Lanka = 'LKA'; + case Sudan = 'SDN'; + case Suriname = 'SUR'; + case Svalbard_Jan_Mayen = 'SJM'; + case Sweden = 'SWE'; + case Switzerland = 'CHE'; + case Syrian_Arab_Republic = 'SYR'; + case Taiwan_Province_of_China = 'TWN'; + case Tajikistan = 'TJK'; + case Tanzania = 'TZA'; + case Thailand = 'THA'; + case Timor_Leste = 'TLS'; + case Togo = 'TGO'; + case Tokelau = 'TKL'; + case Tonga = 'TON'; + case Trinidad_and_Tobago = 'TTO'; + case Tunisia = 'TUN'; + case Turkey = 'TUR'; + case Turkmenistan = 'TKM'; + case Turks_and_Caicos_Islands = 'TCA'; + case Tuvalu = 'TUV'; + case Uganda = 'UGA'; + case Ukraine = 'UKR'; + case United_Arab_Emirates = 'ARE'; + case United_Kingdom = 'GBR'; + case United_States_Outlying_Islands = 'UMI'; + case United_States_of_America = 'USA'; + case Uruguay = 'URY'; + case Uzbekistan = 'UZB'; + case Vanuatu = 'VUT'; + case Venezuela = 'VEN'; + case Viet_Nam = 'VNM'; + case Virgin_Islands_British = 'VGB'; + case Virgin_Islands_U_S = 'VIR'; + case Wallis_and_Futuna = 'WLF'; + case Western_Sahara = 'ESH'; + case Yemen = 'YEM'; + case Zambia = 'ZMB'; + case Zimbabwe = 'ZWE'; + + + + public function getNameInLanguage(LanguageAlpha2|LanguageAlpha3Terminology|LanguageAlpha3Bibliographic|LanguageAlpha3Extensive $language): ?string + { + return $this->toCountryAlpha2()->getNameInLanguage($language); + } + + public function toCountryAlpha2(): mixed + { + return BackedEnum::fromName('x', $this->name); + } +} + +enum LanguageAlpha2: string +{ + case Abkhazian = 'ab'; + case Afar = 'aa'; + case Afrikaans = 'af'; + case Akan = 'ak'; + case Albanian = 'sq'; + case Amharic = 'am'; + case Arabic = 'ar'; + case Aragonese = 'an'; + case Armenian = 'hy'; + case Assamese = 'as'; + case Avaric = 'av'; + case Avestan = 'ae'; + case Aymara = 'ay'; + case Azerbaijani = 'az'; + case Bambara = 'bm'; + case Bashkir = 'ba'; + case Basque = 'eu'; + case Belarusian = 'be'; + case Bengali = 'bn'; + case Bihari_languages = 'bh'; + case Bislama = 'bi'; + case Bokmal_Norwegian_Norwegian_Bokmal = 'nb'; + case Bosnian = 'bs'; + case Breton = 'br'; + case Bulgarian = 'bg'; + case Burmese = 'my'; + case Catalan_Valencian = 'ca'; + case Central_Khmer = 'km'; + case Chamorro = 'ch'; + case Chechen = 'ce'; + case Chichewa_Chewa_Nyanja = 'ny'; + case Chinese = 'zh'; + case Church_Slavic_Old_Slavonic_Church_Slavonic_Old_Bulgarian_Old_Church_Slavonic = 'cu'; + case Chuvash = 'cv'; + case Cornish = 'kw'; + case Corsican = 'co'; + case Cree = 'cr'; + case Croatian = 'hr'; + case Czech = 'cs'; + case Danish = 'da'; + case Divehi_Dhivehi_Maldivian = 'dv'; + case Dutch_Flemish = 'nl'; + case Dzongkha = 'dz'; + case English = 'en'; + case Esperanto = 'eo'; + case Estonian = 'et'; + case Ewe = 'ee'; + case Faroese = 'fo'; + case Fijian = 'fj'; + case Finnish = 'fi'; + case French = 'fr'; + case Fulah = 'ff'; + case Gaelic_Scottish_Gaelic = 'gd'; + case Galician = 'gl'; + case Ganda = 'lg'; + case Georgian = 'ka'; + case German = 'de'; + case Greek_Modern_1453 = 'el'; + case Guarani = 'gn'; + case Gujarati = 'gu'; + case Haitian_Haitian_Creole = 'ht'; + case Hausa = 'ha'; + case Hebrew = 'he'; + case Herero = 'hz'; + case Hindi = 'hi'; + case Hiri_Motu = 'ho'; + case Hungarian = 'hu'; + case Icelandic = 'is'; + case Ido = 'io'; + case Igbo = 'ig'; + case Indonesian = 'id'; + case Interlingua_International_Auxiliary_Language_Association = 'ia'; + case Interlingue_Occidental = 'ie'; + case Inuktitut = 'iu'; + case Inupiaq = 'ik'; + case Irish = 'ga'; + case Italian = 'it'; + case Japanese = 'ja'; + case Javanese = 'jv'; + case Kalaallisut_Greenlandic = 'kl'; + case Kannada = 'kn'; + case Kanuri = 'kr'; + case Kashmiri = 'ks'; + case Kazakh = 'kk'; + case Kikuyu_Gikuyu = 'ki'; + case Kinyarwanda = 'rw'; + case Kirghiz_Kyrgyz = 'ky'; + case Komi = 'kv'; + case Kongo = 'kg'; + case Korean = 'ko'; + case Kuanyama_Kwanyama = 'kj'; + case Kurdish = 'ku'; + case Lao = 'lo'; + case Latin = 'la'; + case Latvian = 'lv'; + case Limburgan_Limburger_Limburgish = 'li'; + case Lingala = 'ln'; + case Lithuanian = 'lt'; + case Luba_Katanga = 'lu'; + case Luxembourgish_Letzeburgesch = 'lb'; + case Macedonian = 'mk'; + case Malagasy = 'mg'; + case Malay = 'ms'; + case Malayalam = 'ml'; + case Maltese = 'mt'; + case Manx = 'gv'; + case Maori = 'mi'; + case Marathi = 'mr'; + case Marshallese = 'mh'; + case Mongolian = 'mn'; + case Nauru = 'na'; + case Navajo_Navaho = 'nv'; + case Ndebele_North_North_Ndebele = 'nd'; + case Ndebele_South_South_Ndebele = 'nr'; + case Ndonga = 'ng'; + case Nepali = 'ne'; + case Northern_Sami = 'se'; + case Norwegian = 'no'; + case Norwegian_Nynorsk_Nynorsk_Norwegian = 'nn'; + case Occitan_post_1500 = 'oc'; + case Ojibwa = 'oj'; + case Oriya = 'or'; + case Oromo = 'om'; + case Ossetian_Ossetic = 'os'; + case Pali = 'pi'; + case Panjabi_Punjabi = 'pa'; + case Persian = 'fa'; + case Polish = 'pl'; + case Portuguese = 'pt'; + case Pushto_Pashto = 'ps'; + case Quechua = 'qu'; + case Romanian_Moldavian_Moldovan = 'ro'; + case Romansh = 'rm'; + case Rundi = 'rn'; + case Russian = 'ru'; + case Samoan = 'sm'; + case Sango = 'sg'; + case Sanskrit = 'sa'; + case Sardinian = 'sc'; + case Serbian = 'sr'; + case Shona = 'sn'; + case Sichuan_Yi_Nuosu = 'ii'; + case Sindhi = 'sd'; + case Sinhala_Sinhalese = 'si'; + case Slovak = 'sk'; + case Slovenian = 'sl'; + case Somali = 'so'; + case Sotho_Southern = 'st'; + case Spanish_Castilian = 'es'; + case Sundanese = 'su'; + case Swahili = 'sw'; + case Swati = 'ss'; + case Swedish = 'sv'; + case Tagalog = 'tl'; + case Tahitian = 'ty'; + case Tajik = 'tg'; + case Tamil = 'ta'; + case Tatar = 'tt'; + case Telugu = 'te'; + case Thai = 'th'; + case Tibetan = 'bo'; + case Tigrinya = 'ti'; + case Tonga_Tonga_Islands = 'to'; + case Tsonga = 'ts'; + case Tswana = 'tn'; + case Turkish = 'tr'; + case Turkmen = 'tk'; + case Twi = 'tw'; + case Uighur_Uyghur = 'ug'; + case Ukrainian = 'uk'; + case Urdu = 'ur'; + case Uzbek = 'uz'; + case Venda = 've'; + case Vietnamese = 'vi'; + case Volapuk = 'vo'; + case Walloon = 'wa'; + case Welsh = 'cy'; + case Western_Frisian = 'fy'; + case Wolof = 'wo'; + case Xhosa = 'xh'; + case Yiddish = 'yi'; + case Yoruba = 'yo'; + case Zhuang_Chuang = 'za'; + case Zulu = 'zu'; + + /** @deprecated Will be removed in v4. Please use ::getNameInLanguage(LanguageAlpha2::English) instead */ + public function toLanguageName(): LanguageName + { + return BackedEnum::fromName(LanguageName::class, $this->name); + } +} + +enum LanguageAlpha3Terminology: string +{ + case Abkhazian = 'abk'; + case Achinese = 'ace'; + case Acoli = 'ach'; + case Adangme = 'ada'; + case Adyghe_Adygei = 'ady'; + case Afar = 'aar'; + case Afrihili = 'afh'; + case Afrikaans = 'afr'; + case Afro_Asiatic_languages = 'afa'; + case Ainu = 'ain'; + case Akan = 'aka'; + case Akkadian = 'akk'; + case Albanian = 'sqi'; + case Aleut = 'ale'; + case Algonquian_languages = 'alg'; + case Altaic_languages = 'tut'; + case Amharic = 'amh'; + case Angika = 'anp'; + case Apache_languages = 'apa'; + case Arabic = 'ara'; + case Aragonese = 'arg'; + case Arapaho = 'arp'; + case Arawak = 'arw'; + case Armenian = 'hye'; + case Aromanian_Arumanian_Macedo_Romanian = 'rup'; + case Artificial_languages = 'art'; + case Assamese = 'asm'; + case Asturian_Bable_Leonese_Asturleonese = 'ast'; + case Athapascan_languages = 'ath'; + case Australian_languages = 'aus'; + case Austronesian_languages = 'map'; + case Avaric = 'ava'; + case Avestan = 'ave'; + case Awadhi = 'awa'; + case Aymara = 'aym'; + case Azerbaijani = 'aze'; + case Balinese = 'ban'; + case Baltic_languages = 'bat'; + case Baluchi = 'bal'; + case Bambara = 'bam'; + case Bamileke_languages = 'bai'; + case Banda_languages = 'bad'; + case Bantu_languages = 'bnt'; + case Basa = 'bas'; + case Bashkir = 'bak'; + case Basque = 'eus'; + case Batak_languages = 'btk'; + case Beja_Bedawiyet = 'bej'; + case Belarusian = 'bel'; + case Bemba = 'bem'; + case Bengali = 'ben'; + case Berber_languages = 'ber'; + case Bhojpuri = 'bho'; + case Bihari_languages = 'bih'; + case Bikol = 'bik'; + case Bini_Edo = 'bin'; + case Bislama = 'bis'; + case Blin_Bilin = 'byn'; + case Blissymbols_Blissymbolics_Bliss = 'zbl'; + case Bokmal_Norwegian_Norwegian_Bokmal = 'nob'; + case Bosnian = 'bos'; + case Braj = 'bra'; + case Breton = 'bre'; + case Buginese = 'bug'; + case Bulgarian = 'bul'; + case Buriat = 'bua'; + case Burmese = 'mya'; + case Caddo = 'cad'; + case Catalan_Valencian = 'cat'; + case Caucasian_languages = 'cau'; + case Cebuano = 'ceb'; + case Celtic_languages = 'cel'; + case Central_American_Indian_languages = 'cai'; + case Central_Khmer = 'khm'; + case Chagatai = 'chg'; + case Chamic_languages = 'cmc'; + case Chamorro = 'cha'; + case Chechen = 'che'; + case Cherokee = 'chr'; + case Cheyenne = 'chy'; + case Chibcha = 'chb'; + case Chichewa_Chewa_Nyanja = 'nya'; + case Chinese = 'zho'; + case Chinook_jargon = 'chn'; + case Chipewyan_Dene_Suline = 'chp'; + case Choctaw = 'cho'; + case Church_Slavic_Old_Slavonic_Church_Slavonic_Old_Bulgarian_Old_Church_Slavonic = 'chu'; + case Chuukese = 'chk'; + case Chuvash = 'chv'; + case Classical_Newari_Old_Newari_Classical_Nepal_Bhasa = 'nwc'; + case Classical_Syriac = 'syc'; + case Coptic = 'cop'; + case Cornish = 'cor'; + case Corsican = 'cos'; + case Cree = 'cre'; + case Creek = 'mus'; + case Creoles_and_pidgins = 'crp'; + case Creoles_and_pidgins_English_based = 'cpe'; + case Creoles_and_pidgins_French_based = 'cpf'; + case Creoles_and_pidgins_Portuguese_based = 'cpp'; + case Crimean_Tatar_Crimean_Turkish = 'crh'; + case Croatian = 'hrv'; + case Cushitic_languages = 'cus'; + case Czech = 'ces'; + case Dakota = 'dak'; + case Danish = 'dan'; + case Dargwa = 'dar'; + case Delaware = 'del'; + case Dinka = 'din'; + case Divehi_Dhivehi_Maldivian = 'div'; + case Dogri = 'doi'; + case Dogrib = 'dgr'; + case Dravidian_languages = 'dra'; + case Duala = 'dua'; + case Dutch_Flemish = 'nld'; + case Dutch_Middle_ca_1050_1350 = 'dum'; + case Dyula = 'dyu'; + case Dzongkha = 'dzo'; + case Eastern_Frisian = 'frs'; + case Efik = 'efi'; + case Egyptian_Ancient = 'egy'; + case Ekajuk = 'eka'; + case Elamite = 'elx'; + case English = 'eng'; + case English_Middle_1100_1500 = 'enm'; + case English_Old_ca_450_1100 = 'ang'; + case Erzya = 'myv'; + case Esperanto = 'epo'; + case Estonian = 'est'; + case Ewe = 'ewe'; + case Ewondo = 'ewo'; + case Fang = 'fan'; + case Fanti = 'fat'; + case Faroese = 'fao'; + case Fijian = 'fij'; + case Filipino_Pilipino = 'fil'; + case Finnish = 'fin'; + case Finno_Ugrian_languages = 'fiu'; + case Fon = 'fon'; + case French = 'fra'; + case French_Middle_ca_1400_1600 = 'frm'; + case French_Old_842_ca_1400 = 'fro'; + case Friulian = 'fur'; + case Fulah = 'ful'; + case Ga = 'gaa'; + case Gaelic_Scottish_Gaelic = 'gla'; + case Galibi_Carib = 'car'; + case Galician = 'glg'; + case Ganda = 'lug'; + case Gayo = 'gay'; + case Gbaya = 'gba'; + case Geez = 'gez'; + case Georgian = 'kat'; + case German = 'deu'; + case German_Middle_High_ca_1050_1500 = 'gmh'; + case German_Old_High_ca_750_1050 = 'goh'; + case Germanic_languages = 'gem'; + case Gilbertese = 'gil'; + case Gondi = 'gon'; + case Gorontalo = 'gor'; + case Gothic = 'got'; + case Grebo = 'grb'; + case Greek_Ancient_to_1453 = 'grc'; + case Greek_Modern_1453 = 'ell'; + case Guarani = 'grn'; + case Gujarati = 'guj'; + case Gwich_in = 'gwi'; + case Haida = 'hai'; + case Haitian_Haitian_Creole = 'hat'; + case Hausa = 'hau'; + case Hawaiian = 'haw'; + case Hebrew = 'heb'; + case Herero = 'her'; + case Hiligaynon = 'hil'; + case Himachali_languages_Western_Pahari_languages = 'him'; + case Hindi = 'hin'; + case Hiri_Motu = 'hmo'; + case Hittite = 'hit'; + case Hmong_Mong = 'hmn'; + case Hungarian = 'hun'; + case Hupa = 'hup'; + case Iban = 'iba'; + case Icelandic = 'isl'; + case Ido = 'ido'; + case Igbo = 'ibo'; + case Ijo_languages = 'ijo'; + case Iloko = 'ilo'; + case Inari_Sami = 'smn'; + case Indic_languages = 'inc'; + case Indo_European_languages = 'ine'; + case Indonesian = 'ind'; + case Ingush = 'inh'; + case Interlingua_International_Auxiliary_Language_Association = 'ina'; + case Interlingue_Occidental = 'ile'; + case Inuktitut = 'iku'; + case Inupiaq = 'ipk'; + case Iranian_languages = 'ira'; + case Irish = 'gle'; + case Irish_Middle_900_1200 = 'mga'; + case Irish_Old_to_900 = 'sga'; + case Iroquoian_languages = 'iro'; + case Italian = 'ita'; + case Japanese = 'jpn'; + case Javanese = 'jav'; + case Judeo_Arabic = 'jrb'; + case Judeo_Persian = 'jpr'; + case Kabardian = 'kbd'; + case Kabyle = 'kab'; + case Kachin_Jingpho = 'kac'; + case Kalaallisut_Greenlandic = 'kal'; + case Kalmyk_Oirat = 'xal'; + case Kamba = 'kam'; + case Kannada = 'kan'; + case Kanuri = 'kau'; + case Kara_Kalpak = 'kaa'; + case Karachay_Balkar = 'krc'; + case Karelian = 'krl'; + case Karen_languages = 'kar'; + case Kashmiri = 'kas'; + case Kashubian = 'csb'; + case Kawi = 'kaw'; + case Kazakh = 'kaz'; + case Khasi = 'kha'; + case Khoisan_languages = 'khi'; + case Khotanese_Sakan = 'kho'; + case Kikuyu_Gikuyu = 'kik'; + case Kimbundu = 'kmb'; + case Kinyarwanda = 'kin'; + case Kirghiz_Kyrgyz = 'kir'; + case Klingon_tlhIngan_Hol = 'tlh'; + case Komi = 'kom'; + case Kongo = 'kon'; + case Konkani = 'kok'; + case Korean = 'kor'; + case Kosraean = 'kos'; + case Kpelle = 'kpe'; + case Kru_languages = 'kro'; + case Kuanyama_Kwanyama = 'kua'; + case Kumyk = 'kum'; + case Kurdish = 'kur'; + case Kurukh = 'kru'; + case Kutenai = 'kut'; + case Ladino = 'lad'; + case Lahnda = 'lah'; + case Lamba = 'lam'; + case Land_Dayak_languages = 'day'; + case Lao = 'lao'; + case Latin = 'lat'; + case Latvian = 'lav'; + case Lezghian = 'lez'; + case Limburgan_Limburger_Limburgish = 'lim'; + case Lingala = 'lin'; + case Lithuanian = 'lit'; + case Lojban = 'jbo'; + case Low_German_Low_Saxon_German_Low_Saxon_Low = 'nds'; + case Lower_Sorbian = 'dsb'; + case Lozi = 'loz'; + case Luba_Katanga = 'lub'; + case Luba_Lulua = 'lua'; + case Luiseno = 'lui'; + case Lule_Sami = 'smj'; + case Lunda = 'lun'; + case Luo_Kenya_and_Tanzania = 'luo'; + case Lushai = 'lus'; + case Luxembourgish_Letzeburgesch = 'ltz'; + case Macedonian = 'mkd'; + case Madurese = 'mad'; + case Magahi = 'mag'; + case Maithili = 'mai'; + case Makasar = 'mak'; + case Malagasy = 'mlg'; + case Malay = 'msa'; + case Malayalam = 'mal'; + case Maltese = 'mlt'; + case Manchu = 'mnc'; + case Mandar = 'mdr'; + case Mandingo = 'man'; + case Manipuri = 'mni'; + case Manobo_languages = 'mno'; + case Manx = 'glv'; + case Maori = 'mri'; + case Mapudungun_Mapuche = 'arn'; + case Marathi = 'mar'; + case Mari = 'chm'; + case Marshallese = 'mah'; + case Marwari = 'mwr'; + case Masai = 'mas'; + case Mayan_languages = 'myn'; + case Mende = 'men'; + case Mi_kmaq_Micmac = 'mic'; + case Minangkabau = 'min'; + case Mirandese = 'mwl'; + case Mohawk = 'moh'; + case Moksha = 'mdf'; + case Mon_Khmer_languages = 'mkh'; + case Mongo = 'lol'; + case Mongolian = 'mon'; + case Montenegrin = 'cnr'; + case Mossi = 'mos'; + case Multiple_languages = 'mul'; + case Munda_languages = 'mun'; + case N_Ko = 'nqo'; + case Nahuatl_languages = 'nah'; + case Nauru = 'nau'; + case Navajo_Navaho = 'nav'; + case Ndebele_North_North_Ndebele = 'nde'; + case Ndebele_South_South_Ndebele = 'nbl'; + case Ndonga = 'ndo'; + case Neapolitan = 'nap'; + case Nepal_Bhasa_Newari = 'new'; + case Nepali = 'nep'; + case Nias = 'nia'; + case Niger_Kordofanian_languages = 'nic'; + case Nilo_Saharan_languages = 'ssa'; + case Niuean = 'niu'; + case No_linguistic_content_Not_applicable = 'zxx'; + case Nogai = 'nog'; + case Norse_Old = 'non'; + case North_American_Indian_languages = 'nai'; + case Northern_Frisian = 'frr'; + case Northern_Sami = 'sme'; + case Norwegian = 'nor'; + case Norwegian_Nynorsk_Nynorsk_Norwegian = 'nno'; + case Nubian_languages = 'nub'; + case Nyamwezi = 'nym'; + case Nyankole = 'nyn'; + case Nyoro = 'nyo'; + case Nzima = 'nzi'; + case Occitan_post_1500 = 'oci'; + case Official_Aramaic_700_300_BCE_Imperial_Aramaic_700_300_BCE = 'arc'; + case Ojibwa = 'oji'; + case Oriya = 'ori'; + case Oromo = 'orm'; + case Osage = 'osa'; + case Ossetian_Ossetic = 'oss'; + case Otomian_languages = 'oto'; + case Pahlavi = 'pal'; + case Palauan = 'pau'; + case Pali = 'pli'; + case Pampanga_Kapampangan = 'pam'; + case Pangasinan = 'pag'; + case Panjabi_Punjabi = 'pan'; + case Papiamento = 'pap'; + case Papuan_languages = 'paa'; + case Pedi_Sepedi_Northern_Sotho = 'nso'; + case Persian = 'fas'; + case Persian_Old_ca_600_400_B_C = 'peo'; + case Philippine_languages = 'phi'; + case Phoenician = 'phn'; + case Pohnpeian = 'pon'; + case Polish = 'pol'; + case Portuguese = 'por'; + case Prakrit_languages = 'pra'; + case Provencal_Old_to_1500_Occitan_Old_to_1500 = 'pro'; + case Pushto_Pashto = 'pus'; + case Quechua = 'que'; + case Rajasthani = 'raj'; + case Rapanui = 'rap'; + case Rarotongan_Cook_Islands_Maori = 'rar'; + case Romance_languages = 'roa'; + case Romanian_Moldavian_Moldovan = 'ron'; + case Romansh = 'roh'; + case Romany = 'rom'; + case Rundi = 'run'; + case Russian = 'rus'; + case Salishan_languages = 'sal'; + case Samaritan_Aramaic = 'sam'; + case Sami_languages = 'smi'; + case Samoan = 'smo'; + case Sandawe = 'sad'; + case Sango = 'sag'; + case Sanskrit = 'san'; + case Santali = 'sat'; + case Sardinian = 'srd'; + case Sasak = 'sas'; + case Scots = 'sco'; + case Selkup = 'sel'; + case Semitic_languages = 'sem'; + case Serbian = 'srp'; + case Serer = 'srr'; + case Shan = 'shn'; + case Shona = 'sna'; + case Sichuan_Yi_Nuosu = 'iii'; + case Sicilian = 'scn'; + case Sidamo = 'sid'; + case Sign_Languages = 'sgn'; + case Siksika = 'bla'; + case Sindhi = 'snd'; + case Sinhala_Sinhalese = 'sin'; + case Sino_Tibetan_languages = 'sit'; + case Siouan_languages = 'sio'; + case Skolt_Sami = 'sms'; + case Slave_Athapascan = 'den'; + case Slavic_languages = 'sla'; + case Slovak = 'slk'; + case Slovenian = 'slv'; + case Sogdian = 'sog'; + case Somali = 'som'; + case Songhai_languages = 'son'; + case Soninke = 'snk'; + case Sorbian_languages = 'wen'; + case Sotho_Southern = 'sot'; + case South_American_Indian_languages = 'sai'; + case Southern_Altai = 'alt'; + case Southern_Sami = 'sma'; + case Spanish_Castilian = 'spa'; + case Sranan_Tongo = 'srn'; + case Standard_Moroccan_Tamazight = 'zgh'; + case Sukuma = 'suk'; + case Sumerian = 'sux'; + case Sundanese = 'sun'; + case Susu = 'sus'; + case Swahili = 'swa'; + case Swati = 'ssw'; + case Swedish = 'swe'; + case Swiss_German_Alemannic_Alsatian = 'gsw'; + case Syriac = 'syr'; + case Tagalog = 'tgl'; + case Tahitian = 'tah'; + case Tai_languages = 'tai'; + case Tajik = 'tgk'; + case Tamashek = 'tmh'; + case Tamil = 'tam'; + case Tatar = 'tat'; + case Telugu = 'tel'; + case Tereno = 'ter'; + case Tetum = 'tet'; + case Thai = 'tha'; + case Tibetan = 'bod'; + case Tigre = 'tig'; + case Tigrinya = 'tir'; + case Timne = 'tem'; + case Tiv = 'tiv'; + case Tlingit = 'tli'; + case Tok_Pisin = 'tpi'; + case Tokelau = 'tkl'; + case Tonga_Nyasa = 'tog'; + case Tonga_Tonga_Islands = 'ton'; + case Tsimshian = 'tsi'; + case Tsonga = 'tso'; + case Tswana = 'tsn'; + case Tumbuka = 'tum'; + case Tupi_languages = 'tup'; + case Turkish = 'tur'; + case Turkish_Ottoman_1500_1928 = 'ota'; + case Turkmen = 'tuk'; + case Tuvalu = 'tvl'; + case Tuvinian = 'tyv'; + case Twi = 'twi'; + case Udmurt = 'udm'; + case Ugaritic = 'uga'; + case Uighur_Uyghur = 'uig'; + case Ukrainian = 'ukr'; + case Umbundu = 'umb'; + case Uncoded_languages = 'mis'; + case Undetermined = 'und'; + case Upper_Sorbian = 'hsb'; + case Urdu = 'urd'; + case Uzbek = 'uzb'; + case Vai = 'vai'; + case Venda = 'ven'; + case Vietnamese = 'vie'; + case Volapuk = 'vol'; + case Votic = 'vot'; + case Wakashan_languages = 'wak'; + case Walloon = 'wln'; + case Waray = 'war'; + case Washo = 'was'; + case Welsh = 'cym'; + case Western_Frisian = 'fry'; + case Wolaitta_Wolaytta = 'wal'; + case Wolof = 'wol'; + case Xhosa = 'xho'; + case Yakut = 'sah'; + case Yao = 'yao'; + case Yapese = 'yap'; + case Yiddish = 'yid'; + case Yoruba = 'yor'; + case Yupik_languages = 'ypk'; + case Zande_languages = 'znd'; + case Zapotec = 'zap'; + case Zaza_Dimili_Dimli_Kirdki_Kirmanjki_Zazaki = 'zza'; + case Zenaga = 'zen'; + case Zhuang_Chuang = 'zha'; + case Zulu = 'zul'; + case Zuni = 'zun'; +} + + +enum LanguageAlpha3Bibliographic: string +{ + case Abkhazian = 'abk'; + case Achinese = 'ace'; + case Acoli = 'ach'; + case Adangme = 'ada'; + case Adyghe_Adygei = 'ady'; + case Afar = 'aar'; + case Afrihili = 'afh'; + case Afrikaans = 'afr'; + case Afro_Asiatic_languages = 'afa'; + case Ainu = 'ain'; + case Akan = 'aka'; + case Akkadian = 'akk'; + case Albanian = 'alb'; + case Aleut = 'ale'; + case Algonquian_languages = 'alg'; + case Altaic_languages = 'tut'; + case Amharic = 'amh'; + case Angika = 'anp'; + case Apache_languages = 'apa'; + case Arabic = 'ara'; + case Aragonese = 'arg'; + case Arapaho = 'arp'; + case Arawak = 'arw'; + case Armenian = 'arm'; + case Aromanian_Arumanian_Macedo_Romanian = 'rup'; + case Artificial_languages = 'art'; + case Assamese = 'asm'; + case Asturian_Bable_Leonese_Asturleonese = 'ast'; + case Athapascan_languages = 'ath'; + case Australian_languages = 'aus'; + case Austronesian_languages = 'map'; + case Avaric = 'ava'; + case Avestan = 'ave'; + case Awadhi = 'awa'; + case Aymara = 'aym'; + case Azerbaijani = 'aze'; + case Balinese = 'ban'; + case Baltic_languages = 'bat'; + case Baluchi = 'bal'; + case Bambara = 'bam'; + case Bamileke_languages = 'bai'; + case Banda_languages = 'bad'; + case Bantu_languages = 'bnt'; + case Basa = 'bas'; + case Bashkir = 'bak'; + case Basque = 'baq'; + case Batak_languages = 'btk'; + case Beja_Bedawiyet = 'bej'; + case Belarusian = 'bel'; + case Bemba = 'bem'; + case Bengali = 'ben'; + case Berber_languages = 'ber'; + case Bhojpuri = 'bho'; + case Bihari_languages = 'bih'; + case Bikol = 'bik'; + case Bini_Edo = 'bin'; + case Bislama = 'bis'; + case Blin_Bilin = 'byn'; + case Blissymbols_Blissymbolics_Bliss = 'zbl'; + case Bokmal_Norwegian_Norwegian_Bokmal = 'nob'; + case Bosnian = 'bos'; + case Braj = 'bra'; + case Breton = 'bre'; + case Buginese = 'bug'; + case Bulgarian = 'bul'; + case Buriat = 'bua'; + case Burmese = 'bur'; + case Caddo = 'cad'; + case Catalan_Valencian = 'cat'; + case Caucasian_languages = 'cau'; + case Cebuano = 'ceb'; + case Celtic_languages = 'cel'; + case Central_American_Indian_languages = 'cai'; + case Central_Khmer = 'khm'; + case Chagatai = 'chg'; + case Chamic_languages = 'cmc'; + case Chamorro = 'cha'; + case Chechen = 'che'; + case Cherokee = 'chr'; + case Cheyenne = 'chy'; + case Chibcha = 'chb'; + case Chichewa_Chewa_Nyanja = 'nya'; + case Chinese = 'chi'; + case Chinook_jargon = 'chn'; + case Chipewyan_Dene_Suline = 'chp'; + case Choctaw = 'cho'; + case Church_Slavic_Old_Slavonic_Church_Slavonic_Old_Bulgarian_Old_Church_Slavonic = 'chu'; + case Chuukese = 'chk'; + case Chuvash = 'chv'; + case Classical_Newari_Old_Newari_Classical_Nepal_Bhasa = 'nwc'; + case Classical_Syriac = 'syc'; + case Coptic = 'cop'; + case Cornish = 'cor'; + case Corsican = 'cos'; + case Cree = 'cre'; + case Creek = 'mus'; + case Creoles_and_pidgins = 'crp'; + case Creoles_and_pidgins_English_based = 'cpe'; + case Creoles_and_pidgins_French_based = 'cpf'; + case Creoles_and_pidgins_Portuguese_based = 'cpp'; + case Crimean_Tatar_Crimean_Turkish = 'crh'; + case Croatian = 'hrv'; + case Cushitic_languages = 'cus'; + case Czech = 'cze'; + case Dakota = 'dak'; + case Danish = 'dan'; + case Dargwa = 'dar'; + case Delaware = 'del'; + case Dinka = 'din'; + case Divehi_Dhivehi_Maldivian = 'div'; + case Dogri = 'doi'; + case Dogrib = 'dgr'; + case Dravidian_languages = 'dra'; + case Duala = 'dua'; + case Dutch_Flemish = 'dut'; + case Dutch_Middle_ca_1050_1350 = 'dum'; + case Dyula = 'dyu'; + case Dzongkha = 'dzo'; + case Eastern_Frisian = 'frs'; + case Efik = 'efi'; + case Egyptian_Ancient = 'egy'; + case Ekajuk = 'eka'; + case Elamite = 'elx'; + case English = 'eng'; + case English_Middle_1100_1500 = 'enm'; + case English_Old_ca_450_1100 = 'ang'; + case Erzya = 'myv'; + case Esperanto = 'epo'; + case Estonian = 'est'; + case Ewe = 'ewe'; + case Ewondo = 'ewo'; + case Fang = 'fan'; + case Fanti = 'fat'; + case Faroese = 'fao'; + case Fijian = 'fij'; + case Filipino_Pilipino = 'fil'; + case Finnish = 'fin'; + case Finno_Ugrian_languages = 'fiu'; + case Fon = 'fon'; + case French = 'fre'; + case French_Middle_ca_1400_1600 = 'frm'; + case French_Old_842_ca_1400 = 'fro'; + case Friulian = 'fur'; + case Fulah = 'ful'; + case Ga = 'gaa'; + case Gaelic_Scottish_Gaelic = 'gla'; + case Galibi_Carib = 'car'; + case Galician = 'glg'; + case Ganda = 'lug'; + case Gayo = 'gay'; + case Gbaya = 'gba'; + case Geez = 'gez'; + case Georgian = 'geo'; + case German = 'ger'; + case German_Middle_High_ca_1050_1500 = 'gmh'; + case German_Old_High_ca_750_1050 = 'goh'; + case Germanic_languages = 'gem'; + case Gilbertese = 'gil'; + case Gondi = 'gon'; + case Gorontalo = 'gor'; + case Gothic = 'got'; + case Grebo = 'grb'; + case Greek_Ancient_to_1453 = 'grc'; + case Greek_Modern_1453 = 'gre'; + case Guarani = 'grn'; + case Gujarati = 'guj'; + case Gwich_in = 'gwi'; + case Haida = 'hai'; + case Haitian_Haitian_Creole = 'hat'; + case Hausa = 'hau'; + case Hawaiian = 'haw'; + case Hebrew = 'heb'; + case Herero = 'her'; + case Hiligaynon = 'hil'; + case Himachali_languages_Western_Pahari_languages = 'him'; + case Hindi = 'hin'; + case Hiri_Motu = 'hmo'; + case Hittite = 'hit'; + case Hmong_Mong = 'hmn'; + case Hungarian = 'hun'; + case Hupa = 'hup'; + case Iban = 'iba'; + case Icelandic = 'ice'; + case Ido = 'ido'; + case Igbo = 'ibo'; + case Ijo_languages = 'ijo'; + case Iloko = 'ilo'; + case Inari_Sami = 'smn'; + case Indic_languages = 'inc'; + case Indo_European_languages = 'ine'; + case Indonesian = 'ind'; + case Ingush = 'inh'; + case Interlingua_International_Auxiliary_Language_Association = 'ina'; + case Interlingue_Occidental = 'ile'; + case Inuktitut = 'iku'; + case Inupiaq = 'ipk'; + case Iranian_languages = 'ira'; + case Irish = 'gle'; + case Irish_Middle_900_1200 = 'mga'; + case Irish_Old_to_900 = 'sga'; + case Iroquoian_languages = 'iro'; + case Italian = 'ita'; + case Japanese = 'jpn'; + case Javanese = 'jav'; + case Judeo_Arabic = 'jrb'; + case Judeo_Persian = 'jpr'; + case Kabardian = 'kbd'; + case Kabyle = 'kab'; + case Kachin_Jingpho = 'kac'; + case Kalaallisut_Greenlandic = 'kal'; + case Kalmyk_Oirat = 'xal'; + case Kamba = 'kam'; + case Kannada = 'kan'; + case Kanuri = 'kau'; + case Kara_Kalpak = 'kaa'; + case Karachay_Balkar = 'krc'; + case Karelian = 'krl'; + case Karen_languages = 'kar'; + case Kashmiri = 'kas'; + case Kashubian = 'csb'; + case Kawi = 'kaw'; + case Kazakh = 'kaz'; + case Khasi = 'kha'; + case Khoisan_languages = 'khi'; + case Khotanese_Sakan = 'kho'; + case Kikuyu_Gikuyu = 'kik'; + case Kimbundu = 'kmb'; + case Kinyarwanda = 'kin'; + case Kirghiz_Kyrgyz = 'kir'; + case Klingon_tlhIngan_Hol = 'tlh'; + case Komi = 'kom'; + case Kongo = 'kon'; + case Konkani = 'kok'; + case Korean = 'kor'; + case Kosraean = 'kos'; + case Kpelle = 'kpe'; + case Kru_languages = 'kro'; + case Kuanyama_Kwanyama = 'kua'; + case Kumyk = 'kum'; + case Kurdish = 'kur'; + case Kurukh = 'kru'; + case Kutenai = 'kut'; + case Ladino = 'lad'; + case Lahnda = 'lah'; + case Lamba = 'lam'; + case Land_Dayak_languages = 'day'; + case Lao = 'lao'; + case Latin = 'lat'; + case Latvian = 'lav'; + case Lezghian = 'lez'; + case Limburgan_Limburger_Limburgish = 'lim'; + case Lingala = 'lin'; + case Lithuanian = 'lit'; + case Lojban = 'jbo'; + case Low_German_Low_Saxon_German_Low_Saxon_Low = 'nds'; + case Lower_Sorbian = 'dsb'; + case Lozi = 'loz'; + case Luba_Katanga = 'lub'; + case Luba_Lulua = 'lua'; + case Luiseno = 'lui'; + case Lule_Sami = 'smj'; + case Lunda = 'lun'; + case Luo_Kenya_and_Tanzania = 'luo'; + case Lushai = 'lus'; + case Luxembourgish_Letzeburgesch = 'ltz'; + case Macedonian = 'mac'; + case Madurese = 'mad'; + case Magahi = 'mag'; + case Maithili = 'mai'; + case Makasar = 'mak'; + case Malagasy = 'mlg'; + case Malay = 'may'; + case Malayalam = 'mal'; + case Maltese = 'mlt'; + case Manchu = 'mnc'; + case Mandar = 'mdr'; + case Mandingo = 'man'; + case Manipuri = 'mni'; + case Manobo_languages = 'mno'; + case Manx = 'glv'; + case Maori = 'mao'; + case Mapudungun_Mapuche = 'arn'; + case Marathi = 'mar'; + case Mari = 'chm'; + case Marshallese = 'mah'; + case Marwari = 'mwr'; + case Masai = 'mas'; + case Mayan_languages = 'myn'; + case Mende = 'men'; + case Mi_kmaq_Micmac = 'mic'; + case Minangkabau = 'min'; + case Mirandese = 'mwl'; + case Mohawk = 'moh'; + case Moksha = 'mdf'; + case Mon_Khmer_languages = 'mkh'; + case Mongo = 'lol'; + case Mongolian = 'mon'; + case Montenegrin = 'cnr'; + case Mossi = 'mos'; + case Multiple_languages = 'mul'; + case Munda_languages = 'mun'; + case N_Ko = 'nqo'; + case Nahuatl_languages = 'nah'; + case Nauru = 'nau'; + case Navajo_Navaho = 'nav'; + case Ndebele_North_North_Ndebele = 'nde'; + case Ndebele_South_South_Ndebele = 'nbl'; + case Ndonga = 'ndo'; + case Neapolitan = 'nap'; + case Nepal_Bhasa_Newari = 'new'; + case Nepali = 'nep'; + case Nias = 'nia'; + case Niger_Kordofanian_languages = 'nic'; + case Nilo_Saharan_languages = 'ssa'; + case Niuean = 'niu'; + case No_linguistic_content_Not_applicable = 'zxx'; + case Nogai = 'nog'; + case Norse_Old = 'non'; + case North_American_Indian_languages = 'nai'; + case Northern_Frisian = 'frr'; + case Northern_Sami = 'sme'; + case Norwegian = 'nor'; + case Norwegian_Nynorsk_Nynorsk_Norwegian = 'nno'; + case Nubian_languages = 'nub'; + case Nyamwezi = 'nym'; + case Nyankole = 'nyn'; + case Nyoro = 'nyo'; + case Nzima = 'nzi'; + case Occitan_post_1500 = 'oci'; + case Official_Aramaic_700_300_BCE_Imperial_Aramaic_700_300_BCE = 'arc'; + case Ojibwa = 'oji'; + case Oriya = 'ori'; + case Oromo = 'orm'; + case Osage = 'osa'; + case Ossetian_Ossetic = 'oss'; + case Otomian_languages = 'oto'; + case Pahlavi = 'pal'; + case Palauan = 'pau'; + case Pali = 'pli'; + case Pampanga_Kapampangan = 'pam'; + case Pangasinan = 'pag'; + case Panjabi_Punjabi = 'pan'; + case Papiamento = 'pap'; + case Papuan_languages = 'paa'; + case Pedi_Sepedi_Northern_Sotho = 'nso'; + case Persian = 'per'; + case Persian_Old_ca_600_400_B_C = 'peo'; + case Philippine_languages = 'phi'; + case Phoenician = 'phn'; + case Pohnpeian = 'pon'; + case Polish = 'pol'; + case Portuguese = 'por'; + case Prakrit_languages = 'pra'; + case Provencal_Old_to_1500_Occitan_Old_to_1500 = 'pro'; + case Pushto_Pashto = 'pus'; + case Quechua = 'que'; + case Rajasthani = 'raj'; + case Rapanui = 'rap'; + case Rarotongan_Cook_Islands_Maori = 'rar'; + case Romance_languages = 'roa'; + case Romanian_Moldavian_Moldovan = 'rum'; + case Romansh = 'roh'; + case Romany = 'rom'; + case Rundi = 'run'; + case Russian = 'rus'; + case Salishan_languages = 'sal'; + case Samaritan_Aramaic = 'sam'; + case Sami_languages = 'smi'; + case Samoan = 'smo'; + case Sandawe = 'sad'; + case Sango = 'sag'; + case Sanskrit = 'san'; + case Santali = 'sat'; + case Sardinian = 'srd'; + case Sasak = 'sas'; + case Scots = 'sco'; + case Selkup = 'sel'; + case Semitic_languages = 'sem'; + case Serbian = 'srp'; + case Serer = 'srr'; + case Shan = 'shn'; + case Shona = 'sna'; + case Sichuan_Yi_Nuosu = 'iii'; + case Sicilian = 'scn'; + case Sidamo = 'sid'; + case Sign_Languages = 'sgn'; + case Siksika = 'bla'; + case Sindhi = 'snd'; + case Sinhala_Sinhalese = 'sin'; + case Sino_Tibetan_languages = 'sit'; + case Siouan_languages = 'sio'; + case Skolt_Sami = 'sms'; + case Slave_Athapascan = 'den'; + case Slavic_languages = 'sla'; + case Slovak = 'slo'; + case Slovenian = 'slv'; + case Sogdian = 'sog'; + case Somali = 'som'; + case Songhai_languages = 'son'; + case Soninke = 'snk'; + case Sorbian_languages = 'wen'; + case Sotho_Southern = 'sot'; + case South_American_Indian_languages = 'sai'; + case Southern_Altai = 'alt'; + case Southern_Sami = 'sma'; + case Spanish_Castilian = 'spa'; + case Sranan_Tongo = 'srn'; + case Standard_Moroccan_Tamazight = 'zgh'; + case Sukuma = 'suk'; + case Sumerian = 'sux'; + case Sundanese = 'sun'; + case Susu = 'sus'; + case Swahili = 'swa'; + case Swati = 'ssw'; + case Swedish = 'swe'; + case Swiss_German_Alemannic_Alsatian = 'gsw'; + case Syriac = 'syr'; + case Tagalog = 'tgl'; + case Tahitian = 'tah'; + case Tai_languages = 'tai'; + case Tajik = 'tgk'; + case Tamashek = 'tmh'; + case Tamil = 'tam'; + case Tatar = 'tat'; + case Telugu = 'tel'; + case Tereno = 'ter'; + case Tetum = 'tet'; + case Thai = 'tha'; + case Tibetan = 'tib'; + case Tigre = 'tig'; + case Tigrinya = 'tir'; + case Timne = 'tem'; + case Tiv = 'tiv'; + case Tlingit = 'tli'; + case Tok_Pisin = 'tpi'; + case Tokelau = 'tkl'; + case Tonga_Nyasa = 'tog'; + case Tonga_Tonga_Islands = 'ton'; + case Tsimshian = 'tsi'; + case Tsonga = 'tso'; + case Tswana = 'tsn'; + case Tumbuka = 'tum'; + case Tupi_languages = 'tup'; + case Turkish = 'tur'; + case Turkish_Ottoman_1500_1928 = 'ota'; + case Turkmen = 'tuk'; + case Tuvalu = 'tvl'; + case Tuvinian = 'tyv'; + case Twi = 'twi'; + case Udmurt = 'udm'; + case Ugaritic = 'uga'; + case Uighur_Uyghur = 'uig'; + case Ukrainian = 'ukr'; + case Umbundu = 'umb'; + case Uncoded_languages = 'mis'; + case Undetermined = 'und'; + case Upper_Sorbian = 'hsb'; + case Urdu = 'urd'; + case Uzbek = 'uzb'; + case Vai = 'vai'; + case Venda = 'ven'; + case Vietnamese = 'vie'; + case Volapuk = 'vol'; + case Votic = 'vot'; + case Wakashan_languages = 'wak'; + case Walloon = 'wln'; + case Waray = 'war'; + case Washo = 'was'; + case Welsh = 'wel'; + case Western_Frisian = 'fry'; + case Wolaitta_Wolaytta = 'wal'; + case Wolof = 'wol'; + case Xhosa = 'xho'; + case Yakut = 'sah'; + case Yao = 'yao'; + case Yapese = 'yap'; + case Yiddish = 'yid'; + case Yoruba = 'yor'; + case Yupik_languages = 'ypk'; + case Zande_languages = 'znd'; + case Zapotec = 'zap'; + case Zaza_Dimili_Dimli_Kirdki_Kirmanjki_Zazaki = 'zza'; + case Zenaga = 'zen'; + case Zhuang_Chuang = 'zha'; + case Zulu = 'zul'; + case Zuni = 'zun'; + +} + + +enum LanguageAlpha3Extensive: string +{ + case Ghotuo = 'aaa'; + case Alumu_Tesu = 'aab'; + case Ari = 'aac'; + case Amal = 'aad'; + case Arbereshe_Albanian = 'aae'; + case Aranadan = 'aaf'; + case Ambrak = 'aag'; + case Abu_Arapesh = 'aah'; + case Arifama_Miniafia = 'aai'; + case Ankave = 'aak'; + case Afade = 'aal'; + case Anambe = 'aan'; + case Algerian_Saharan_Arabic = 'aao'; + case Para_Arara = 'aap'; + case Eastern_Abnaki = 'aaq'; + case Afar = 'aar'; + case Aasax = 'aas'; + case Arvanitika_Albanian = 'aat'; + case Abau = 'aau'; + case Solong = 'aaw'; + case Mandobo_Atas = 'aax'; + case Amarasi = 'aaz'; + case Abe = 'aba'; + case Bankon = 'abb'; + case Ambala_Ayta = 'abc'; + case Manide = 'abd'; + case Western_Abnaki = 'abe'; + case Abai_Sungai = 'abf'; + case Abaga = 'abg'; + case Tajiki_Arabic = 'abh'; + case Abidji = 'abi'; + case Aka_Bea = 'abj'; + case Abkhazian = 'abk'; + case Lampung_Nyo = 'abl'; + case Abanyom = 'abm'; + case Abua = 'abn'; + case Abon = 'abo'; + case Abellen_Ayta = 'abp'; + case Abaza = 'abq'; + case Abron = 'abr'; + case Ambonese_Malay = 'abs'; + case Ambulas = 'abt'; + case Abure = 'abu'; + case Baharna_Arabic = 'abv'; + case Pal = 'abw'; + case Inabaknon = 'abx'; + case Aneme_Wake = 'aby'; + case Abui = 'abz'; + case Achagua = 'aca'; + case Anca = 'acb'; + case Gikyode = 'acd'; + case Achinese = 'ace'; + case Saint_Lucian_Creole_French = 'acf'; + case Acoli = 'ach'; + case Aka_Cari = 'aci'; + case Aka_Kora = 'ack'; + case Akar_Bale = 'acl'; + case Mesopotamian_Arabic = 'acm'; + case Achang = 'acn'; + case Eastern_Acipa = 'acp'; + case Ta_izzi_Adeni_Arabic = 'acq'; + case Achi = 'acr'; + case Acroa = 'acs'; + case Achterhoeks = 'act'; + case Achuar_Shiwiar = 'acu'; + case Achumawi = 'acv'; + case Hijazi_Arabic = 'acw'; + case Omani_Arabic = 'acx'; + case Cypriot_Arabic = 'acy'; + case Acheron = 'acz'; + case Adangme = 'ada'; + case Atauran = 'adb'; + case Lidzonka = 'add'; + case Adele = 'ade'; + case Dhofari_Arabic = 'adf'; + case Andegerebinha = 'adg'; + case Adhola = 'adh'; + case Adi = 'adi'; + case Adioukrou = 'adj'; + case Galo = 'adl'; + case Adang = 'adn'; + case Abu = 'ado'; + case Adangbe = 'adq'; + case Adonara = 'adr'; + case Adamorobe_Sign_Language = 'ads'; + case Adnyamathanha = 'adt'; + case Aduge = 'adu'; + case Amundava = 'adw'; + case Amdo_Tibetan = 'adx'; + case Adyghe = 'ady'; + case Adzera = 'adz'; + case Areba = 'aea'; + case Tunisian_Arabic = 'aeb'; + case Saidi_Arabic = 'aec'; + case Argentine_Sign_Language = 'aed'; + case Northeast_Pashai = 'aee'; + case Haeke = 'aek'; + case Ambele = 'ael'; + case Arem = 'aem'; + case Armenian_Sign_Language = 'aen'; + case Aer = 'aeq'; + case Eastern_Arrernte = 'aer'; + case Alsea = 'aes'; + case Akeu = 'aeu'; + case Ambakich = 'aew'; + case Amele = 'aey'; + case Aeka = 'aez'; + case Gulf_Arabic = 'afb'; + case Andai = 'afd'; + case Putukwam = 'afe'; + case Afghan_Sign_Language = 'afg'; + case Afrihili = 'afh'; + case Akrukay = 'afi'; + case Nanubae = 'afk'; + case Defaka = 'afn'; + case Eloyi = 'afo'; + case Tapei = 'afp'; + case Afrikaans = 'afr'; + case Afro_Seminole_Creole = 'afs'; + case Afitti = 'aft'; + case Awutu = 'afu'; + case Obokuitai = 'afz'; + case Aguano = 'aga'; + case Legbo = 'agb'; + case Agatu = 'agc'; + case Agarabi = 'agd'; + case Angal = 'age'; + case Arguni = 'agf'; + case Angor = 'agg'; + case Ngelima = 'agh'; + case Agariya = 'agi'; + case Argobba = 'agj'; + case Isarog_Agta = 'agk'; + case Fembe = 'agl'; + case Angaataha = 'agm'; + case Agutaynen = 'agn'; + case Tainae = 'ago'; + case Aghem = 'agq'; + case Aguaruna = 'agr'; + case Esimbi = 'ags'; + case Central_Cagayan_Agta = 'agt'; + case Aguacateco = 'agu'; + case Remontado_Dumagat = 'agv'; + case Kahua = 'agw'; + case Aghul = 'agx'; + case Southern_Alta = 'agy'; + case Mt_Iriga_Agta = 'agz'; + case Ahanta = 'aha'; + case Axamb = 'ahb'; + case Qimant = 'ahg'; + case Aghu = 'ahh'; + case Tiagbamrin_Aizi = 'ahi'; + case Akha = 'ahk'; + case Igo = 'ahl'; + case Mobumrin_Aizi = 'ahm'; + case Ahan = 'ahn'; + case Ahom = 'aho'; + case Aproumu_Aizi = 'ahp'; + case Ahirani = 'ahr'; + case Ashe = 'ahs'; + case Ahtena = 'aht'; + case Arosi = 'aia'; + case Ainu_China = 'aib'; + case Ainbai = 'aic'; + case Alngith = 'aid'; + case Amara = 'aie'; + case Agi = 'aif'; + case Antigua_and_Barbuda_Creole_English = 'aig'; + case Ai_Cham = 'aih'; + case Assyrian_Neo_Aramaic = 'aii'; + case Lishanid_Noshan = 'aij'; + case Ake = 'aik'; + case Aimele = 'ail'; + case Aimol = 'aim'; + case Ainu_Japan = 'ain'; + case Aiton = 'aio'; + case Burumakok = 'aip'; + case Aimaq = 'aiq'; + case Airoran = 'air'; + case Arikem = 'ait'; + case Aari = 'aiw'; + case Aighon = 'aix'; + case Ali = 'aiy'; + case Aja_South_Sudan = 'aja'; + case Aja_Benin = 'ajg'; + case Ajie = 'aji'; + case Andajin = 'ajn'; + case Algerian_Jewish_Sign_Language = 'ajs'; + case Judeo_Moroccan_Arabic = 'aju'; + case Ajawa = 'ajw'; + case Amri_Karbi = 'ajz'; + case Akan = 'aka'; + case Batak_Angkola = 'akb'; + case Mpur = 'akc'; + case Ukpet_Ehom = 'akd'; + case Akawaio = 'ake'; + case Akpa = 'akf'; + case Anakalangu = 'akg'; + case Angal_Heneng = 'akh'; + case Aiome = 'aki'; + case Aka_Jeru = 'akj'; + case Akkadian = 'akk'; + case Aklanon = 'akl'; + case Aka_Bo = 'akm'; + case Akurio = 'ako'; + case Siwu = 'akp'; + case Ak = 'akq'; + case Araki = 'akr'; + case Akaselem = 'aks'; + case Akolet = 'akt'; + case Akum = 'aku'; + case Akhvakh = 'akv'; + case Akwa = 'akw'; + case Aka_Kede = 'akx'; + case Aka_Kol = 'aky'; + case Alabama = 'akz'; + case Alago = 'ala'; + case Qawasqar = 'alc'; + case Alladian = 'ald'; + case Aleut = 'ale'; + case Alege = 'alf'; + case Alawa = 'alh'; + case Amaimon = 'ali'; + case Alangan = 'alj'; + case Alak = 'alk'; + case Allar = 'all'; + case Amblong = 'alm'; + case Gheg_Albanian = 'aln'; + case Larike_Wakasihu = 'alo'; + case Alune = 'alp'; + case Algonquin = 'alq'; + case Alutor = 'alr'; + case Tosk_Albanian = 'als'; + case Southern_Altai = 'alt'; + case Are_are = 'alu'; + case Alaba_K_abeena = 'alw'; + case Amol = 'alx'; + case Alyawarr = 'aly'; + case Alur = 'alz'; + case Amanaye = 'ama'; + case Ambo = 'amb'; + case Amahuaca = 'amc'; + case Yanesha = 'ame'; + case Hamer_Banna = 'amf'; + case Amurdak = 'amg'; + case Amharic = 'amh'; + case Amis = 'ami'; + case Amdang = 'amj'; + case Ambai = 'amk'; + case War_Jaintia = 'aml'; + case Ama_Papua_New_Guinea = 'amm'; + case Amanab = 'amn'; + case Amo = 'amo'; + case Alamblak = 'amp'; + case Amahai = 'amq'; + case Amarakaeri = 'amr'; + case Southern_Amami_Oshima = 'ams'; + case Amto = 'amt'; + case Guerrero_Amuzgo = 'amu'; + case Ambelau = 'amv'; + case Western_Neo_Aramaic = 'amw'; + case Anmatyerre = 'amx'; + case Ami = 'amy'; + case Atampaya = 'amz'; + case Andaqui = 'ana'; + case Andoa = 'anb'; + case Ngas = 'anc'; + case Ansus = 'and'; + case Xaracuu = 'ane'; + case Animere = 'anf'; + case Old_English_ca_450_1100 = 'ang'; + case Nend = 'anh'; + case Andi = 'ani'; + case Anor = 'anj'; + case Goemai = 'ank'; + case Anu_Hkongso_Chin = 'anl'; + case Anal = 'anm'; + case Obolo = 'ann'; + case Andoque = 'ano'; + case Angika = 'anp'; + case Jarawa_India = 'anq'; + case Andh = 'anr'; + case Anserma = 'ans'; + case Antakarinya = 'ant'; + case Anuak = 'anu'; + case Denya = 'anv'; + case Anaang = 'anw'; + case Andra_Hus = 'anx'; + case Anyin = 'any'; + case Anem = 'anz'; + case Angolar = 'aoa'; + case Abom = 'aob'; + case Pemon = 'aoc'; + case Andarum = 'aod'; + case Angal_Enen = 'aoe'; + case Bragat = 'aof'; + case Angoram = 'aog'; + case Anindilyakwa = 'aoi'; + case Mufian = 'aoj'; + case Arho = 'aok'; + case Alor = 'aol'; + case Omie = 'aom'; + case Bumbita_Arapesh = 'aon'; + case Aore = 'aor'; + case Taikat = 'aos'; + case Atong_India = 'aot'; + case A_ou = 'aou'; + case Atorada = 'aox'; + case Uab_Meto = 'aoz'; + case Sa_a = 'apb'; + case Levantine_Arabic = 'apc'; + case Sudanese_Arabic = 'apd'; + case Bukiyip = 'ape'; + case Pahanan_Agta = 'apf'; + case Ampanang = 'apg'; + case Athpariya = 'aph'; + case Apiaka = 'api'; + case Jicarilla_Apache = 'apj'; + case Kiowa_Apache = 'apk'; + case Lipan_Apache = 'apl'; + case Mescalero_Chiricahua_Apache = 'apm'; + case Apinaye = 'apn'; + case Ambul = 'apo'; + case Apma = 'app'; + case A_Pucikwar = 'apq'; + case Arop_Lokep = 'apr'; + case Arop_Sissano = 'aps'; + case Apatani = 'apt'; + case Apurina = 'apu'; + case Alapmunte = 'apv'; + case Western_Apache = 'apw'; + case Aputai = 'apx'; + case Apalai = 'apy'; + case Safeyoka = 'apz'; + case Archi = 'aqc'; + case Ampari_Dogon = 'aqd'; + case Arigidi = 'aqg'; + case Aninka = 'aqk'; + case Atohwaim = 'aqm'; + case Northern_Alta = 'aqn'; + case Atakapa = 'aqp'; + case Arha = 'aqr'; + case Angaite = 'aqt'; + case Akuntsu = 'aqz'; + case Arabic = 'ara'; + case Standard_Arabic = 'arb'; + case Official_Aramaic_700_300_BCE = 'arc'; + case Arabana = 'ard'; + case Western_Arrarnta = 'are'; + case Aragonese = 'arg'; + case Arhuaco = 'arh'; + case Arikara = 'ari'; + case Arapaso = 'arj'; + case Arikapu = 'ark'; + case Arabela = 'arl'; + case Mapudungun = 'arn'; + case Araona = 'aro'; + case Arapaho = 'arp'; + case Algerian_Arabic = 'arq'; + case Karo_Brazil = 'arr'; + case Najdi_Arabic = 'ars'; + case Arua_Amazonas_State = 'aru'; + case Arbore = 'arv'; + case Arawak = 'arw'; + case Arua_Rodonia_State = 'arx'; + case Moroccan_Arabic = 'ary'; + case Egyptian_Arabic = 'arz'; + case Asu_Tanzania = 'asa'; + case Assiniboine = 'asb'; + case Casuarina_Coast_Asmat = 'asc'; + case American_Sign_Language = 'ase'; + case Auslan = 'asf'; + case Cishingini = 'asg'; + case Abishira = 'ash'; + case Buruwai = 'asi'; + case Sari = 'asj'; + case Ashkun = 'ask'; + case Asilulu = 'asl'; + case Assamese = 'asm'; + case Xingu_Asurini = 'asn'; + case Dano = 'aso'; + case Algerian_Sign_Language = 'asp'; + case Austrian_Sign_Language = 'asq'; + case Asuri = 'asr'; + case Ipulo = 'ass'; + case Asturian = 'ast'; + case Tocantins_Asurini = 'asu'; + case Asoa = 'asv'; + case Australian_Aborigines_Sign_Language = 'asw'; + case Muratayak = 'asx'; + case Yaosakor_Asmat = 'asy'; + case As = 'asz'; + case Pele_Ata = 'ata'; + case Zaiwa = 'atb'; + case Atsahuaca = 'atc'; + case Ata_Manobo = 'atd'; + case Atemble = 'ate'; + case Ivbie_North_Okpela_Arhe = 'atg'; + case Attie = 'ati'; + case Atikamekw = 'atj'; + case Ati = 'atk'; + case Mt_Iraya_Agta = 'atl'; + case Ata = 'atm'; + case Ashtiani = 'atn'; + case Atong_Cameroon = 'ato'; + case Pudtol_Atta = 'atp'; + case Aralle_Tabulahan = 'atq'; + case Waimiri_Atroari = 'atr'; + case Gros_Ventre = 'ats'; + case Pamplona_Atta = 'att'; + case Reel = 'atu'; + case Northern_Altai = 'atv'; + case Atsugewi = 'atw'; + case Arutani = 'atx'; + case Aneityum = 'aty'; + case Arta = 'atz'; + case Asumboa = 'aua'; + case Alugu = 'aub'; + case Waorani = 'auc'; + case Anuta = 'aud'; + case Aguna = 'aug'; + case Aushi = 'auh'; + case Anuki = 'aui'; + case Awjilah = 'auj'; + case Heyo = 'auk'; + case Aulua = 'aul'; + case Asu_Nigeria = 'aum'; + case Molmo_One = 'aun'; + case Auyokawa = 'auo'; + case Makayam = 'aup'; + case Anus = 'auq'; + case Aruek = 'aur'; + case Austral = 'aut'; + case Auye = 'auu'; + case Awyi = 'auw'; + case Aura = 'aux'; + case Awiyaana = 'auy'; + case Uzbeki_Arabic = 'auz'; + case Avaric = 'ava'; + case Avau = 'avb'; + case Alviri_Vidari = 'avd'; + case Avestan = 'ave'; + case Avikam = 'avi'; + case Kotava = 'avk'; + case Eastern_Egyptian_Bedawi_Arabic = 'avl'; + case Angkamuthi = 'avm'; + case Avatime = 'avn'; + case Agavotaguerra = 'avo'; + case Aushiri = 'avs'; + case Au = 'avt'; + case Avokaya = 'avu'; + case Ava_Canoeiro = 'avv'; + case Awadhi = 'awa'; + case Awa_Papua_New_Guinea = 'awb'; + case Cicipu = 'awc'; + case Aweti = 'awe'; + case Anguthimri = 'awg'; + case Awbono = 'awh'; + case Aekyom = 'awi'; + case Awabakal = 'awk'; + case Arawum = 'awm'; + case Awngi = 'awn'; + case Awak = 'awo'; + case Awera = 'awr'; + case South_Awyu = 'aws'; + case Arawete = 'awt'; + case Central_Awyu = 'awu'; + case Jair_Awyu = 'awv'; + case Awun = 'aww'; + case Awara = 'awx'; + case Edera_Awyu = 'awy'; + case Abipon = 'axb'; + case Ayerrerenge = 'axe'; + case Mato_Grosso_Arara = 'axg'; + case Yaka_Central_African_Republic = 'axk'; + case Lower_Southern_Aranda = 'axl'; + case Middle_Armenian = 'axm'; + case Xaragure = 'axx'; + case Awar = 'aya'; + case Ayizo_Gbe = 'ayb'; + case Southern_Aymara = 'ayc'; + case Ayabadhu = 'ayd'; + case Ayere = 'aye'; + case Ginyanga = 'ayg'; + case Hadrami_Arabic = 'ayh'; + case Leyigha = 'ayi'; + case Akuku = 'ayk'; + case Libyan_Arabic = 'ayl'; + case Aymara = 'aym'; + case Sanaani_Arabic = 'ayn'; + case Ayoreo = 'ayo'; + case North_Mesopotamian_Arabic = 'ayp'; + case Ayi_Papua_New_Guinea = 'ayq'; + case Central_Aymara = 'ayr'; + case Sorsogon_Ayta = 'ays'; + case Magbukun_Ayta = 'ayt'; + case Ayu = 'ayu'; + case Mai_Brat = 'ayz'; + case Azha = 'aza'; + case South_Azerbaijani = 'azb'; + case Eastern_Durango_Nahuatl = 'azd'; + case Azerbaijani = 'aze'; + case San_Pedro_Amuzgos_Amuzgo = 'azg'; + case North_Azerbaijani = 'azj'; + case Ipalapa_Amuzgo = 'azm'; + case Western_Durango_Nahuatl = 'azn'; + case Awing = 'azo'; + case Faire_Atta = 'azt'; + case Highland_Puebla_Nahuatl = 'azz'; + case Babatana = 'baa'; + case Bainouk_Gunyuno = 'bab'; + case Badui = 'bac'; + case Bare = 'bae'; + case Nubaca = 'baf'; + case Tuki = 'bag'; + case Bahamas_Creole_English = 'bah'; + case Barakai = 'baj'; + case Bashkir = 'bak'; + case Baluchi = 'bal'; + case Bambara = 'bam'; + case Balinese = 'ban'; + case Waimaha = 'bao'; + case Bantawa = 'bap'; + case Bavarian = 'bar'; + case Basa_Cameroon = 'bas'; + case Bada_Nigeria = 'bau'; + case Vengo = 'bav'; + case Bambili_Bambui = 'baw'; + case Bamun = 'bax'; + case Batuley = 'bay'; + case Baatonum = 'bba'; + case Barai = 'bbb'; + case Batak_Toba = 'bbc'; + case Bau = 'bbd'; + case Bangba = 'bbe'; + case Baibai = 'bbf'; + case Barama = 'bbg'; + case Bugan = 'bbh'; + case Barombi = 'bbi'; + case Ghomala = 'bbj'; + case Babanki = 'bbk'; + case Bats = 'bbl'; + case Babango = 'bbm'; + case Uneapa = 'bbn'; + case Northern_Bobo_Madare = 'bbo'; + case West_Central_Banda = 'bbp'; + case Bamali = 'bbq'; + case Girawa = 'bbr'; + case Bakpinka = 'bbs'; + case Mburku = 'bbt'; + case Kulung_Nigeria = 'bbu'; + case Karnai = 'bbv'; + case Baba = 'bbw'; + case Bubia = 'bbx'; + case Befang = 'bby'; + case Central_Bai = 'bca'; + case Bainouk_Samik = 'bcb'; + case Southern_Balochi = 'bcc'; + case North_Babar = 'bcd'; + case Bamenyam = 'bce'; + case Bamu = 'bcf'; + case Baga_Pokur = 'bcg'; + case Bariai = 'bch'; + case Baoule = 'bci'; + case Bardi = 'bcj'; + case Bunuba = 'bck'; + case Central_Bikol = 'bcl'; + case Bannoni = 'bcm'; + case Bali_Nigeria = 'bcn'; + case Kaluli = 'bco'; + case Bali_Democratic_Republic_of_Congo = 'bcp'; + case Bench = 'bcq'; + case Babine = 'bcr'; + case Kohumono = 'bcs'; + case Bendi = 'bct'; + case Awad_Bing = 'bcu'; + case Shoo_Minda_Nye = 'bcv'; + case Bana = 'bcw'; + case Bacama = 'bcy'; + case Bainouk_Gunyaamolo = 'bcz'; + case Bayot = 'bda'; + case Basap = 'bdb'; + case Embera_Baudo = 'bdc'; + case Bunama = 'bdd'; + case Bade = 'bde'; + case Biage = 'bdf'; + case Bonggi = 'bdg'; + case Baka_South_Sudan = 'bdh'; + case Burun = 'bdi'; + case Bai_South_Sudan = 'bdj'; + case Budukh = 'bdk'; + case Indonesian_Bajau = 'bdl'; + case Buduma = 'bdm'; + case Baldemu = 'bdn'; + case Morom = 'bdo'; + case Bende = 'bdp'; + case Bahnar = 'bdq'; + case West_Coast_Bajau = 'bdr'; + case Burunge = 'bds'; + case Bokoto = 'bdt'; + case Oroko = 'bdu'; + case Bodo_Parja = 'bdv'; + case Baham = 'bdw'; + case Budong_Budong = 'bdx'; + case Bandjalang = 'bdy'; + case Badeshi = 'bdz'; + case Beaver = 'bea'; + case Bebele = 'beb'; + case Iceve_Maci = 'bec'; + case Bedoanas = 'bed'; + case Byangsi = 'bee'; + case Benabena = 'bef'; + case Belait = 'beg'; + case Biali = 'beh'; + case Bekati = 'bei'; + case Beja = 'bej'; + case Bebeli = 'bek'; + case Belarusian = 'bel'; + case Bemba_Zambia = 'bem'; + case Bengali = 'ben'; + case Beami = 'beo'; + case Besoa = 'bep'; + case Beembe = 'beq'; + case Besme = 'bes'; + case Guiberoua_Bete = 'bet'; + case Blagar = 'beu'; + case Daloa_Bete = 'bev'; + case Betawi = 'bew'; + case Jur_Modo = 'bex'; + case Beli_Papua_New_Guinea = 'bey'; + case Bena_Tanzania = 'bez'; + case Bari = 'bfa'; + case Pauri_Bareli = 'bfb'; + case Panyi_Bai = 'bfc'; + case Bafut = 'bfd'; + case Betaf = 'bfe'; + case Bofi = 'bff'; + case Busang_Kayan = 'bfg'; + case Blafe = 'bfh'; + case British_Sign_Language = 'bfi'; + case Bafanji = 'bfj'; + case Ban_Khor_Sign_Language = 'bfk'; + case Banda_Ndele = 'bfl'; + case Mmen = 'bfm'; + case Bunak = 'bfn'; + case Malba_Birifor = 'bfo'; + case Beba = 'bfp'; + case Badaga = 'bfq'; + case Bazigar = 'bfr'; + case Southern_Bai = 'bfs'; + case Balti = 'bft'; + case Gahri = 'bfu'; + case Bondo = 'bfw'; + case Bantayanon = 'bfx'; + case Bagheli = 'bfy'; + case Mahasu_Pahari = 'bfz'; + case Gwamhi_Wuri = 'bga'; + case Bobongko = 'bgb'; + case Haryanvi = 'bgc'; + case Rathwi_Bareli = 'bgd'; + case Bauria = 'bge'; + case Bangandu = 'bgf'; + case Bugun = 'bgg'; + case Giangan = 'bgi'; + case Bangolan = 'bgj'; + case Bit = 'bgk'; + case Bo_Laos = 'bgl'; + case Western_Balochi = 'bgn'; + case Baga_Koga = 'bgo'; + case Eastern_Balochi = 'bgp'; + case Bagri = 'bgq'; + case Bawm_Chin = 'bgr'; + case Tagabawa = 'bgs'; + case Bughotu = 'bgt'; + case Mbongno = 'bgu'; + case Warkay_Bipim = 'bgv'; + case Bhatri = 'bgw'; + case Balkan_Gagauz_Turkish = 'bgx'; + case Benggoi = 'bgy'; + case Banggai = 'bgz'; + case Bharia = 'bha'; + case Bhili = 'bhb'; + case Biga = 'bhc'; + case Bhadrawahi = 'bhd'; + case Bhaya = 'bhe'; + case Odiai = 'bhf'; + case Binandere = 'bhg'; + case Bukharic = 'bhh'; + case Bhilali = 'bhi'; + case Bahing = 'bhj'; + case Bimin = 'bhl'; + case Bathari = 'bhm'; + case Bohtan_Neo_Aramaic = 'bhn'; + case Bhojpuri = 'bho'; + case Bima = 'bhp'; + case Tukang_Besi_South = 'bhq'; + case Bara_Malagasy = 'bhr'; + case Buwal = 'bhs'; + case Bhattiyali = 'bht'; + case Bhunjia = 'bhu'; + case Bahau = 'bhv'; + case Biak = 'bhw'; + case Bhalay = 'bhx'; + case Bhele = 'bhy'; + case Bada_Indonesia = 'bhz'; + case Badimaya = 'bia'; + case Bissa = 'bib'; + case Bidiyo = 'bid'; + case Bepour = 'bie'; + case Biafada = 'bif'; + case Biangai = 'big'; + case Bikol = 'bik'; + case Bile = 'bil'; + case Bimoba = 'bim'; + case Bini = 'bin'; + case Nai = 'bio'; + case Bila = 'bip'; + case Bipi = 'biq'; + case Bisorio = 'bir'; + case Bislama = 'bis'; + case Berinomo = 'bit'; + case Biete = 'biu'; + case Southern_Birifor = 'biv'; + case Kol_Cameroon = 'biw'; + case Bijori = 'bix'; + case Birhor = 'biy'; + case Baloi = 'biz'; + case Budza = 'bja'; + case Banggarla = 'bjb'; + case Bariji = 'bjc'; + case Biao_Jiao_Mien = 'bje'; + case Barzani_Jewish_Neo_Aramaic = 'bjf'; + case Bidyogo = 'bjg'; + case Bahinemo = 'bjh'; + case Burji = 'bji'; + case Kanauji = 'bjj'; + case Barok = 'bjk'; + case Bulu_Papua_New_Guinea = 'bjl'; + case Bajelani = 'bjm'; + case Banjar = 'bjn'; + case Mid_Southern_Banda = 'bjo'; + case Fanamaket = 'bjp'; + case Binumarien = 'bjr'; + case Bajan = 'bjs'; + case Balanta_Ganja = 'bjt'; + case Busuu = 'bju'; + case Bedjond = 'bjv'; + case Bakwe = 'bjw'; + case Banao_Itneg = 'bjx'; + case Bayali = 'bjy'; + case Baruga = 'bjz'; + case Kyak = 'bka'; + case Baka_Cameroon = 'bkc'; + case Binukid = 'bkd'; + case Beeke = 'bkf'; + case Buraka = 'bkg'; + case Bakoko = 'bkh'; + case Baki = 'bki'; + case Pande = 'bkj'; + case Brokskat = 'bkk'; + case Berik = 'bkl'; + case Kom_Cameroon = 'bkm'; + case Bukitan = 'bkn'; + case Kwa = 'bko'; + case Boko_Democratic_Republic_of_Congo = 'bkp'; + case Bakairi = 'bkq'; + case Bakumpai = 'bkr'; + case Northern_Sorsoganon = 'bks'; + case Boloki = 'bkt'; + case Buhid = 'bku'; + case Bekwarra = 'bkv'; + case Bekwel = 'bkw'; + case Baikeno = 'bkx'; + case Bokyi = 'bky'; + case Bungku = 'bkz'; + case Siksika = 'bla'; + case Bilua = 'blb'; + case Bella_Coola = 'blc'; + case Bolango = 'bld'; + case Balanta_Kentohe = 'ble'; + case Buol = 'blf'; + case Kuwaa = 'blh'; + case Bolia = 'bli'; + case Bolongan = 'blj'; + case Pa_o_Karen = 'blk'; + case Biloxi = 'bll'; + case Beli_South_Sudan = 'blm'; + case Southern_Catanduanes_Bikol = 'bln'; + case Anii = 'blo'; + case Blablanga = 'blp'; + case Baluan_Pam = 'blq'; + case Blang = 'blr'; + case Balaesang = 'bls'; + case Tai_Dam = 'blt'; + case Kibala = 'blv'; + case Balangao = 'blw'; + case Mag_Indi_Ayta = 'blx'; + case Notre = 'bly'; + case Balantak = 'blz'; + case Lame = 'bma'; + case Bembe = 'bmb'; + case Biem = 'bmc'; + case Baga_Manduri = 'bmd'; + case Limassa = 'bme'; + case Bom_Kim = 'bmf'; + case Bamwe = 'bmg'; + case Kein = 'bmh'; + case Bagirmi = 'bmi'; + case Bote_Majhi = 'bmj'; + case Ghayavi = 'bmk'; + case Bomboli = 'bml'; + case Northern_Betsimisaraka_Malagasy = 'bmm'; + case Bina_Papua_New_Guinea = 'bmn'; + case Bambalang = 'bmo'; + case Bulgebi = 'bmp'; + case Bomu = 'bmq'; + case Muinane = 'bmr'; + case Bilma_Kanuri = 'bms'; + case Biao_Mon = 'bmt'; + case Somba_Siawari = 'bmu'; + case Bum = 'bmv'; + case Bomwali = 'bmw'; + case Baimak = 'bmx'; + case Baramu = 'bmz'; + case Bonerate = 'bna'; + case Bookan = 'bnb'; + case Bontok = 'bnc'; + case Banda_Indonesia = 'bnd'; + case Bintauna = 'bne'; + case Masiwang = 'bnf'; + case Benga = 'bng'; + case Bangi = 'bni'; + case Eastern_Tawbuid = 'bnj'; + case Bierebo = 'bnk'; + case Boon = 'bnl'; + case Batanga = 'bnm'; + case Bunun = 'bnn'; + case Bantoanon = 'bno'; + case Bola = 'bnp'; + case Bantik = 'bnq'; + case Butmas_Tur = 'bnr'; + case Bundeli = 'bns'; + case Bentong = 'bnu'; + case Bonerif = 'bnv'; + case Bisis = 'bnw'; + case Bangubangu = 'bnx'; + case Bintulu = 'bny'; + case Beezen = 'bnz'; + case Bora = 'boa'; + case Aweer = 'bob'; + case Tibetan = 'bod'; + case Mundabli = 'boe'; + case Bolon = 'bof'; + case Bamako_Sign_Language = 'bog'; + case Boma = 'boh'; + case Barbareno = 'boi'; + case Anjam = 'boj'; + case Bonjo = 'bok'; + case Bole = 'bol'; + case Berom = 'bom'; + case Bine = 'bon'; + case Tiemacewe_Bozo = 'boo'; + case Bonkiman = 'bop'; + case Bogaya = 'boq'; + case Bororo = 'bor'; + case Bosnian = 'bos'; + case Bongo = 'bot'; + case Bondei = 'bou'; + case Tuwuli = 'bov'; + case Rema = 'bow'; + case Buamu = 'box'; + case Bodo_Central_African_Republic = 'boy'; + case Tieyaxo_Bozo = 'boz'; + case Daakaka = 'bpa'; + case Mbuk = 'bpc'; + case Banda_Banda = 'bpd'; + case Bauni = 'bpe'; + case Bonggo = 'bpg'; + case Botlikh = 'bph'; + case Bagupi = 'bpi'; + case Binji = 'bpj'; + case Orowe = 'bpk'; + case Broome_Pearling_Lugger_Pidgin = 'bpl'; + case Biyom = 'bpm'; + case Dzao_Min = 'bpn'; + case Anasi = 'bpo'; + case Kaure = 'bpp'; + case Banda_Malay = 'bpq'; + case Koronadal_Blaan = 'bpr'; + case Sarangani_Blaan = 'bps'; + case Barrow_Point = 'bpt'; + case Bongu = 'bpu'; + case Bian_Marind = 'bpv'; + case Bo_Papua_New_Guinea = 'bpw'; + case Palya_Bareli = 'bpx'; + case Bishnupriya = 'bpy'; + case Bilba = 'bpz'; + case Tchumbuli = 'bqa'; + case Bagusa = 'bqb'; + case Boko_Benin = 'bqc'; + case Bung = 'bqd'; + case Baga_Kaloum = 'bqf'; + case Bago_Kusuntu = 'bqg'; + case Baima = 'bqh'; + case Bakhtiari = 'bqi'; + case Bandial = 'bqj'; + case Banda_Mbres = 'bqk'; + case Bilakura = 'bql'; + case Wumboko = 'bqm'; + case Bulgarian_Sign_Language = 'bqn'; + case Balo = 'bqo'; + case Busa = 'bqp'; + case Biritai = 'bqq'; + case Burusu = 'bqr'; + case Bosngun = 'bqs'; + case Bamukumbit = 'bqt'; + case Boguru = 'bqu'; + case Koro_Wachi = 'bqv'; + case Buru_Nigeria = 'bqw'; + case Baangi = 'bqx'; + case Bengkala_Sign_Language = 'bqy'; + case Bakaka = 'bqz'; + case Braj = 'bra'; + case Brao = 'brb'; + case Berbice_Creole_Dutch = 'brc'; + case Baraamu = 'brd'; + case Breton = 'bre'; + case Bira = 'brf'; + case Baure = 'brg'; + case Brahui = 'brh'; + case Mokpwe = 'bri'; + case Bieria = 'brj'; + case Birked = 'brk'; + case Birwa = 'brl'; + case Barambu = 'brm'; + case Boruca = 'brn'; + case Brokkat = 'bro'; + case Barapasi = 'brp'; + case Breri = 'brq'; + case Birao = 'brr'; + case Baras = 'brs'; + case Bitare = 'brt'; + case Eastern_Bru = 'bru'; + case Western_Bru = 'brv'; + case Bellari = 'brw'; + case Bodo_India = 'brx'; + case Burui = 'bry'; + case Bilbil = 'brz'; + case Abinomn = 'bsa'; + case Brunei_Bisaya = 'bsb'; + case Bassari = 'bsc'; + case Wushi = 'bse'; + case Bauchi = 'bsf'; + case Bashkardi = 'bsg'; + case Kati = 'bsh'; + case Bassossi = 'bsi'; + case Bangwinji = 'bsj'; + case Burushaski = 'bsk'; + case Basa_Gumna = 'bsl'; + case Busami = 'bsm'; + case Barasana_Eduria = 'bsn'; + case Buso = 'bso'; + case Baga_Sitemu = 'bsp'; + case Bassa = 'bsq'; + case Bassa_Kontagora = 'bsr'; + case Akoose = 'bss'; + case Basketo = 'bst'; + case Bahonsuai = 'bsu'; + case Baga_Sobane = 'bsv'; + case Baiso = 'bsw'; + case Yangkam = 'bsx'; + case Sabah_Bisaya = 'bsy'; + case Bata = 'bta'; + case Bati_Cameroon = 'btc'; + case Batak_Dairi = 'btd'; + case Gamo_Ningi = 'bte'; + case Birgit = 'btf'; + case Gagnoa_Bete = 'btg'; + case Biatah_Bidayuh = 'bth'; + case Burate = 'bti'; + case Bacanese_Malay = 'btj'; + case Batak_Mandailing = 'btm'; + case Ratagnon = 'btn'; + case Rinconada_Bikol = 'bto'; + case Budibud = 'btp'; + case Batek = 'btq'; + case Baetora = 'btr'; + case Batak_Simalungun = 'bts'; + case Bete_Bendi = 'btt'; + case Batu = 'btu'; + case Bateri = 'btv'; + case Butuanon = 'btw'; + case Batak_Karo = 'btx'; + case Bobot = 'bty'; + case Batak_Alas_Kluet = 'btz'; + case Buriat = 'bua'; + case Bua = 'bub'; + case Bushi = 'buc'; + case Ntcham = 'bud'; + case Beothuk = 'bue'; + case Bushoong = 'buf'; + case Buginese = 'bug'; + case Younuo_Bunu = 'buh'; + case Bongili = 'bui'; + case Basa_Gurmana = 'buj'; + case Bugawac = 'buk'; + case Bulgarian = 'bul'; + case Bulu_Cameroon = 'bum'; + case Sherbro = 'bun'; + case Terei = 'buo'; + case Busoa = 'bup'; + case Brem = 'buq'; + case Bokobaru = 'bus'; + case Bungain = 'but'; + case Budu = 'buu'; + case Bun = 'buv'; + case Bubi = 'buw'; + case Boghom = 'bux'; + case Bullom_So = 'buy'; + case Bukwen = 'buz'; + case Barein = 'bva'; + case Bube = 'bvb'; + case Baelelea = 'bvc'; + case Baeggu = 'bvd'; + case Berau_Malay = 'bve'; + case Boor = 'bvf'; + case Bonkeng = 'bvg'; + case Bure = 'bvh'; + case Belanda_Viri = 'bvi'; + case Baan = 'bvj'; + case Bukat = 'bvk'; + case Bolivian_Sign_Language = 'bvl'; + case Bamunka = 'bvm'; + case Buna = 'bvn'; + case Bolgo = 'bvo'; + case Bumang = 'bvp'; + case Birri = 'bvq'; + case Burarra = 'bvr'; + case Bati_Indonesia = 'bvt'; + case Bukit_Malay = 'bvu'; + case Baniva = 'bvv'; + case Boga = 'bvw'; + case Dibole = 'bvx'; + case Baybayanon = 'bvy'; + case Bauzi = 'bvz'; + case Bwatoo = 'bwa'; + case Namosi_Naitasiri_Serua = 'bwb'; + case Bwile = 'bwc'; + case Bwaidoka = 'bwd'; + case Bwe_Karen = 'bwe'; + case Boselewa = 'bwf'; + case Barwe = 'bwg'; + case Bishuo = 'bwh'; + case Baniwa = 'bwi'; + case Laa_Laa_Bwamu = 'bwj'; + case Bauwaki = 'bwk'; + case Bwela = 'bwl'; + case Biwat = 'bwm'; + case Wunai_Bunu = 'bwn'; + case Boro_Ethiopia = 'bwo'; + case Mandobo_Bawah = 'bwp'; + case Southern_Bobo_Madare = 'bwq'; + case Bura_Pabir = 'bwr'; + case Bomboma = 'bws'; + case Bafaw_Balong = 'bwt'; + case Buli_Ghana = 'bwu'; + case Bwa = 'bww'; + case Bu_Nao_Bunu = 'bwx'; + case Cwi_Bwamu = 'bwy'; + case Bwisi = 'bwz'; + case Tairaha = 'bxa'; + case Belanda_Bor = 'bxb'; + case Molengue = 'bxc'; + case Pela = 'bxd'; + case Birale = 'bxe'; + case Bilur = 'bxf'; + case Bangala = 'bxg'; + case Buhutu = 'bxh'; + case Pirlatapa = 'bxi'; + case Bayungu = 'bxj'; + case Bukusu = 'bxk'; + case Jalkunan = 'bxl'; + case Mongolia_Buriat = 'bxm'; + case Burduna = 'bxn'; + case Barikanchi = 'bxo'; + case Bebil = 'bxp'; + case Beele = 'bxq'; + case Russia_Buriat = 'bxr'; + case Busam = 'bxs'; + case China_Buriat = 'bxu'; + case Berakou = 'bxv'; + case Bankagooma = 'bxw'; + case Binahari = 'bxz'; + case Batak = 'bya'; + case Bikya = 'byb'; + case Ubaghara = 'byc'; + case Benyadu = 'byd'; + case Pouye = 'bye'; + case Bete = 'byf'; + case Baygo = 'byg'; + case Bhujel = 'byh'; + case Buyu = 'byi'; + case Bina_Nigeria = 'byj'; + case Biao = 'byk'; + case Bayono = 'byl'; + case Bidjara = 'bym'; + case Bilin = 'byn'; + case Biyo = 'byo'; + case Bumaji = 'byp'; + case Basay = 'byq'; + case Baruya = 'byr'; + case Burak = 'bys'; + case Berti = 'byt'; + case Medumba = 'byv'; + case Belhariya = 'byw'; + case Qaqet = 'byx'; + case Banaro = 'byz'; + case Bandi = 'bza'; + case Andio = 'bzb'; + case Southern_Betsimisaraka_Malagasy = 'bzc'; + case Bribri = 'bzd'; + case Jenaama_Bozo = 'bze'; + case Boikin = 'bzf'; + case Babuza = 'bzg'; + case Mapos_Buang = 'bzh'; + case Bisu = 'bzi'; + case Belize_Kriol_English = 'bzj'; + case Nicaragua_Creole_English = 'bzk'; + case Boano_Sulawesi = 'bzl'; + case Bolondo = 'bzm'; + case Boano_Maluku = 'bzn'; + case Bozaba = 'bzo'; + case Kemberano = 'bzp'; + case Buli_Indonesia = 'bzq'; + case Biri = 'bzr'; + case Brazilian_Sign_Language = 'bzs'; + case Brithenig = 'bzt'; + case Burmeso = 'bzu'; + case Naami = 'bzv'; + case Basa_Nigeria = 'bzw'; + case Kelengaxo_Bozo = 'bzx'; + case Obanliku = 'bzy'; + case Evant = 'bzz'; + case Chorti = 'caa'; + case Garifuna = 'cab'; + case Chuj = 'cac'; + case Caddo = 'cad'; + case Lehar = 'cae'; + case Southern_Carrier = 'caf'; + case Nivacle = 'cag'; + case Cahuarano = 'cah'; + case Chane = 'caj'; + case Kaqchikel = 'cak'; + case Carolinian = 'cal'; + case Cemuhi = 'cam'; + case Chambri = 'can'; + case Chacobo = 'cao'; + case Chipaya = 'cap'; + case Car_Nicobarese = 'caq'; + case Galibi_Carib = 'car'; + case Tsimane = 'cas'; + case Catalan = 'cat'; + case Cavinena = 'cav'; + case Callawalla = 'caw'; + case Chiquitano = 'cax'; + case Cayuga = 'cay'; + case Canichana = 'caz'; + case Cabiyari = 'cbb'; + case Carapana = 'cbc'; + case Carijona = 'cbd'; + case Chimila = 'cbg'; + case Chachi = 'cbi'; + case Ede_Cabe = 'cbj'; + case Chavacano = 'cbk'; + case Bualkhaw_Chin = 'cbl'; + case Nyahkur = 'cbn'; + case Izora = 'cbo'; + case Tsucuba = 'cbq'; + case Cashibo_Cacataibo = 'cbr'; + case Cashinahua = 'cbs'; + case Chayahuita = 'cbt'; + case Candoshi_Shapra = 'cbu'; + case Cacua = 'cbv'; + case Kinabalian = 'cbw'; + case Carabayo = 'cby'; + case Chamicuro = 'ccc'; + case Cafundo_Creole = 'ccd'; + case Chopi = 'cce'; + case Samba_Daka = 'ccg'; + case Atsam = 'cch'; + case Kasanga = 'ccj'; + case Cutchi_Swahili = 'ccl'; + case Malaccan_Creole_Malay = 'ccm'; + case Comaltepec_Chinantec = 'cco'; + case Chakma = 'ccp'; + case Cacaopera = 'ccr'; + case Choni = 'cda'; + case Chenchu = 'cde'; + case Chiru = 'cdf'; + case Chambeali = 'cdh'; + case Chodri = 'cdi'; + case Churahi = 'cdj'; + case Chepang = 'cdm'; + case Chaudangsi = 'cdn'; + case Min_Dong_Chinese = 'cdo'; + case Cinda_Regi_Tiyal = 'cdr'; + case Chadian_Sign_Language = 'cds'; + case Chadong = 'cdy'; + case Koda = 'cdz'; + case Lower_Chehalis = 'cea'; + case Cebuano = 'ceb'; + case Chamacoco = 'ceg'; + case Eastern_Khumi_Chin = 'cek'; + case Cen = 'cen'; + case Czech = 'ces'; + case Centuum = 'cet'; + case Ekai_Chin = 'cey'; + case Dijim_Bwilim = 'cfa'; + case Cara = 'cfd'; + case Como_Karim = 'cfg'; + case Falam_Chin = 'cfm'; + case Changriwa = 'cga'; + case Kagayanen = 'cgc'; + case Chiga = 'cgg'; + case Chocangacakha = 'cgk'; + case Chamorro = 'cha'; + case Chibcha = 'chb'; + case Catawba = 'chc'; + case Highland_Oaxaca_Chontal = 'chd'; + case Chechen = 'che'; + case Tabasco_Chontal = 'chf'; + case Chagatai = 'chg'; + case Chinook = 'chh'; + case Ojitlan_Chinantec = 'chj'; + case Chuukese = 'chk'; + case Cahuilla = 'chl'; + case Mari_Russia = 'chm'; + case Chinook_jargon = 'chn'; + case Choctaw = 'cho'; + case Chipewyan = 'chp'; + case Quiotepec_Chinantec = 'chq'; + case Cherokee = 'chr'; + case Cholon = 'cht'; + case Church_Slavic = 'chu'; + case Chuvash = 'chv'; + case Chuwabu = 'chw'; + case Chantyal = 'chx'; + case Cheyenne = 'chy'; + case Ozumacin_Chinantec = 'chz'; + case Cia_Cia = 'cia'; + case Ci_Gbe = 'cib'; + case Chickasaw = 'cic'; + case Chimariko = 'cid'; + case Cineni = 'cie'; + case Chinali = 'cih'; + case Chitkuli_Kinnauri = 'cik'; + case Cimbrian = 'cim'; + case Cinta_Larga = 'cin'; + case Chiapanec = 'cip'; + case Tiri = 'cir'; + case Chippewa = 'ciw'; + case Chaima = 'ciy'; + case Western_Cham = 'cja'; + case Chru = 'cje'; + case Upper_Chehalis = 'cjh'; + case Chamalal = 'cji'; + case Chokwe = 'cjk'; + case Eastern_Cham = 'cjm'; + case Chenapian = 'cjn'; + case Asheninka_Pajonal = 'cjo'; + case Cabecar = 'cjp'; + case Shor = 'cjs'; + case Chuave = 'cjv'; + case Jinyu_Chinese = 'cjy'; + case Central_Kurdish = 'ckb'; + case Chak = 'ckh'; + case Cibak = 'ckl'; + case Chakavian = 'ckm'; + case Kaang_Chin = 'ckn'; + case Anufo = 'cko'; + case Kajakse = 'ckq'; + case Kairak = 'ckr'; + case Tayo = 'cks'; + case Chukot = 'ckt'; + case Koasati = 'cku'; + case Kavalan = 'ckv'; + case Caka = 'ckx'; + case Cakfem_Mushere = 'cky'; + case Cakchiquel_Quiche_Mixed_Language = 'ckz'; + case Ron = 'cla'; + case Chilcotin = 'clc'; + case Chaldean_Neo_Aramaic = 'cld'; + case Lealao_Chinantec = 'cle'; + case Chilisso = 'clh'; + case Chakali = 'cli'; + case Laitu_Chin = 'clj'; + case Idu_Mishmi = 'clk'; + case Chala = 'cll'; + case Clallam = 'clm'; + case Lowland_Oaxaca_Chontal = 'clo'; + case Classical_Sanskrit = 'cls'; + case Lautu_Chin = 'clt'; + case Caluyanun = 'clu'; + case Chulym = 'clw'; + case Eastern_Highland_Chatino = 'cly'; + case Maa = 'cma'; + case Cerma = 'cme'; + case Classical_Mongolian = 'cmg'; + case Embera_Chami = 'cmi'; + case Campalagian = 'cml'; + case Michigamea = 'cmm'; + case Mandarin_Chinese = 'cmn'; + case Central_Mnong = 'cmo'; + case Mro_Khimi_Chin = 'cmr'; + case Messapic = 'cms'; + case Camtho = 'cmt'; + case Changthang = 'cna'; + case Chinbon_Chin = 'cnb'; + case Coong = 'cnc'; + case Northern_Qiang = 'cng'; + case Hakha_Chin = 'cnh'; + case Ashaninka = 'cni'; + case Khumi_Chin = 'cnk'; + case Lalana_Chinantec = 'cnl'; + case Con = 'cno'; + case Northern_Ping_Chinese = 'cnp'; + case Chung = 'cnq'; + case Montenegrin = 'cnr'; + case Central_Asmat = 'cns'; + case Tepetotutla_Chinantec = 'cnt'; + case Chenoua = 'cnu'; + case Ngawn_Chin = 'cnw'; + case Middle_Cornish = 'cnx'; + case Cocos_Islands_Malay = 'coa'; + case Chicomuceltec = 'cob'; + case Cocopa = 'coc'; + case Cocama_Cocamilla = 'cod'; + case Koreguaje = 'coe'; + case Colorado = 'cof'; + case Chong = 'cog'; + case Chonyi_Dzihana_Kauma = 'coh'; + case Cochimi = 'coj'; + case Santa_Teresa_Cora = 'cok'; + case Columbia_Wenatchi = 'col'; + case Comanche = 'com'; + case Cofan = 'con'; + case Comox = 'coo'; + case Coptic = 'cop'; + case Coquille = 'coq'; + case Cornish = 'cor'; + case Corsican = 'cos'; + case Caquinte = 'cot'; + case Wamey = 'cou'; + case Cao_Miao = 'cov'; + case Cowlitz = 'cow'; + case Nanti = 'cox'; + case Chochotec = 'coz'; + case Palantla_Chinantec = 'cpa'; + case Ucayali_Yurua_Asheninka = 'cpb'; + case Ajyininka_Apurucayali = 'cpc'; + case Cappadocian_Greek = 'cpg'; + case Chinese_Pidgin_English = 'cpi'; + case Cherepon = 'cpn'; + case Kpeego = 'cpo'; + case Capiznon = 'cps'; + case Pichis_Asheninka = 'cpu'; + case Pu_Xian_Chinese = 'cpx'; + case South_Ucayali_Asheninka = 'cpy'; + case Chuanqiandian_Cluster_Miao = 'cqd'; + case Chara = 'cra'; + case Island_Carib = 'crb'; + case Lonwolwol = 'crc'; + case Coeur_d_Alene = 'crd'; + case Cree = 'cre'; + case Caramanta = 'crf'; + case Michif = 'crg'; + case Crimean_Tatar = 'crh'; + case Saotomense = 'cri'; + case Southern_East_Cree = 'crj'; + case Plains_Cree = 'crk'; + case Northern_East_Cree = 'crl'; + case Moose_Cree = 'crm'; + case El_Nayar_Cora = 'crn'; + case Crow = 'cro'; + case Iyo_wujwa_Chorote = 'crq'; + case Carolina_Algonquian = 'crr'; + case Seselwa_Creole_French = 'crs'; + case Iyojwa_ja_Chorote = 'crt'; + case Chaura = 'crv'; + case Chrau = 'crw'; + case Carrier = 'crx'; + case Cori = 'cry'; + case Cruzeno = 'crz'; + case Chiltepec_Chinantec = 'csa'; + case Kashubian = 'csb'; + case Catalan_Sign_Language = 'csc'; + case Chiangmai_Sign_Language = 'csd'; + case Czech_Sign_Language = 'cse'; + case Cuba_Sign_Language = 'csf'; + case Chilean_Sign_Language = 'csg'; + case Asho_Chin = 'csh'; + case Coast_Miwok = 'csi'; + case Songlai_Chin = 'csj'; + case Jola_Kasa = 'csk'; + case Chinese_Sign_Language = 'csl'; + case Central_Sierra_Miwok = 'csm'; + case Colombian_Sign_Language = 'csn'; + case Sochiapam_Chinantec = 'cso'; + case Southern_Ping_Chinese = 'csp'; + case Croatia_Sign_Language = 'csq'; + case Costa_Rican_Sign_Language = 'csr'; + case Southern_Ohlone = 'css'; + case Northern_Ohlone = 'cst'; + case Sumtu_Chin = 'csv'; + case Swampy_Cree = 'csw'; + case Cambodian_Sign_Language = 'csx'; + case Siyin_Chin = 'csy'; + case Coos = 'csz'; + case Tataltepec_Chatino = 'cta'; + case Chetco = 'ctc'; + case Tedim_Chin = 'ctd'; + case Tepinapa_Chinantec = 'cte'; + case Chittagonian = 'ctg'; + case Thaiphum_Chin = 'cth'; + case Tlacoatzintepec_Chinantec = 'ctl'; + case Chitimacha = 'ctm'; + case Chhintange = 'ctn'; + case Embera_Catio = 'cto'; + case Western_Highland_Chatino = 'ctp'; + case Northern_Catanduanes_Bikol = 'cts'; + case Wayanad_Chetti = 'ctt'; + case Chol = 'ctu'; + case Moundadan_Chetty = 'cty'; + case Zacatepec_Chatino = 'ctz'; + case Cua = 'cua'; + case Cubeo = 'cub'; + case Usila_Chinantec = 'cuc'; + case Chuka = 'cuh'; + case Cuiba = 'cui'; + case Mashco_Piro = 'cuj'; + case San_Blas_Kuna = 'cuk'; + case Culina = 'cul'; + case Cumanagoto = 'cuo'; + case Cupeno = 'cup'; + case Cun = 'cuq'; + case Chhulung = 'cur'; + case Teutila_Cuicatec = 'cut'; + case Tai_Ya = 'cuu'; + case Cuvok = 'cuv'; + case Chukwa = 'cuw'; + case Tepeuxila_Cuicatec = 'cux'; + case Cuitlatec = 'cuy'; + case Chug = 'cvg'; + case Valle_Nacional_Chinantec = 'cvn'; + case Kabwa = 'cwa'; + case Maindo = 'cwb'; + case Woods_Cree = 'cwd'; + case Kwere = 'cwe'; + case Chewong = 'cwg'; + case Kuwaataay = 'cwt'; + case Cha_ari = 'cxh'; + case Nopala_Chatino = 'cya'; + case Cayubaba = 'cyb'; + case Welsh = 'cym'; + case Cuyonon = 'cyo'; + case Huizhou_Chinese = 'czh'; + case Knaanic = 'czk'; + case Zenzontepec_Chatino = 'czn'; + case Min_Zhong_Chinese = 'czo'; + case Zotung_Chin = 'czt'; + case Dangaleat = 'daa'; + case Dambi = 'dac'; + case Marik = 'dad'; + case Duupa = 'dae'; + case Dagbani = 'dag'; + case Gwahatike = 'dah'; + case Day = 'dai'; + case Dar_Fur_Daju = 'daj'; + case Dakota = 'dak'; + case Dahalo = 'dal'; + case Damakawa = 'dam'; + case Danish = 'dan'; + case Daai_Chin = 'dao'; + case Dandami_Maria = 'daq'; + case Dargwa = 'dar'; + case Daho_Doo = 'das'; + case Dar_Sila_Daju = 'dau'; + case Taita = 'dav'; + case Davawenyo = 'daw'; + case Dayi = 'dax'; + case Dao = 'daz'; + case Bangime = 'dba'; + case Deno = 'dbb'; + case Dadiya = 'dbd'; + case Dabe = 'dbe'; + case Edopi = 'dbf'; + case Dogul_Dom_Dogon = 'dbg'; + case Doka = 'dbi'; + case Ida_an = 'dbj'; + case Dyirbal = 'dbl'; + case Duguri = 'dbm'; + case Duriankere = 'dbn'; + case Dulbu = 'dbo'; + case Duwai = 'dbp'; + case Daba = 'dbq'; + case Dabarre = 'dbr'; + case Ben_Tey_Dogon = 'dbt'; + case Bondum_Dom_Dogon = 'dbu'; + case Dungu = 'dbv'; + case Bankan_Tey_Dogon = 'dbw'; + case Dibiyaso = 'dby'; + case Deccan = 'dcc'; + case Negerhollands = 'dcr'; + case Dadi_Dadi = 'dda'; + case Dongotono = 'ddd'; + case Doondo = 'dde'; + case Fataluku = 'ddg'; + case West_Goodenough = 'ddi'; + case Jaru = 'ddj'; + case Dendi_Benin = 'ddn'; + case Dido = 'ddo'; + case Dhudhuroa = 'ddr'; + case Donno_So_Dogon = 'dds'; + case Dawera_Daweloor = 'ddw'; + case Dagik = 'dec'; + case Dedua = 'ded'; + case Dewoin = 'dee'; + case Dezfuli = 'def'; + case Degema = 'deg'; + case Dehwari = 'deh'; + case Demisa = 'dei'; + case Dek = 'dek'; + case Delaware = 'del'; + case Dem = 'dem'; + case Slave_Athapascan = 'den'; + case Pidgin_Delaware = 'dep'; + case Dendi_Central_African_Republic = 'deq'; + case Deori = 'der'; + case Desano = 'des'; + case German = 'deu'; + case Domung = 'dev'; + case Dengese = 'dez'; + case Southern_Dagaare = 'dga'; + case Bunoge_Dogon = 'dgb'; + case Casiguran_Dumagat_Agta = 'dgc'; + case Dagaari_Dioula = 'dgd'; + case Degenan = 'dge'; + case Doga = 'dgg'; + case Dghwede = 'dgh'; + case Northern_Dagara = 'dgi'; + case Dagba = 'dgk'; + case Andaandi = 'dgl'; + case Dagoman = 'dgn'; + case Dogri_individual_language = 'dgo'; + case Dogrib = 'dgr'; + case Dogoso = 'dgs'; + case Ndra_ngith = 'dgt'; + case Daungwurrung = 'dgw'; + case Doghoro = 'dgx'; + case Daga = 'dgz'; + case Dhundari = 'dhd'; + case Dhangu_Djangu = 'dhg'; + case Dhimal = 'dhi'; + case Dhalandji = 'dhl'; + case Zemba = 'dhm'; + case Dhanki = 'dhn'; + case Dhodia = 'dho'; + case Dhargari = 'dhr'; + case Dhaiso = 'dhs'; + case Dhurga = 'dhu'; + case Dehu = 'dhv'; + case Dhanwar_Nepal = 'dhw'; + case Dhungaloo = 'dhx'; + case Dia = 'dia'; + case South_Central_Dinka = 'dib'; + case Lakota_Dida = 'dic'; + case Didinga = 'did'; + case Dieri = 'dif'; + case Digo = 'dig'; + case Kumiai = 'dih'; + case Dimbong = 'dii'; + case Dai = 'dij'; + case Southwestern_Dinka = 'dik'; + case Dilling = 'dil'; + case Dime = 'dim'; + case Dinka = 'din'; + case Dibo = 'dio'; + case Northeastern_Dinka = 'dip'; + case Dimli_individual_language = 'diq'; + case Dirim = 'dir'; + case Dimasa = 'dis'; + case Diriku = 'diu'; + case Dhivehi = 'div'; + case Northwestern_Dinka = 'diw'; + case Dixon_Reef = 'dix'; + case Diuwe = 'diy'; + case Ding = 'diz'; + case Djadjawurrung = 'dja'; + case Djinba = 'djb'; + case Dar_Daju_Daju = 'djc'; + case Djamindjung = 'djd'; + case Zarma = 'dje'; + case Djangun = 'djf'; + case Djinang = 'dji'; + case Djeebbana = 'djj'; + case Eastern_Maroon_Creole = 'djk'; + case Jamsay_Dogon = 'djm'; + case Jawoyn = 'djn'; + case Jangkang = 'djo'; + case Djambarrpuyngu = 'djr'; + case Kapriman = 'dju'; + case Djawi = 'djw'; + case Dakpakha = 'dka'; + case Kadung = 'dkg'; + case Dakka = 'dkk'; + case Kuijau = 'dkr'; + case Southeastern_Dinka = 'dks'; + case Mazagway = 'dkx'; + case Dolgan = 'dlg'; + case Dahalik = 'dlk'; + case Dalmatian = 'dlm'; + case Darlong = 'dln'; + case Duma = 'dma'; + case Mombo_Dogon = 'dmb'; + case Gavak = 'dmc'; + case Madhi_Madhi = 'dmd'; + case Dugwor = 'dme'; + case Medefaidrin = 'dmf'; + case Upper_Kinabatangan = 'dmg'; + case Domaaki = 'dmk'; + case Dameli = 'dml'; + case Dama = 'dmm'; + case Kemedzung = 'dmo'; + case East_Damar = 'dmr'; + case Dampelas = 'dms'; + case Dubu = 'dmu'; + case Dumpas = 'dmv'; + case Mudburra = 'dmw'; + case Dema = 'dmx'; + case Demta = 'dmy'; + case Upper_Grand_Valley_Dani = 'dna'; + case Daonda = 'dnd'; + case Ndendeule = 'dne'; + case Dungan = 'dng'; + case Lower_Grand_Valley_Dani = 'dni'; + case Dan = 'dnj'; + case Dengka = 'dnk'; + case Dzuungoo = 'dnn'; + case Ndrulo = 'dno'; + case Danaru = 'dnr'; + case Mid_Grand_Valley_Dani = 'dnt'; + case Danau = 'dnu'; + case Danu = 'dnv'; + case Western_Dani = 'dnw'; + case Deni = 'dny'; + case Dom = 'doa'; + case Dobu = 'dob'; + case Northern_Dong = 'doc'; + case Doe = 'doe'; + case Domu = 'dof'; + case Dong = 'doh'; + case Dogri_macrolanguage = 'doi'; + case Dondo = 'dok'; + case Doso = 'dol'; + case Toura_Papua_New_Guinea = 'don'; + case Dongo = 'doo'; + case Lukpa = 'dop'; + case Dominican_Sign_Language = 'doq'; + case Dori_o = 'dor'; + case Dogose = 'dos'; + case Dass = 'dot'; + case Dombe = 'dov'; + case Doyayo = 'dow'; + case Bussa = 'dox'; + case Dompo = 'doy'; + case Dorze = 'doz'; + case Papar = 'dpp'; + case Dair = 'drb'; + case Minderico = 'drc'; + case Darmiya = 'drd'; + case Dolpo = 'dre'; + case Rungus = 'drg'; + case C_Lela = 'dri'; + case Paakantyi = 'drl'; + case West_Damar = 'drn'; + case Daro_Matu_Melanau = 'dro'; + case Dura = 'drq'; + case Gedeo = 'drs'; + case Drents = 'drt'; + case Rukai = 'dru'; + case Darai = 'dry'; + case Lower_Sorbian = 'dsb'; + case Dutch_Sign_Language = 'dse'; + case Daasanach = 'dsh'; + case Disa = 'dsi'; + case Dokshi = 'dsk'; + case Danish_Sign_Language = 'dsl'; + case Dusner = 'dsn'; + case Desiya = 'dso'; + case Tadaksahak = 'dsq'; + case Mardin_Sign_Language = 'dsz'; + case Daur = 'dta'; + case Labuk_Kinabatangan_Kadazan = 'dtb'; + case Ditidaht = 'dtd'; + case Adithinngithigh = 'dth'; + case Ana_Tinga_Dogon = 'dti'; + case Tene_Kan_Dogon = 'dtk'; + case Tomo_Kan_Dogon = 'dtm'; + case Daats_iin = 'dtn'; + case Tommo_So_Dogon = 'dto'; + case Kadazan_Dusun = 'dtp'; + case Lotud = 'dtr'; + case Toro_So_Dogon = 'dts'; + case Toro_Tegu_Dogon = 'dtt'; + case Tebul_Ure_Dogon = 'dtu'; + case Dotyali = 'dty'; + case Duala = 'dua'; + case Dubli = 'dub'; + case Duna = 'duc'; + case Umiray_Dumaget_Agta = 'due'; + case Dumbea = 'duf'; + case Duruma = 'dug'; + case Dungra_Bhil = 'duh'; + case Dumun = 'dui'; + case Uyajitaya = 'duk'; + case Alabat_Island_Agta = 'dul'; + case Middle_Dutch_ca_1050_1350 = 'dum'; + case Dusun_Deyah = 'dun'; + case Dupaninan_Agta = 'duo'; + case Duano = 'dup'; + case Dusun_Malang = 'duq'; + case Dii = 'dur'; + case Dumi = 'dus'; + case Drung = 'duu'; + case Duvle = 'duv'; + case Dusun_Witu = 'duw'; + case Duungooma = 'dux'; + case Dicamay_Agta = 'duy'; + case Duli_Gey = 'duz'; + case Duau = 'dva'; + case Diri = 'dwa'; + case Dawik_Kui = 'dwk'; + case Dawro = 'dwr'; + case Dutton_World_Speedwords = 'dws'; + case Dhuwal = 'dwu'; + case Dawawa = 'dww'; + case Dhuwaya = 'dwy'; + case Dewas_Rai = 'dwz'; + case Dyan = 'dya'; + case Dyaberdyaber = 'dyb'; + case Dyugun = 'dyd'; + case Villa_Viciosa_Agta = 'dyg'; + case Djimini_Senoufo = 'dyi'; + case Yanda_Dom_Dogon = 'dym'; + case Dyangadi = 'dyn'; + case Jola_Fonyi = 'dyo'; + case Dyarim = 'dyr'; + case Dyula = 'dyu'; + case Djabugay = 'dyy'; + case Tunzu = 'dza'; + case Daza = 'dzd'; + case Djiwarli = 'dze'; + case Dazaga = 'dzg'; + case Dzalakha = 'dzl'; + case Dzando = 'dzn'; + case Dzongkha = 'dzo'; + case Karenggapa = 'eaa'; + case Beginci = 'ebc'; + case Ebughu = 'ebg'; + case Eastern_Bontok = 'ebk'; + case Teke_Ebo = 'ebo'; + case Ebrie = 'ebr'; + case Embu = 'ebu'; + case Eteocretan = 'ecr'; + case Ecuadorian_Sign_Language = 'ecs'; + case Eteocypriot = 'ecy'; + case E = 'eee'; + case Efai = 'efa'; + case Efe = 'efe'; + case Efik = 'efi'; + case Ega = 'ega'; + case Emilian = 'egl'; + case Benamanga = 'egm'; + case Eggon = 'ego'; + case Egyptian_Ancient = 'egy'; + case Miyakubo_Sign_Language = 'ehs'; + case Ehueun = 'ehu'; + case Eipomek = 'eip'; + case Eitiep = 'eit'; + case Askopan = 'eiv'; + case Ejamat = 'eja'; + case Ekajuk = 'eka'; + case Ekit = 'eke'; + case Ekari = 'ekg'; + case Eki = 'eki'; + case Standard_Estonian = 'ekk'; + case Kol_Bangladesh = 'ekl'; + case Elip = 'ekm'; + case Koti = 'eko'; + case Ekpeye = 'ekp'; + case Yace = 'ekr'; + case Eastern_Kayah = 'eky'; + case Elepi = 'ele'; + case El_Hugeirat = 'elh'; + case Nding = 'eli'; + case Elkei = 'elk'; + case Modern_Greek_1453 = 'ell'; + case Eleme = 'elm'; + case El_Molo = 'elo'; + case Elu = 'elu'; + case Elamite = 'elx'; + case Emai_Iuleha_Ora = 'ema'; + case Embaloh = 'emb'; + case Emerillon = 'eme'; + case Eastern_Meohang = 'emg'; + case Mussau_Emira = 'emi'; + case Eastern_Maninkakan = 'emk'; + case Mamulique = 'emm'; + case Eman = 'emn'; + case Northern_Embera = 'emp'; + case Eastern_Minyag = 'emq'; + case Pacific_Gulf_Yupik = 'ems'; + case Eastern_Muria = 'emu'; + case Emplawas = 'emw'; + case Erromintxela = 'emx'; + case Epigraphic_Mayan = 'emy'; + case Mbessa = 'emz'; + case Apali = 'ena'; + case Markweeta = 'enb'; + case En = 'enc'; + case Ende = 'end'; + case Forest_Enets = 'enf'; + case English = 'eng'; + case Tundra_Enets = 'enh'; + case Enlhet = 'enl'; + case Middle_English_1100_1500 = 'enm'; + case Engenni = 'enn'; + case Enggano = 'eno'; + case Enga = 'enq'; + case Emumu = 'enr'; + case Enu = 'enu'; + case Enwan_Edo_State = 'env'; + case Enwan_Akwa_Ibom_State = 'enw'; + case Enxet = 'enx'; + case Beti_Cote_d_Ivoire = 'eot'; + case Epie = 'epi'; + case Esperanto = 'epo'; + case Eravallan = 'era'; + case Sie = 'erg'; + case Eruwa = 'erh'; + case Ogea = 'eri'; + case South_Efate = 'erk'; + case Horpa = 'ero'; + case Erre = 'err'; + case Ersu = 'ers'; + case Eritai = 'ert'; + case Erokwanas = 'erw'; + case Ese_Ejja = 'ese'; + case Aheri_Gondi = 'esg'; + case Eshtehardi = 'esh'; + case North_Alaskan_Inupiatun = 'esi'; + case Northwest_Alaska_Inupiatun = 'esk'; + case Egypt_Sign_Language = 'esl'; + case Esuma = 'esm'; + case Salvadoran_Sign_Language = 'esn'; + case Estonian_Sign_Language = 'eso'; + case Esselen = 'esq'; + case Central_Siberian_Yupik = 'ess'; + case Estonian = 'est'; + case Central_Yupik = 'esu'; + case Eskayan = 'esy'; + case Etebi = 'etb'; + case Etchemin = 'etc'; + case Ethiopian_Sign_Language = 'eth'; + case Eton_Vanuatu = 'etn'; + case Eton_Cameroon = 'eto'; + case Edolo = 'etr'; + case Yekhee = 'ets'; + case Etruscan = 'ett'; + case Ejagham = 'etu'; + case Eten = 'etx'; + case Semimi = 'etz'; + case Eudeve = 'eud'; + case Basque = 'eus'; + case Even = 'eve'; + case Uvbie = 'evh'; + case Evenki = 'evn'; + case Ewe = 'ewe'; + case Ewondo = 'ewo'; + case Extremaduran = 'ext'; + case Eyak = 'eya'; + case Keiyo = 'eyo'; + case Ezaa = 'eza'; + case Uzekwe = 'eze'; + case Fasu = 'faa'; + case Fa_d_Ambu = 'fab'; + case Wagi = 'fad'; + case Fagani = 'faf'; + case Finongan = 'fag'; + case Baissa_Fali = 'fah'; + case Faiwol = 'fai'; + case Faita = 'faj'; + case Fang_Cameroon = 'fak'; + case South_Fali = 'fal'; + case Fam = 'fam'; + case Fang_Equatorial_Guinea = 'fan'; + case Faroese = 'fao'; + case Paloor = 'fap'; + case Fataleka = 'far'; + case Persian = 'fas'; + case Fanti = 'fat'; + case Fayu = 'fau'; + case Fala = 'fax'; + case Southwestern_Fars = 'fay'; + case Northwestern_Fars = 'faz'; + case West_Albay_Bikol = 'fbl'; + case Quebec_Sign_Language = 'fcs'; + case Feroge = 'fer'; + case Foia_Foia = 'ffi'; + case Maasina_Fulfulde = 'ffm'; + case Fongoro = 'fgr'; + case Nobiin = 'fia'; + case Fyer = 'fie'; + case Faifi = 'fif'; + case Fijian = 'fij'; + case Filipino = 'fil'; + case Finnish = 'fin'; + case Fipa = 'fip'; + case Firan = 'fir'; + case Tornedalen_Finnish = 'fit'; + case Fiwaga = 'fiw'; + case Kirya_Konzel = 'fkk'; + case Kven_Finnish = 'fkv'; + case Kalispel_Pend_d_Oreille = 'fla'; + case Foau = 'flh'; + case Fali = 'fli'; + case North_Fali = 'fll'; + case Flinders_Island = 'fln'; + case Fuliiru = 'flr'; + case Flaaitaal = 'fly'; + case Fe_fe = 'fmp'; + case Far_Western_Muria = 'fmu'; + case Fanbak = 'fnb'; + case Fanagalo = 'fng'; + case Fania = 'fni'; + case Foodo = 'fod'; + case Foi = 'foi'; + case Foma = 'fom'; + case Fon = 'fon'; + case Fore = 'for'; + case Siraya = 'fos'; + case Fernando_Po_Creole_English = 'fpe'; + case Fas = 'fqs'; + case French = 'fra'; + case Cajun_French = 'frc'; + case Fordata = 'frd'; + case Frankish = 'frk'; + case Middle_French_ca_1400_1600 = 'frm'; + case Old_French_842_ca_1400 = 'fro'; + case Arpitan = 'frp'; + case Forak = 'frq'; + case Northern_Frisian = 'frr'; + case Eastern_Frisian = 'frs'; + case Fortsenal = 'frt'; + case Western_Frisian = 'fry'; + case Finnish_Sign_Language = 'fse'; + case French_Sign_Language = 'fsl'; + case Finland_Swedish_Sign_Language = 'fss'; + case Adamawa_Fulfulde = 'fub'; + case Pulaar = 'fuc'; + case East_Futuna = 'fud'; + case Borgu_Fulfulde = 'fue'; + case Pular = 'fuf'; + case Western_Niger_Fulfulde = 'fuh'; + case Bagirmi_Fulfulde = 'fui'; + case Ko = 'fuj'; + case Fulah = 'ful'; + case Fum = 'fum'; + case Fulnio = 'fun'; + case Central_Eastern_Niger_Fulfulde = 'fuq'; + case Friulian = 'fur'; + case Futuna_Aniwa = 'fut'; + case Furu = 'fuu'; + case Nigerian_Fulfulde = 'fuv'; + case Fuyug = 'fuy'; + case Fur = 'fvr'; + case Fwai = 'fwa'; + case Fwe = 'fwe'; + case Ga = 'gaa'; + case Gabri = 'gab'; + case Mixed_Great_Andamanese = 'gac'; + case Gaddang = 'gad'; + case Guarequena = 'gae'; + case Gende = 'gaf'; + case Gagauz = 'gag'; + case Alekano = 'gah'; + case Borei = 'gai'; + case Gadsup = 'gaj'; + case Gamkonora = 'gak'; + case Galolen = 'gal'; + case Kandawo = 'gam'; + case Gan_Chinese = 'gan'; + case Gants = 'gao'; + case Gal = 'gap'; + case Gata = 'gaq'; + case Galeya = 'gar'; + case Adiwasi_Garasia = 'gas'; + case Kenati = 'gat'; + case Mudhili_Gadaba = 'gau'; + case Nobonob = 'gaw'; + case Borana_Arsi_Guji_Oromo = 'gax'; + case Gayo = 'gay'; + case West_Central_Oromo = 'gaz'; + case Gbaya_Central_African_Republic = 'gba'; + case Kaytetye = 'gbb'; + case Karajarri = 'gbd'; + case Niksek = 'gbe'; + case Gaikundi = 'gbf'; + case Gbanziri = 'gbg'; + case Defi_Gbe = 'gbh'; + case Galela = 'gbi'; + case Bodo_Gadaba = 'gbj'; + case Gaddi = 'gbk'; + case Gamit = 'gbl'; + case Garhwali = 'gbm'; + case Mo_da = 'gbn'; + case Northern_Grebo = 'gbo'; + case Gbaya_Bossangoa = 'gbp'; + case Gbaya_Bozoum = 'gbq'; + case Gbagyi = 'gbr'; + case Gbesi_Gbe = 'gbs'; + case Gagadu = 'gbu'; + case Gbanu = 'gbv'; + case Gabi_Gabi = 'gbw'; + case Eastern_Xwla_Gbe = 'gbx'; + case Gbari = 'gby'; + case Zoroastrian_Dari = 'gbz'; + case Mali = 'gcc'; + case Ganggalida = 'gcd'; + case Galice = 'gce'; + case Guadeloupean_Creole_French = 'gcf'; + case Grenadian_Creole_English = 'gcl'; + case Gaina = 'gcn'; + case Guianese_Creole_French = 'gcr'; + case Colonia_Tovar_German = 'gct'; + case Gade_Lohar = 'gda'; + case Pottangi_Ollar_Gadaba = 'gdb'; + case Gugu_Badhun = 'gdc'; + case Gedaged = 'gdd'; + case Gude = 'gde'; + case Guduf_Gava = 'gdf'; + case Ga_dang = 'gdg'; + case Gadjerawang = 'gdh'; + case Gundi = 'gdi'; + case Gurdjar = 'gdj'; + case Gadang = 'gdk'; + case Dirasha = 'gdl'; + case Laal = 'gdm'; + case Umanakaina = 'gdn'; + case Ghodoberi = 'gdo'; + case Mehri = 'gdq'; + case Wipi = 'gdr'; + case Ghandruk_Sign_Language = 'gds'; + case Kungardutyi = 'gdt'; + case Gudu = 'gdu'; + case Godwari = 'gdx'; + case Geruma = 'gea'; + case Kire = 'geb'; + case Gboloo_Grebo = 'gec'; + case Gade = 'ged'; + case Gerai = 'gef'; + case Gengle = 'geg'; + case Hutterite_German = 'geh'; + case Gebe = 'gei'; + case Gen = 'gej'; + case Ywom = 'gek'; + case ut_Ma_in = 'gel'; + case Geme = 'geq'; + case Geser_Gorom = 'ges'; + case Eviya = 'gev'; + case Gera = 'gew'; + case Garre = 'gex'; + case Enya = 'gey'; + case Geez = 'gez'; + case Patpatar = 'gfk'; + case Gafat = 'gft'; + case Gao = 'gga'; + case Gbii = 'ggb'; + case Gugadj = 'ggd'; + case Gurr_goni = 'gge'; + case Gurgula = 'ggg'; + case Kungarakany = 'ggk'; + case Ganglau = 'ggl'; + case Gitua = 'ggt'; + case Gagu = 'ggu'; + case Gogodala = 'ggw'; + case Ghadames = 'gha'; + case Hiberno_Scottish_Gaelic = 'ghc'; + case Southern_Ghale = 'ghe'; + case Northern_Ghale = 'ghh'; + case Geko_Karen = 'ghk'; + case Ghulfan = 'ghl'; + case Ghanongga = 'ghn'; + case Ghomara = 'gho'; + case Ghera = 'ghr'; + case Guhu_Samane = 'ghs'; + case Kuke = 'ght'; + case Kija = 'gia'; + case Gibanawa = 'gib'; + case Gail = 'gic'; + case Gidar = 'gid'; + case Gabogbo = 'gie'; + case Goaria = 'gig'; + case Githabul = 'gih'; + case Girirra = 'gii'; + case Gilbertese = 'gil'; + case Gimi_Eastern_Highlands = 'gim'; + case Hinukh = 'gin'; + case Gimi_West_New_Britain = 'gip'; + case Green_Gelao = 'giq'; + case Red_Gelao = 'gir'; + case North_Giziga = 'gis'; + case Gitxsan = 'git'; + case Mulao = 'giu'; + case White_Gelao = 'giw'; + case Gilima = 'gix'; + case Giyug = 'giy'; + case South_Giziga = 'giz'; + case Kachi_Koli = 'gjk'; + case Gunditjmara = 'gjm'; + case Gonja = 'gjn'; + case Gurindji_Kriol = 'gjr'; + case Gujari = 'gju'; + case Guya = 'gka'; + case Magi_Madang_Province = 'gkd'; + case Ndai = 'gke'; + case Gokana = 'gkn'; + case Kok_Nar = 'gko'; + case Guinea_Kpelle = 'gkp'; + case Ungkue = 'gku'; + case Scottish_Gaelic = 'gla'; + case Belning = 'glb'; + case Bon_Gula = 'glc'; + case Nanai = 'gld'; + case Irish = 'gle'; + case Galician = 'glg'; + case Northwest_Pashai = 'glh'; + case Gula_Iro = 'glj'; + case Gilaki = 'glk'; + case Garlali = 'gll'; + case Galambu = 'glo'; + case Glaro_Twabo = 'glr'; + case Gula_Chad = 'glu'; + case Manx = 'glv'; + case Glavda = 'glw'; + case Gule = 'gly'; + case Gambera = 'gma'; + case Gula_alaa = 'gmb'; + case Maghdi = 'gmd'; + case Magiyi = 'gmg'; + case Middle_High_German_ca_1050_1500 = 'gmh'; + case Middle_Low_German = 'gml'; + case Gbaya_Mbodomo = 'gmm'; + case Gimnime = 'gmn'; + case Mirning = 'gmr'; + case Gumalu = 'gmu'; + case Gamo = 'gmv'; + case Magoma = 'gmx'; + case Mycenaean_Greek = 'gmy'; + case Mgbolizhia = 'gmz'; + case Kaansa = 'gna'; + case Gangte = 'gnb'; + case Guanche = 'gnc'; + case Zulgo_Gemzek = 'gnd'; + case Ganang = 'gne'; + case Ngangam = 'gng'; + case Lere = 'gnh'; + case Gooniyandi = 'gni'; + case Ngen = 'gnj'; + case Gana = 'gnk'; + case Gangulu = 'gnl'; + case Ginuman = 'gnm'; + case Gumatj = 'gnn'; + case Northern_Gondi = 'gno'; + case Gana_2 = 'gnq'; + case Gureng_Gureng = 'gnr'; + case Guntai = 'gnt'; + case Gnau = 'gnu'; + case Western_Bolivian_Guarani = 'gnw'; + case Ganzi = 'gnz'; + case Guro = 'goa'; + case Playero = 'gob'; + case Gorakor = 'goc'; + case Godie = 'god'; + case Gongduk = 'goe'; + case Gofa = 'gof'; + case Gogo = 'gog'; + case Old_High_German_ca_750_1050 = 'goh'; + case Gobasi = 'goi'; + case Gowlan = 'goj'; + case Gowli = 'gok'; + case Gola = 'gol'; + case Goan_Konkani = 'gom'; + case Gondi = 'gon'; + case Gone_Dau = 'goo'; + case Yeretuar = 'gop'; + case Gorap = 'goq'; + case Gorontalo = 'gor'; + case Gronings = 'gos'; + case Gothic = 'got'; + case Gavar = 'gou'; + case Goo = 'gov'; + case Gorowa = 'gow'; + case Gobu = 'gox'; + case Goundo = 'goy'; + case Gozarkhani = 'goz'; + case Gupa_Abawa = 'gpa'; + case Ghanaian_Pidgin_English = 'gpe'; + case Taiap = 'gpn'; + case Ga_anda = 'gqa'; + case Guiqiong = 'gqi'; + case Guana_Brazil = 'gqn'; + case Gor = 'gqr'; + case Qau = 'gqu'; + case Rajput_Garasia = 'gra'; + case Grebo = 'grb'; + case Ancient_Greek_to_1453 = 'grc'; + case Guruntum_Mbaaru = 'grd'; + case Madi = 'grg'; + case Gbiri_Niragu = 'grh'; + case Ghari = 'gri'; + case Southern_Grebo = 'grj'; + case Kota_Marudu_Talantang = 'grm'; + case Guarani = 'grn'; + case Groma = 'gro'; + case Gorovu = 'grq'; + case Taznatit = 'grr'; + case Gresi = 'grs'; + case Garo = 'grt'; + case Kistane = 'gru'; + case Central_Grebo = 'grv'; + case Gweda = 'grw'; + case Guriaso = 'grx'; + case Barclayville_Grebo = 'gry'; + case Guramalum = 'grz'; + case Ghanaian_Sign_Language = 'gse'; + case German_Sign_Language = 'gsg'; + case Gusilay = 'gsl'; + case Guatemalan_Sign_Language = 'gsm'; + case Nema = 'gsn'; + case Southwest_Gbaya = 'gso'; + case Wasembo = 'gsp'; + case Greek_Sign_Language = 'gss'; + case Swiss_German = 'gsw'; + case Guato = 'gta'; + case Aghu_Tharnggala = 'gtu'; + case Shiki = 'gua'; + case Guajajara = 'gub'; + case Wayuu = 'guc'; + case Yocoboue_Dida = 'gud'; + case Gurindji = 'gue'; + case Gupapuyngu = 'guf'; + case Paraguayan_Guarani = 'gug'; + case Guahibo = 'guh'; + case Eastern_Bolivian_Guarani = 'gui'; + case Gujarati = 'guj'; + case Gumuz = 'guk'; + case Sea_Island_Creole_English = 'gul'; + case Guambiano = 'gum'; + case Mbya_Guarani = 'gun'; + case Guayabero = 'guo'; + case Gunwinggu = 'gup'; + case Ache = 'guq'; + case Farefare = 'gur'; + case Guinean_Sign_Language = 'gus'; + case Maleku_Jaika = 'gut'; + case Yanomamo = 'guu'; + case Gun = 'guw'; + case Gourmanchema = 'gux'; + case Gusii = 'guz'; + case Guana_Paraguay = 'gva'; + case Guanano = 'gvc'; + case Duwet = 'gve'; + case Golin = 'gvf'; + case Guaja = 'gvj'; + case Gulay = 'gvl'; + case Gurmana = 'gvm'; + case Kuku_Yalanji = 'gvn'; + case Gaviao_Do_Jiparana = 'gvo'; + case Para_Gaviao = 'gvp'; + case Gurung = 'gvr'; + case Gumawana = 'gvs'; + case Guyani = 'gvy'; + case Mbato = 'gwa'; + case Gwa = 'gwb'; + case Gawri = 'gwc'; + case Gawwada = 'gwd'; + case Gweno = 'gwe'; + case Gowro = 'gwf'; + case Moo = 'gwg'; + case Gwich_in = 'gwi'; + case Gwi = 'gwj'; + case Awngthim = 'gwm'; + case Gwandara = 'gwn'; + case Gwere = 'gwr'; + case Gawar_Bati = 'gwt'; + case Guwamu = 'gwu'; + case Kwini = 'gww'; + case Gua = 'gwx'; + case We_Southern = 'gxx'; + case Northwest_Gbaya = 'gya'; + case Garus = 'gyb'; + case Kayardild = 'gyd'; + case Gyem = 'gye'; + case Gungabula = 'gyf'; + case Gbayi = 'gyg'; + case Gyele = 'gyi'; + case Gayil = 'gyl'; + case Ngabere = 'gym'; + case Guyanese_Creole_English = 'gyn'; + case Gyalsumdo = 'gyo'; + case Guarayu = 'gyr'; + case Gunya = 'gyy'; + case Geji = 'gyz'; + case Ganza = 'gza'; + case Gazi = 'gzi'; + case Gane = 'gzn'; + case Han = 'haa'; + case Hanoi_Sign_Language = 'hab'; + case Gurani = 'hac'; + case Hatam = 'had'; + case Eastern_Oromo = 'hae'; + case Haiphong_Sign_Language = 'haf'; + case Hanga = 'hag'; + case Hahon = 'hah'; + case Haida = 'hai'; + case Hajong = 'haj'; + case Hakka_Chinese = 'hak'; + case Halang = 'hal'; + case Hewa = 'ham'; + case Hangaza = 'han'; + case Hako = 'hao'; + case Hupla = 'hap'; + case Ha = 'haq'; + case Harari = 'har'; + case Haisla = 'has'; + case Haitian = 'hat'; + case Hausa = 'hau'; + case Havu = 'hav'; + case Hawaiian = 'haw'; + case Southern_Haida = 'hax'; + case Haya = 'hay'; + case Hazaragi = 'haz'; + case Hamba = 'hba'; + case Huba = 'hbb'; + case Heiban = 'hbn'; + case Ancient_Hebrew = 'hbo'; + case Serbo_Croatian = 'hbs'; + case Habu = 'hbu'; + case Andaman_Creole_Hindi = 'hca'; + case Huichol = 'hch'; + case Northern_Haida = 'hdn'; + case Honduras_Sign_Language = 'hds'; + case Hadiyya = 'hdy'; + case Northern_Qiandong_Miao = 'hea'; + case Hebrew = 'heb'; + case Herde = 'hed'; + case Helong = 'heg'; + case Hehe = 'heh'; + case Heiltsuk = 'hei'; + case Hemba = 'hem'; + case Herero = 'her'; + case Hai_om = 'hgm'; + case Haigwai = 'hgw'; + case Hoia_Hoia = 'hhi'; + case Kerak = 'hhr'; + case Hoyahoya = 'hhy'; + case Lamang = 'hia'; + case Hibito = 'hib'; + case Hidatsa = 'hid'; + case Fiji_Hindi = 'hif'; + case Kamwe = 'hig'; + case Pamosu = 'hih'; + case Hinduri = 'hii'; + case Hijuk = 'hij'; + case Seit_Kaitetu = 'hik'; + case Hiligaynon = 'hil'; + case Hindi = 'hin'; + case Tsoa = 'hio'; + case Himarima = 'hir'; + case Hittite = 'hit'; + case Hiw = 'hiw'; + case Hixkaryana = 'hix'; + case Haji = 'hji'; + case Kahe = 'hka'; + case Hunde = 'hke'; + case Khah = 'hkh'; + case Hunjara_Kaina_Ke = 'hkk'; + case Mel_Khaonh = 'hkn'; + case Hong_Kong_Sign_Language = 'hks'; + case Halia = 'hla'; + case Halbi = 'hlb'; + case Halang_Doan = 'hld'; + case Hlersu = 'hle'; + case Matu_Chin = 'hlt'; + case Hieroglyphic_Luwian = 'hlu'; + case Southern_Mashan_Hmong = 'hma'; + case Humburi_Senni_Songhay = 'hmb'; + case Central_Huishui_Hmong = 'hmc'; + case Large_Flowery_Miao = 'hmd'; + case Eastern_Huishui_Hmong = 'hme'; + case Hmong_Don = 'hmf'; + case Southwestern_Guiyang_Hmong = 'hmg'; + case Southwestern_Huishui_Hmong = 'hmh'; + case Northern_Huishui_Hmong = 'hmi'; + case Ge = 'hmj'; + case Maek = 'hmk'; + case Luopohe_Hmong = 'hml'; + case Central_Mashan_Hmong = 'hmm'; + case Hmong = 'hmn'; + case Hiri_Motu = 'hmo'; + case Northern_Mashan_Hmong = 'hmp'; + case Eastern_Qiandong_Miao = 'hmq'; + case Hmar = 'hmr'; + case Southern_Qiandong_Miao = 'hms'; + case Hamtai = 'hmt'; + case Hamap = 'hmu'; + case Hmong_Do = 'hmv'; + case Western_Mashan_Hmong = 'hmw'; + case Southern_Guiyang_Hmong = 'hmy'; + case Hmong_Shua = 'hmz'; + case Mina_Cameroon = 'hna'; + case Southern_Hindko = 'hnd'; + case Chhattisgarhi = 'hne'; + case Hungu = 'hng'; + case Ani = 'hnh'; + case Hani = 'hni'; + case Hmong_Njua = 'hnj'; + case Hanunoo = 'hnn'; + case Northern_Hindko = 'hno'; + case Caribbean_Hindustani = 'hns'; + case Hung = 'hnu'; + case Hoava = 'hoa'; + case Mari_Madang_Province = 'hob'; + case Ho = 'hoc'; + case Holma = 'hod'; + case Horom = 'hoe'; + case Hobyot = 'hoh'; + case Holikachuk = 'hoi'; + case Hadothi = 'hoj'; + case Holu = 'hol'; + case Homa = 'hom'; + case Holoholo = 'hoo'; + case Hopi = 'hop'; + case Horo = 'hor'; + case Ho_Chi_Minh_City_Sign_Language = 'hos'; + case Hote = 'hot'; + case Hovongan = 'hov'; + case Honi = 'how'; + case Holiya = 'hoy'; + case Hozo = 'hoz'; + case Hpon = 'hpo'; + case Hawai_i_Sign_Language_HSL = 'hps'; + case Hrangkhol = 'hra'; + case Niwer_Mil = 'hrc'; + case Hre = 'hre'; + case Haruku = 'hrk'; + case Horned_Miao = 'hrm'; + case Haroi = 'hro'; + case Nhirrpi = 'hrp'; + case Hertevin = 'hrt'; + case Hruso = 'hru'; + case Croatian = 'hrv'; + case Warwar_Feni = 'hrw'; + case Hunsrik = 'hrx'; + case Harzani = 'hrz'; + case Upper_Sorbian = 'hsb'; + case Hungarian_Sign_Language = 'hsh'; + case Hausa_Sign_Language = 'hsl'; + case Xiang_Chinese = 'hsn'; + case Harsusi = 'hss'; + case Hoti = 'hti'; + case Minica_Huitoto = 'hto'; + case Hadza = 'hts'; + case Hitu = 'htu'; + case Middle_Hittite = 'htx'; + case Huambisa = 'hub'; + case Hua = 'huc'; + case Huaulu = 'hud'; + case San_Francisco_Del_Mar_Huave = 'hue'; + case Humene = 'huf'; + case Huachipaeri = 'hug'; + case Huilliche = 'huh'; + case Huli = 'hui'; + case Northern_Guiyang_Hmong = 'huj'; + case Hulung = 'huk'; + case Hula = 'hul'; + case Hungana = 'hum'; + case Hungarian = 'hun'; + case Hu = 'huo'; + case Hupa = 'hup'; + case Tsat = 'huq'; + case Halkomelem = 'hur'; + case Huastec = 'hus'; + case Humla = 'hut'; + case Murui_Huitoto = 'huu'; + case San_Mateo_Del_Mar_Huave = 'huv'; + case Hukumina = 'huw'; + case Nupode_Huitoto = 'hux'; + case Hulaula = 'huy'; + case Hunzib = 'huz'; + case Haitian_Vodoun_Culture_Language = 'hvc'; + case San_Dionisio_Del_Mar_Huave = 'hve'; + case Haveke = 'hvk'; + case Sabu = 'hvn'; + case Santa_Maria_Del_Mar_Huave = 'hvv'; + case Wane = 'hwa'; + case Hawai_i_Creole_English = 'hwc'; + case Hwana = 'hwo'; + case Hya = 'hya'; + case Armenian = 'hye'; + case Western_Armenian = 'hyw'; + case Iaai = 'iai'; + case Iatmul = 'ian'; + case Purari = 'iar'; + case Iban = 'iba'; + case Ibibio = 'ibb'; + case Iwaidja = 'ibd'; + case Akpes = 'ibe'; + case Ibanag = 'ibg'; + case Bih = 'ibh'; + case Ibaloi = 'ibl'; + case Agoi = 'ibm'; + case Ibino = 'ibn'; + case Igbo = 'ibo'; + case Ibuoro = 'ibr'; + case Ibu = 'ibu'; + case Ibani = 'iby'; + case Ede_Ica = 'ica'; + case Etkywan = 'ich'; + case Icelandic_Sign_Language = 'icl'; + case Islander_Creole_English = 'icr'; + case Idakho_Isukha_Tiriki = 'ida'; + case Indo_Portuguese = 'idb'; + case Idon = 'idc'; + case Ede_Idaca = 'idd'; + case Idere = 'ide'; + case Idi = 'idi'; + case Ido = 'ido'; + case Indri = 'idr'; + case Idesa = 'ids'; + case Idate = 'idt'; + case Idoma = 'idu'; + case Amganad_Ifugao = 'ifa'; + case Batad_Ifugao = 'ifb'; + case Ife = 'ife'; + case Ifo = 'iff'; + case Tuwali_Ifugao = 'ifk'; + case Teke_Fuumu = 'ifm'; + case Mayoyao_Ifugao = 'ifu'; + case Keley_I_Kallahan = 'ify'; + case Ebira = 'igb'; + case Igede = 'ige'; + case Igana = 'igg'; + case Igala = 'igl'; + case Kanggape = 'igm'; + case Ignaciano = 'ign'; + case Isebe = 'igo'; + case Interglossa = 'igs'; + case Igwe = 'igw'; + case Iha_Based_Pidgin = 'ihb'; + case Ihievbe = 'ihi'; + case Iha = 'ihp'; + case Bidhawal = 'ihw'; + case Sichuan_Yi = 'iii'; + case Thiin = 'iin'; + case Izon = 'ijc'; + case Biseni = 'ije'; + case Ede_Ije = 'ijj'; + case Kalabari = 'ijn'; + case Southeast_Ijo = 'ijs'; + case Eastern_Canadian_Inuktitut = 'ike'; + case Ikhin_Arokho = 'ikh'; + case Iko = 'iki'; + case Ika = 'ikk'; + case Ikulu = 'ikl'; + case Olulumo_Ikom = 'iko'; + case Ikpeshi = 'ikp'; + case Ikaranggal = 'ikr'; + case Inuit_Sign_Language = 'iks'; + case Inuinnaqtun = 'ikt'; + case Inuktitut = 'iku'; + case Iku_Gora_Ankwa = 'ikv'; + case Ikwere = 'ikw'; + case Ik = 'ikx'; + case Ikizu = 'ikz'; + case Ile_Ape = 'ila'; + case Ila = 'ilb'; + case Interlingue = 'ile'; + case Garig_Ilgar = 'ilg'; + case Ili_Turki = 'ili'; + case Ilongot = 'ilk'; + case Iranun_Malaysia = 'ilm'; + case Iloko = 'ilo'; + case Iranun_Philippines = 'ilp'; + case International_Sign = 'ils'; + case Ili_uun = 'ilu'; + case Ilue = 'ilv'; + case Mala_Malasar = 'ima'; + case Anamgura = 'imi'; + case Miluk = 'iml'; + case Imonda = 'imn'; + case Imbongu = 'imo'; + case Imroing = 'imr'; + case Marsian = 'ims'; + case Imotong = 'imt'; + case Milyan = 'imy'; + case Interlingua_International_Auxiliary_Language_Association = 'ina'; + case Inga = 'inb'; + case Indonesian = 'ind'; + case Degexit_an = 'ing'; + case Ingush = 'inh'; + case Jungle_Inga = 'inj'; + case Indonesian_Sign_Language = 'inl'; + case Minaean = 'inm'; + case Isinai = 'inn'; + case Inoke_Yate = 'ino'; + case Inapari = 'inp'; + case Indian_Sign_Language = 'ins'; + case Intha = 'int'; + case Ineseno = 'inz'; + case Inor = 'ior'; + case Tuma_Irumu = 'iou'; + case Iowa_Oto = 'iow'; + case Ipili = 'ipi'; + case Inupiaq = 'ipk'; + case Ipiko = 'ipo'; + case Iquito = 'iqu'; + case Ikwo = 'iqw'; + case Iresim = 'ire'; + case Irarutu = 'irh'; + case Rigwe = 'iri'; + case Iraqw = 'irk'; + case Irantxe = 'irn'; + case Ir = 'irr'; + case Irula = 'iru'; + case Kamberau = 'irx'; + case Iraya = 'iry'; + case Isabi = 'isa'; + case Isconahua = 'isc'; + case Isnag = 'isd'; + case Italian_Sign_Language = 'ise'; + case Irish_Sign_Language = 'isg'; + case Esan = 'ish'; + case Nkem_Nkum = 'isi'; + case Ishkashimi = 'isk'; + case Icelandic = 'isl'; + case Masimasi = 'ism'; + case Isanzu = 'isn'; + case Isoko = 'iso'; + case Israeli_Sign_Language = 'isr'; + case Istriot = 'ist'; + case Isu_Menchum_Division = 'isu'; + case Italian = 'ita'; + case Binongan_Itneg = 'itb'; + case Southern_Tidung = 'itd'; + case Itene = 'ite'; + case Inlaod_Itneg = 'iti'; + case Judeo_Italian = 'itk'; + case Itelmen = 'itl'; + case Itu_Mbon_Uzo = 'itm'; + case Itonama = 'ito'; + case Iteri = 'itr'; + case Isekiri = 'its'; + case Maeng_Itneg = 'itt'; + case Itawit = 'itv'; + case Ito = 'itw'; + case Itik = 'itx'; + case Moyadan_Itneg = 'ity'; + case Itza = 'itz'; + case Iu_Mien = 'ium'; + case Ibatan = 'ivb'; + case Ivatan = 'ivv'; + case I_Wak = 'iwk'; + case Iwam = 'iwm'; + case Iwur = 'iwo'; + case Sepik_Iwam = 'iws'; + case Ixcatec = 'ixc'; + case Ixil = 'ixl'; + case Iyayu = 'iya'; + case Mesaka = 'iyo'; + case Yaka_Congo = 'iyx'; + case Ingrian = 'izh'; + case Kizamani = 'izm'; + case Izere = 'izr'; + case Izii = 'izz'; + case Jamamadi = 'jaa'; + case Hyam = 'jab'; + case Popti = 'jac'; + case Jahanka = 'jad'; + case Yabem = 'jae'; + case Jara = 'jaf'; + case Jah_Hut = 'jah'; + case Zazao = 'jaj'; + case Jakun = 'jak'; + case Yalahatan = 'jal'; + case Jamaican_Creole_English = 'jam'; + case Jandai = 'jan'; + case Yanyuwa = 'jao'; + case Yaqay = 'jaq'; + case New_Caledonian_Javanese = 'jas'; + case Jakati = 'jat'; + case Yaur = 'jau'; + case Javanese = 'jav'; + case Jambi_Malay = 'jax'; + case Yan_nhangu = 'jay'; + case Jawe = 'jaz'; + case Judeo_Berber = 'jbe'; + case Badjiri = 'jbi'; + case Arandai = 'jbj'; + case Barikewa = 'jbk'; + case Bijim = 'jbm'; + case Nafusi = 'jbn'; + case Lojban = 'jbo'; + case Jofotek_Bromnya = 'jbr'; + case Jabuti = 'jbt'; + case Jukun_Takum = 'jbu'; + case Yawijibaya = 'jbw'; + case Jamaican_Country_Sign_Language = 'jcs'; + case Krymchak = 'jct'; + case Jad = 'jda'; + case Jadgali = 'jdg'; + case Judeo_Tat = 'jdt'; + case Jebero = 'jeb'; + case Jerung = 'jee'; + case Jeh = 'jeh'; + case Yei = 'jei'; + case Jeri_Kuo = 'jek'; + case Yelmek = 'jel'; + case Dza = 'jen'; + case Jere = 'jer'; + case Manem = 'jet'; + case Jonkor_Bourmataguil = 'jeu'; + case Ngbee = 'jgb'; + case Judeo_Georgian = 'jge'; + case Gwak = 'jgk'; + case Ngomba = 'jgo'; + case Jehai = 'jhi'; + case Jhankot_Sign_Language = 'jhs'; + case Jina = 'jia'; + case Jibu = 'jib'; + case Tol = 'jic'; + case Bu_Kaduna_State = 'jid'; + case Jilbe = 'jie'; + case Jingulu = 'jig'; + case sTodsde = 'jih'; + case Jiiddu = 'jii'; + case Jilim = 'jil'; + case Jimi_Cameroon = 'jim'; + case Jiamao = 'jio'; + case Guanyinqiao = 'jiq'; + case Jita = 'jit'; + case Youle_Jinuo = 'jiu'; + case Shuar = 'jiv'; + case Buyuan_Jinuo = 'jiy'; + case Jejueo = 'jje'; + case Bankal = 'jjr'; + case Kaera = 'jka'; + case Mobwa_Karen = 'jkm'; + case Kubo = 'jko'; + case Paku_Karen = 'jkp'; + case Koro_India = 'jkr'; + case Amami_Koniya_Sign_Language = 'jks'; + case Labir = 'jku'; + case Ngile = 'jle'; + case Jamaican_Sign_Language = 'jls'; + case Dima = 'jma'; + case Zumbun = 'jmb'; + case Machame = 'jmc'; + case Yamdena = 'jmd'; + case Jimi_Nigeria = 'jmi'; + case Jumli = 'jml'; + case Makuri_Naga = 'jmn'; + case Kamara = 'jmr'; + case Mashi_Nigeria = 'jms'; + case Mouwase = 'jmw'; + case Western_Juxtlahuaca_Mixtec = 'jmx'; + case Jangshung = 'jna'; + case Jandavra = 'jnd'; + case Yangman = 'jng'; + case Janji = 'jni'; + case Yemsa = 'jnj'; + case Rawat = 'jnl'; + case Jaunsari = 'jns'; + case Joba = 'job'; + case Wojenaka = 'jod'; + case Jogi = 'jog'; + case Jora = 'jor'; + case Jordanian_Sign_Language = 'jos'; + case Jowulu = 'jow'; + case Jewish_Palestinian_Aramaic = 'jpa'; + case Japanese = 'jpn'; + case Judeo_Persian = 'jpr'; + case Jaqaru = 'jqr'; + case Jarai = 'jra'; + case Judeo_Arabic = 'jrb'; + case Jiru = 'jrr'; + case Jakattoe = 'jrt'; + case Japreria = 'jru'; + case Japanese_Sign_Language = 'jsl'; + case Juma = 'jua'; + case Wannu = 'jub'; + case Jurchen = 'juc'; + case Worodougou = 'jud'; + case Hone = 'juh'; + case Ngadjuri = 'jui'; + case Wapan = 'juk'; + case Jirel = 'jul'; + case Jumjum = 'jum'; + case Juang = 'jun'; + case Jiba = 'juo'; + case Hupde = 'jup'; + case Juruna = 'jur'; + case Jumla_Sign_Language = 'jus'; + case Jutish = 'jut'; + case Ju = 'juu'; + case Wapha = 'juw'; + case Juray = 'juy'; + case Javindo = 'jvd'; + case Caribbean_Javanese = 'jvn'; + case Jwira_Pepesa = 'jwi'; + case Jiarong = 'jya'; + case Judeo_Yemeni_Arabic = 'jye'; + case Jaya = 'jyy'; + case Kara_Kalpak = 'kaa'; + case Kabyle = 'kab'; + case Kachin = 'kac'; + case Adara = 'kad'; + case Ketangalan = 'kae'; + case Katso = 'kaf'; + case Kajaman = 'kag'; + case Kara_Central_African_Republic = 'kah'; + case Karekare = 'kai'; + case Jju = 'kaj'; + case Kalanguya = 'kak'; + case Kalaallisut = 'kal'; + case Kamba_Kenya = 'kam'; + case Kannada = 'kan'; + case Xaasongaxango = 'kao'; + case Bezhta = 'kap'; + case Capanahua = 'kaq'; + case Kashmiri = 'kas'; + case Georgian = 'kat'; + case Kanuri = 'kau'; + case Katukina = 'kav'; + case Kawi = 'kaw'; + case Kao = 'kax'; + case Kamayura = 'kay'; + case Kazakh = 'kaz'; + case Kalarko = 'kba'; + case Kaxuiana = 'kbb'; + case Kadiweu = 'kbc'; + case Kabardian = 'kbd'; + case Kanju = 'kbe'; + case Khamba = 'kbg'; + case Camsa = 'kbh'; + case Kaptiau = 'kbi'; + case Kari = 'kbj'; + case Grass_Koiari = 'kbk'; + case Kanembu = 'kbl'; + case Iwal = 'kbm'; + case Kare_Central_African_Republic = 'kbn'; + case Keliko = 'kbo'; + case Kabiye = 'kbp'; + case Kamano = 'kbq'; + case Kafa = 'kbr'; + case Kande = 'kbs'; + case Abadi = 'kbt'; + case Kabutra = 'kbu'; + case Dera_Indonesia = 'kbv'; + case Kaiep = 'kbw'; + case Ap_Ma = 'kbx'; + case Manga_Kanuri = 'kby'; + case Duhwa = 'kbz'; + case Khanty = 'kca'; + case Kawacha = 'kcb'; + case Lubila = 'kcc'; + case Ngkalmpw_Kanum = 'kcd'; + case Kaivi = 'kce'; + case Ukaan = 'kcf'; + case Tyap = 'kcg'; + case Vono = 'kch'; + case Kamantan = 'kci'; + case Kobiana = 'kcj'; + case Kalanga = 'kck'; + case Kela_Papua_New_Guinea = 'kcl'; + case Gula_Central_African_Republic = 'kcm'; + case Nubi = 'kcn'; + case Kinalakna = 'kco'; + case Kanga = 'kcp'; + case Kamo = 'kcq'; + case Katla = 'kcr'; + case Koenoem = 'kcs'; + case Kaian = 'kct'; + case Kami_Tanzania = 'kcu'; + case Kete = 'kcv'; + case Kabwari = 'kcw'; + case Kachama_Ganjule = 'kcx'; + case Korandje = 'kcy'; + case Konongo = 'kcz'; + case Worimi = 'kda'; + case Kutu = 'kdc'; + case Yankunytjatjara = 'kdd'; + case Makonde = 'kde'; + case Mamusi = 'kdf'; + case Seba = 'kdg'; + case Tem = 'kdh'; + case Kumam = 'kdi'; + case Karamojong = 'kdj'; + case Numee = 'kdk'; + case Tsikimba = 'kdl'; + case Kagoma = 'kdm'; + case Kunda = 'kdn'; + case Kaningdon_Nindem = 'kdp'; + case Koch = 'kdq'; + case Karaim = 'kdr'; + case Kuy = 'kdt'; + case Kadaru = 'kdu'; + case Koneraw = 'kdw'; + case Kam = 'kdx'; + case Keder = 'kdy'; + case Kwaja = 'kdz'; + case Kabuverdianu = 'kea'; + case Kele = 'keb'; + case Keiga = 'kec'; + case Kerewe = 'ked'; + case Eastern_Keres = 'kee'; + case Kpessi = 'kef'; + case Tese = 'keg'; + case Keak = 'keh'; + case Kei = 'kei'; + case Kadar = 'kej'; + case Kekchi = 'kek'; + case Kela_Democratic_Republic_of_Congo = 'kel'; + case Kemak = 'kem'; + case Kenyang = 'ken'; + case Kakwa = 'keo'; + case Kaikadi = 'kep'; + case Kamar = 'keq'; + case Kera = 'ker'; + case Kugbo = 'kes'; + case Ket = 'ket'; + case Akebu = 'keu'; + case Kanikkaran = 'kev'; + case West_Kewa = 'kew'; + case Kukna = 'kex'; + case Kupia = 'key'; + case Kukele = 'kez'; + case Kodava = 'kfa'; + case Northwestern_Kolami = 'kfb'; + case Konda_Dora = 'kfc'; + case Korra_Koraga = 'kfd'; + case Kota_India = 'kfe'; + case Koya = 'kff'; + case Kudiya = 'kfg'; + case Kurichiya = 'kfh'; + case Kannada_Kurumba = 'kfi'; + case Kemiehua = 'kfj'; + case Kinnauri = 'kfk'; + case Kung = 'kfl'; + case Khunsari = 'kfm'; + case Kuk = 'kfn'; + case Koro_Cote_d_Ivoire = 'kfo'; + case Korwa = 'kfp'; + case Korku = 'kfq'; + case Kachhi = 'kfr'; + case Bilaspuri = 'kfs'; + case Kanjari = 'kft'; + case Katkari = 'kfu'; + case Kurmukar = 'kfv'; + case Kharam_Naga = 'kfw'; + case Kullu_Pahari = 'kfx'; + case Kumaoni = 'kfy'; + case Koromfe = 'kfz'; + case Koyaga = 'kga'; + case Kawe = 'kgb'; + case Komering = 'kge'; + case Kube = 'kgf'; + case Kusunda = 'kgg'; + case Selangor_Sign_Language = 'kgi'; + case Gamale_Kham = 'kgj'; + case Kaiwa = 'kgk'; + case Kunggari = 'kgl'; + case Karingani = 'kgn'; + case Krongo = 'kgo'; + case Kaingang = 'kgp'; + case Kamoro = 'kgq'; + case Abun = 'kgr'; + case Kumbainggar = 'kgs'; + case Somyev = 'kgt'; + case Kobol = 'kgu'; + case Karas = 'kgv'; + case Karon_Dori = 'kgw'; + case Kamaru = 'kgx'; + case Kyerung = 'kgy'; + case Khasi = 'kha'; + case Lu = 'khb'; + case Tukang_Besi_North = 'khc'; + case Badi_Kanum = 'khd'; + case Korowai = 'khe'; + case Khuen = 'khf'; + case Khams_Tibetan = 'khg'; + case Kehu = 'khh'; + case Kuturmi = 'khj'; + case Halh_Mongolian = 'khk'; + case Lusi = 'khl'; + case Khmer = 'khm'; + case Khandesi = 'khn'; + case Khotanese = 'kho'; + case Kapori = 'khp'; + case Koyra_Chiini_Songhay = 'khq'; + case Kharia = 'khr'; + case Kasua = 'khs'; + case Khamti = 'kht'; + case Nkhumbi = 'khu'; + case Khvarshi = 'khv'; + case Khowar = 'khw'; + case Kanu = 'khx'; + case Kele_Democratic_Republic_of_Congo = 'khy'; + case Keapara = 'khz'; + case Kim = 'kia'; + case Koalib = 'kib'; + case Kickapoo = 'kic'; + case Koshin = 'kid'; + case Kibet = 'kie'; + case Eastern_Parbate_Kham = 'kif'; + case Kimaama = 'kig'; + case Kilmeri = 'kih'; + case Kitsai = 'kii'; + case Kilivila = 'kij'; + case Kikuyu = 'kik'; + case Kariya = 'kil'; + case Karagas = 'kim'; + case Kinyarwanda = 'kin'; + case Kiowa = 'kio'; + case Sheshi_Kham = 'kip'; + case Kosadle = 'kiq'; + case Kirghiz = 'kir'; + case Kis = 'kis'; + case Agob = 'kit'; + case Kirmanjki_individual_language = 'kiu'; + case Kimbu = 'kiv'; + case Northeast_Kiwai = 'kiw'; + case Khiamniungan_Naga = 'kix'; + case Kirikiri = 'kiy'; + case Kisi = 'kiz'; + case Mlap = 'kja'; + case Q_anjob_al = 'kjb'; + case Coastal_Konjo = 'kjc'; + case Southern_Kiwai = 'kjd'; + case Kisar = 'kje'; + case Khmu = 'kjg'; + case Khakas = 'kjh'; + case Zabana = 'kji'; + case Khinalugh = 'kjj'; + case Highland_Konjo = 'kjk'; + case Western_Parbate_Kham = 'kjl'; + case Khang = 'kjm'; + case Kunjen = 'kjn'; + case Harijan_Kinnauri = 'kjo'; + case Pwo_Eastern_Karen = 'kjp'; + case Western_Keres = 'kjq'; + case Kurudu = 'kjr'; + case East_Kewa = 'kjs'; + case Phrae_Pwo_Karen = 'kjt'; + case Kashaya = 'kju'; + case Kaikavian_Literary_Language = 'kjv'; + case Ramopa = 'kjx'; + case Erave = 'kjy'; + case Bumthangkha = 'kjz'; + case Kakanda = 'kka'; + case Kwerisa = 'kkb'; + case Odoodee = 'kkc'; + case Kinuku = 'kkd'; + case Kakabe = 'kke'; + case Kalaktang_Monpa = 'kkf'; + case Mabaka_Valley_Kalinga = 'kkg'; + case Khun = 'kkh'; + case Kagulu = 'kki'; + case Kako = 'kkj'; + case Kokota = 'kkk'; + case Kosarek_Yale = 'kkl'; + case Kiong = 'kkm'; + case Kon_Keu = 'kkn'; + case Karko = 'kko'; + case Gugubera = 'kkp'; + case Kaeku = 'kkq'; + case Kir_Balar = 'kkr'; + case Giiwo = 'kks'; + case Koi = 'kkt'; + case Tumi = 'kku'; + case Kangean = 'kkv'; + case Teke_Kukuya = 'kkw'; + case Kohin = 'kkx'; + case Guugu_Yimidhirr = 'kky'; + case Kaska = 'kkz'; + case Klamath_Modoc = 'kla'; + case Kiliwa = 'klb'; + case Kolbila = 'klc'; + case Gamilaraay = 'kld'; + case Kulung_Nepal = 'kle'; + case Kendeje = 'klf'; + case Tagakaulo = 'klg'; + case Weliki = 'klh'; + case Kalumpang = 'kli'; + case Khalaj = 'klj'; + case Kono_Nigeria = 'klk'; + case Kagan_Kalagan = 'kll'; + case Migum = 'klm'; + case Kalenjin = 'kln'; + case Kapya = 'klo'; + case Kamasa = 'klp'; + case Rumu = 'klq'; + case Khaling = 'klr'; + case Kalasha = 'kls'; + case Nukna = 'klt'; + case Klao = 'klu'; + case Maskelynes = 'klv'; + case Tado = 'klw'; + case Koluwawa = 'klx'; + case Kalao = 'kly'; + case Kabola = 'klz'; + case Konni = 'kma'; + case Kimbundu = 'kmb'; + case Southern_Dong = 'kmc'; + case Majukayang_Kalinga = 'kmd'; + case Bakole = 'kme'; + case Kare_Papua_New_Guinea = 'kmf'; + case Kate = 'kmg'; + case Kalam = 'kmh'; + case Kami_Nigeria = 'kmi'; + case Kumarbhag_Paharia = 'kmj'; + case Limos_Kalinga = 'kmk'; + case Tanudan_Kalinga = 'kml'; + case Kom_India = 'kmm'; + case Awtuw = 'kmn'; + case Kwoma = 'kmo'; + case Gimme = 'kmp'; + case Kwama = 'kmq'; + case Northern_Kurdish = 'kmr'; + case Kamasau = 'kms'; + case Kemtuik = 'kmt'; + case Kanite = 'kmu'; + case Karipuna_Creole_French = 'kmv'; + case Komo_Democratic_Republic_of_Congo = 'kmw'; + case Waboda = 'kmx'; + case Koma = 'kmy'; + case Khorasani_Turkish = 'kmz'; + case Dera_Nigeria = 'kna'; + case Lubuagan_Kalinga = 'knb'; + case Central_Kanuri = 'knc'; + case Konda = 'knd'; + case Kankanaey = 'kne'; + case Mankanya = 'knf'; + case Koongo = 'kng'; + case Kanufi = 'kni'; + case Western_Kanjobal = 'knj'; + case Kuranko = 'knk'; + case Keninjal = 'knl'; + case Kanamari = 'knm'; + case Konkani_individual_language = 'knn'; + case Kono_Sierra_Leone = 'kno'; + case Kwanja = 'knp'; + case Kintaq = 'knq'; + case Kaningra = 'knr'; + case Kensiu = 'kns'; + case Panoan_Katukina = 'knt'; + case Kono_Guinea = 'knu'; + case Tabo = 'knv'; + case Kung_Ekoka = 'knw'; + case Kendayan = 'knx'; + case Kanyok = 'kny'; + case Kalamse = 'knz'; + case Konomala = 'koa'; + case Kpati = 'koc'; + case Kodi = 'kod'; + case Kacipo_Bale_Suri = 'koe'; + case Kubi = 'kof'; + case Cogui = 'kog'; + case Koyo = 'koh'; + case Komi_Permyak = 'koi'; + case Konkani_macrolanguage = 'kok'; + case Kol_Papua_New_Guinea = 'kol'; + case Komi = 'kom'; + case Kongo = 'kon'; + case Konzo = 'koo'; + case Waube = 'kop'; + case Kota_Gabon = 'koq'; + case Korean = 'kor'; + case Kosraean = 'kos'; + case Lagwan = 'kot'; + case Koke = 'kou'; + case Kudu_Camo = 'kov'; + case Kugama = 'kow'; + case Koyukon = 'koy'; + case Korak = 'koz'; + case Kutto = 'kpa'; + case Mullu_Kurumba = 'kpb'; + case Curripaco = 'kpc'; + case Koba = 'kpd'; + case Kpelle = 'kpe'; + case Komba = 'kpf'; + case Kapingamarangi = 'kpg'; + case Kplang = 'kph'; + case Kofei = 'kpi'; + case Karaja = 'kpj'; + case Kpan = 'kpk'; + case Kpala = 'kpl'; + case Koho = 'kpm'; + case Kepkiriwat = 'kpn'; + case Ikposo = 'kpo'; + case Korupun_Sela = 'kpq'; + case Korafe_Yegha = 'kpr'; + case Tehit = 'kps'; + case Karata = 'kpt'; + case Kafoa = 'kpu'; + case Komi_Zyrian = 'kpv'; + case Kobon = 'kpw'; + case Mountain_Koiali = 'kpx'; + case Koryak = 'kpy'; + case Kupsabiny = 'kpz'; + case Mum = 'kqa'; + case Kovai = 'kqb'; + case Doromu_Koki = 'kqc'; + case Koy_Sanjaq_Surat = 'kqd'; + case Kalagan = 'kqe'; + case Kakabai = 'kqf'; + case Khe = 'kqg'; + case Kisankasa = 'kqh'; + case Koitabu = 'kqi'; + case Koromira = 'kqj'; + case Kotafon_Gbe = 'kqk'; + case Kyenele = 'kql'; + case Khisa = 'kqm'; + case Kaonde = 'kqn'; + case Eastern_Krahn = 'kqo'; + case Kimre = 'kqp'; + case Krenak = 'kqq'; + case Kimaragang = 'kqr'; + case Northern_Kissi = 'kqs'; + case Klias_River_Kadazan = 'kqt'; + case Seroa = 'kqu'; + case Okolod = 'kqv'; + case Kandas = 'kqw'; + case Mser = 'kqx'; + case Koorete = 'kqy'; + case Korana = 'kqz'; + case Kumhali = 'kra'; + case Karkin = 'krb'; + case Karachay_Balkar = 'krc'; + case Kairui_Midiki = 'krd'; + case Panara = 'kre'; + case Koro_Vanuatu = 'krf'; + case Kurama = 'krh'; + case Krio = 'kri'; + case Kinaray_A = 'krj'; + case Kerek = 'krk'; + case Karelian = 'krl'; + case Sapo = 'krn'; + case Durop = 'krp'; + case Krung = 'krr'; + case Gbaya_Sudan = 'krs'; + case Tumari_Kanuri = 'krt'; + case Kurukh = 'kru'; + case Kavet = 'krv'; + case Western_Krahn = 'krw'; + case Karon = 'krx'; + case Kryts = 'kry'; + case Sota_Kanum = 'krz'; + case Shambala = 'ksb'; + case Southern_Kalinga = 'ksc'; + case Kuanua = 'ksd'; + case Kuni = 'kse'; + case Bafia = 'ksf'; + case Kusaghe = 'ksg'; + case Kolsch = 'ksh'; + case Krisa = 'ksi'; + case Uare = 'ksj'; + case Kansa = 'ksk'; + case Kumalu = 'ksl'; + case Kumba = 'ksm'; + case Kasiguranin = 'ksn'; + case Kofa = 'kso'; + case Kaba = 'ksp'; + case Kwaami = 'ksq'; + case Borong = 'ksr'; + case Southern_Kisi = 'kss'; + case Winye = 'kst'; + case Khamyang = 'ksu'; + case Kusu = 'ksv'; + case S_gaw_Karen = 'ksw'; + case Kedang = 'ksx'; + case Kharia_Thar = 'ksy'; + case Kodaku = 'ksz'; + case Katua = 'kta'; + case Kambaata = 'ktb'; + case Kholok = 'ktc'; + case Kokata = 'ktd'; + case Nubri = 'kte'; + case Kwami = 'ktf'; + case Kalkutung = 'ktg'; + case Karanga = 'kth'; + case North_Muyu = 'kti'; + case Plapo_Krumen = 'ktj'; + case Kaniet = 'ktk'; + case Koroshi = 'ktl'; + case Kurti = 'ktm'; + case Karitiana = 'ktn'; + case Kuot = 'kto'; + case Kaduo = 'ktp'; + case Katabaga = 'ktq'; + case South_Muyu = 'kts'; + case Ketum = 'ktt'; + case Kituba_Democratic_Republic_of_Congo = 'ktu'; + case Eastern_Katu = 'ktv'; + case Kato = 'ktw'; + case Kaxarari = 'ktx'; + case Kango_Bas_Uele_District = 'kty'; + case Ju_hoan = 'ktz'; + case Kuanyama = 'kua'; + case Kutep = 'kub'; + case Kwinsu = 'kuc'; + case Auhelawa = 'kud'; + case Kuman_Papua_New_Guinea = 'kue'; + case Western_Katu = 'kuf'; + case Kupa = 'kug'; + case Kushi = 'kuh'; + case Kuikuro_Kalapalo = 'kui'; + case Kuria = 'kuj'; + case Kepo = 'kuk'; + case Kulere = 'kul'; + case Kumyk = 'kum'; + case Kunama = 'kun'; + case Kumukio = 'kuo'; + case Kunimaipa = 'kup'; + case Karipuna = 'kuq'; + case Kurdish = 'kur'; + case Kusaal = 'kus'; + case Kutenai = 'kut'; + case Upper_Kuskokwim = 'kuu'; + case Kur = 'kuv'; + case Kpagua = 'kuw'; + case Kukatja = 'kux'; + case Kuuku_Ya_u = 'kuy'; + case Kunza = 'kuz'; + case Bagvalal = 'kva'; + case Kubu = 'kvb'; + case Kove = 'kvc'; + case Kui_Indonesia = 'kvd'; + case Kalabakan = 'kve'; + case Kabalai = 'kvf'; + case Kuni_Boazi = 'kvg'; + case Komodo = 'kvh'; + case Kwang = 'kvi'; + case Psikye = 'kvj'; + case Korean_Sign_Language = 'kvk'; + case Kayaw = 'kvl'; + case Kendem = 'kvm'; + case Border_Kuna = 'kvn'; + case Dobel = 'kvo'; + case Kompane = 'kvp'; + case Geba_Karen = 'kvq'; + case Kerinci = 'kvr'; + case Lahta_Karen = 'kvt'; + case Yinbaw_Karen = 'kvu'; + case Kola = 'kvv'; + case Wersing = 'kvw'; + case Parkari_Koli = 'kvx'; + case Yintale_Karen = 'kvy'; + case Tsakwambo = 'kvz'; + case Daw = 'kwa'; + case Kwa_2 = 'kwb'; + case Likwala = 'kwc'; + case Kwaio = 'kwd'; + case Kwerba = 'kwe'; + case Kwara_ae = 'kwf'; + case Sara_Kaba_Deme = 'kwg'; + case Kowiai = 'kwh'; + case Awa_Cuaiquer = 'kwi'; + case Kwanga = 'kwj'; + case Kwakiutl = 'kwk'; + case Kofyar = 'kwl'; + case Kwambi = 'kwm'; + case Kwangali = 'kwn'; + case Kwomtari = 'kwo'; + case Kodia = 'kwp'; + case Kwer = 'kwr'; + case Kwese = 'kws'; + case Kwesten = 'kwt'; + case Kwakum = 'kwu'; + case Sara_Kaba_Naa = 'kwv'; + case Kwinti = 'kww'; + case Khirwar = 'kwx'; + case San_Salvador_Kongo = 'kwy'; + case Kwadi = 'kwz'; + case Kairiru = 'kxa'; + case Krobu = 'kxb'; + case Konso = 'kxc'; + case Brunei = 'kxd'; + case Manumanaw_Karen = 'kxf'; + case Karo_Ethiopia = 'kxh'; + case Keningau_Murut = 'kxi'; + case Kulfa = 'kxj'; + case Zayein_Karen = 'kxk'; + case Northern_Khmer = 'kxm'; + case Kanowit_Tanjong_Melanau = 'kxn'; + case Kanoe = 'kxo'; + case Wadiyara_Koli = 'kxp'; + case Smarky_Kanum = 'kxq'; + case Koro_Papua_New_Guinea = 'kxr'; + case Kangjia = 'kxs'; + case Koiwat = 'kxt'; + case Kuvi = 'kxv'; + case Konai = 'kxw'; + case Likuba = 'kxx'; + case Kayong = 'kxy'; + case Kerewo = 'kxz'; + case Kwaya = 'kya'; + case Butbut_Kalinga = 'kyb'; + case Kyaka = 'kyc'; + case Karey = 'kyd'; + case Krache = 'kye'; + case Kouya = 'kyf'; + case Keyagana = 'kyg'; + case Karok = 'kyh'; + case Kiput = 'kyi'; + case Karao = 'kyj'; + case Kamayo = 'kyk'; + case Kalapuya = 'kyl'; + case Kpatili = 'kym'; + case Northern_Binukidnon = 'kyn'; + case Kelon = 'kyo'; + case Kang = 'kyp'; + case Kenga = 'kyq'; + case Kuruaya = 'kyr'; + case Baram_Kayan = 'kys'; + case Kayagar = 'kyt'; + case Western_Kayah = 'kyu'; + case Kayort = 'kyv'; + case Kudmali = 'kyw'; + case Rapoisi = 'kyx'; + case Kambaira = 'kyy'; + case Kayabi = 'kyz'; + case Western_Karaboro = 'kza'; + case Kaibobo = 'kzb'; + case Bondoukou_Kulango = 'kzc'; + case Kadai = 'kzd'; + case Kosena = 'kze'; + case Da_a_Kaili = 'kzf'; + case Kikai = 'kzg'; + case Kelabit = 'kzi'; + case Kazukuru = 'kzk'; + case Kayeli = 'kzl'; + case Kais = 'kzm'; + case Kokola = 'kzn'; + case Kaningi = 'kzo'; + case Kaidipang = 'kzp'; + case Kaike = 'kzq'; + case Karang = 'kzr'; + case Sugut_Dusun = 'kzs'; + case Kayupulau = 'kzu'; + case Komyandaret = 'kzv'; + case Kariri_Xoco = 'kzw'; + case Kamarian = 'kzx'; + case Kango_Tshopo_District = 'kzy'; + case Kalabra = 'kzz'; + case Southern_Subanen = 'laa'; + case Linear_A = 'lab'; + case Lacandon = 'lac'; + case Ladino = 'lad'; + case Pattani = 'lae'; + case Lafofa = 'laf'; + case Rangi = 'lag'; + case Lahnda = 'lah'; + case Lambya = 'lai'; + case Lango_Uganda = 'laj'; + case Lalia = 'lal'; + case Lamba = 'lam'; + case Laru = 'lan'; + case Lao = 'lao'; + case Laka_Chad = 'lap'; + case Qabiao = 'laq'; + case Larteh = 'lar'; + case Lama_Togo = 'las'; + case Latin = 'lat'; + case Laba = 'lau'; + case Latvian = 'lav'; + case Lauje = 'law'; + case Tiwa = 'lax'; + case Lama_Bai = 'lay'; + case Aribwatsa = 'laz'; + case Label = 'lbb'; + case Lakkia = 'lbc'; + case Lak = 'lbe'; + case Tinani = 'lbf'; + case Laopang = 'lbg'; + case La_bi = 'lbi'; + case Ladakhi = 'lbj'; + case Central_Bontok = 'lbk'; + case Libon_Bikol = 'lbl'; + case Lodhi = 'lbm'; + case Rmeet = 'lbn'; + case Laven = 'lbo'; + case Wampar = 'lbq'; + case Lohorung = 'lbr'; + case Libyan_Sign_Language = 'lbs'; + case Lachi = 'lbt'; + case Labu = 'lbu'; + case Lavatbura_Lamusong = 'lbv'; + case Tolaki = 'lbw'; + case Lawangan = 'lbx'; + case Lamalama = 'lby'; + case Lardil = 'lbz'; + case Legenyem = 'lcc'; + case Lola = 'lcd'; + case Loncong = 'lce'; + case Lubu = 'lcf'; + case Luchazi = 'lch'; + case Lisela = 'lcl'; + case Tungag = 'lcm'; + case Western_Lawa = 'lcp'; + case Luhu = 'lcq'; + case Lisabata_Nuniali = 'lcs'; + case Kla_Dan = 'lda'; + case Du_ya = 'ldb'; + case Luri = 'ldd'; + case Lenyima = 'ldg'; + case Lamja_Dengsa_Tola = 'ldh'; + case Laari = 'ldi'; + case Lemoro = 'ldj'; + case Leelau = 'ldk'; + case Kaan = 'ldl'; + case Landoma = 'ldm'; + case Laadan = 'ldn'; + case Loo = 'ldo'; + case Tso = 'ldp'; + case Lufu = 'ldq'; + case Lega_Shabunda = 'lea'; + case Lala_Bisa = 'leb'; + case Leco = 'lec'; + case Lendu = 'led'; + case Lyele = 'lee'; + case Lelemi = 'lef'; + case Lenje = 'leh'; + case Lemio = 'lei'; + case Lengola = 'lej'; + case Leipon = 'lek'; + case Lele_Democratic_Republic_of_Congo = 'lel'; + case Nomaande = 'lem'; + case Lenca = 'len'; + case Leti_Cameroon = 'leo'; + case Lepcha = 'lep'; + case Lembena = 'leq'; + case Lenkau = 'ler'; + case Lese = 'les'; + case Lesing_Gelimi = 'let'; + case Kara_Papua_New_Guinea = 'leu'; + case Lamma = 'lev'; + case Ledo_Kaili = 'lew'; + case Luang = 'lex'; + case Lemolang = 'ley'; + case Lezghian = 'lez'; + case Lefa = 'lfa'; + case Lingua_Franca_Nova = 'lfn'; + case Lungga = 'lga'; + case Laghu = 'lgb'; + case Lugbara = 'lgg'; + case Laghuu = 'lgh'; + case Lengilu = 'lgi'; + case Lingarak = 'lgk'; + case Wala = 'lgl'; + case Lega_Mwenga = 'lgm'; + case T_apo = 'lgn'; + case Lango_South_Sudan = 'lgo'; + case Logba = 'lgq'; + case Lengo = 'lgr'; + case Guinea_Bissau_Sign_Language = 'lgs'; + case Pahi = 'lgt'; + case Longgu = 'lgu'; + case Ligenza = 'lgz'; + case Laha_Viet_Nam = 'lha'; + case Laha_Indonesia = 'lhh'; + case Lahu_Shi = 'lhi'; + case Lahul_Lohar = 'lhl'; + case Lhomi = 'lhm'; + case Lahanan = 'lhn'; + case Lhokpu = 'lhp'; + case Mlahso = 'lhs'; + case Lo_Toga = 'lht'; + case Lahu = 'lhu'; + case West_Central_Limba = 'lia'; + case Likum = 'lib'; + case Hlai = 'lic'; + case Nyindrou = 'lid'; + case Likila = 'lie'; + case Limbu = 'lif'; + case Ligbi = 'lig'; + case Lihir = 'lih'; + case Ligurian = 'lij'; + case Lika = 'lik'; + case Lillooet = 'lil'; + case Limburgan = 'lim'; + case Lingala = 'lin'; + case Liki = 'lio'; + case Sekpele = 'lip'; + case Libido = 'liq'; + case Liberian_English = 'lir'; + case Lisu = 'lis'; + case Lithuanian = 'lit'; + case Logorik = 'liu'; + case Liv = 'liv'; + case Col = 'liw'; + case Liabuku = 'lix'; + case Banda_Bambari = 'liy'; + case Libinza = 'liz'; + case Golpa = 'lja'; + case Rampi = 'lje'; + case Laiyolo = 'lji'; + case Li_o = 'ljl'; + case Lampung_Api = 'ljp'; + case Yirandali = 'ljw'; + case Yuru = 'ljx'; + case Lakalei = 'lka'; + case Kabras = 'lkb'; + case Kucong = 'lkc'; + case Lakonde = 'lkd'; + case Kenyi = 'lke'; + case Lakha = 'lkh'; + case Laki = 'lki'; + case Remun = 'lkj'; + case Laeko_Libuat = 'lkl'; + case Kalaamaya = 'lkm'; + case Lakon = 'lkn'; + case Khayo = 'lko'; + case Pari = 'lkr'; + case Kisa = 'lks'; + case Lakota = 'lkt'; + case Kungkari = 'lku'; + case Lokoya = 'lky'; + case Lala_Roba = 'lla'; + case Lolo = 'llb'; + case Lele_Guinea = 'llc'; + case Ladin = 'lld'; + case Lele_Papua_New_Guinea = 'lle'; + case Hermit = 'llf'; + case Lole = 'llg'; + case Lamu = 'llh'; + case Teke_Laali = 'lli'; + case Ladji_Ladji = 'llj'; + case Lelak = 'llk'; + case Lilau = 'lll'; + case Lasalimu = 'llm'; + case Lele_Chad = 'lln'; + case North_Efate = 'llp'; + case Lolak = 'llq'; + case Lithuanian_Sign_Language = 'lls'; + case Lau = 'llu'; + case Lauan = 'llx'; + case East_Limba = 'lma'; + case Merei = 'lmb'; + case Limilngan = 'lmc'; + case Lumun = 'lmd'; + case Peve = 'lme'; + case South_Lembata = 'lmf'; + case Lamogai = 'lmg'; + case Lambichhong = 'lmh'; + case Lombi = 'lmi'; + case West_Lembata = 'lmj'; + case Lamkang = 'lmk'; + case Hano = 'lml'; + case Lambadi = 'lmn'; + case Lombard = 'lmo'; + case Limbum = 'lmp'; + case Lamatuka = 'lmq'; + case Lamalera = 'lmr'; + case Lamenu = 'lmu'; + case Lomaiviti = 'lmv'; + case Lake_Miwok = 'lmw'; + case Laimbue = 'lmx'; + case Lamboya = 'lmy'; + case Langbashe = 'lna'; + case Mbalanhu = 'lnb'; + case Lundayeh = 'lnd'; + case Langobardic = 'lng'; + case Lanoh = 'lnh'; + case Daantanai = 'lni'; + case Leningitij = 'lnj'; + case South_Central_Banda = 'lnl'; + case Langam = 'lnm'; + case Lorediakarkar = 'lnn'; + case Lamnso = 'lns'; + case Longuda = 'lnu'; + case Lanima = 'lnw'; + case Lonzo = 'lnz'; + case Loloda = 'loa'; + case Lobi = 'lob'; + case Inonhan = 'loc'; + case Saluan = 'loe'; + case Logol = 'lof'; + case Logo = 'log'; + case Laarim = 'loh'; + case Loma_Cote_d_Ivoire = 'loi'; + case Lou = 'loj'; + case Loko = 'lok'; + case Mongo = 'lol'; + case Loma_Liberia = 'lom'; + case Malawi_Lomwe = 'lon'; + case Lombo = 'loo'; + case Lopa = 'lop'; + case Lobala = 'loq'; + case Teen = 'lor'; + case Loniu = 'los'; + case Otuho = 'lot'; + case Louisiana_Creole = 'lou'; + case Lopi = 'lov'; + case Tampias_Lobu = 'low'; + case Loun = 'lox'; + case Loke = 'loy'; + case Lozi = 'loz'; + case Lelepa = 'lpa'; + case Lepki = 'lpe'; + case Long_Phuri_Naga = 'lpn'; + case Lipo = 'lpo'; + case Lopit = 'lpx'; + case Logir = 'lqr'; + case Rara_Bakati = 'lra'; + case Northern_Luri = 'lrc'; + case Laurentian = 'lre'; + case Laragia = 'lrg'; + case Marachi = 'lri'; + case Loarki = 'lrk'; + case Lari = 'lrl'; + case Marama = 'lrm'; + case Lorang = 'lrn'; + case Laro = 'lro'; + case Southern_Yamphu = 'lrr'; + case Larantuka_Malay = 'lrt'; + case Larevat = 'lrv'; + case Lemerig = 'lrz'; + case Lasgerdi = 'lsa'; + case Burundian_Sign_Language = 'lsb'; + case Albarradas_Sign_Language = 'lsc'; + case Lishana_Deni = 'lsd'; + case Lusengo = 'lse'; + case Lish = 'lsh'; + case Lashi = 'lsi'; + case Latvian_Sign_Language = 'lsl'; + case Saamia = 'lsm'; + case Tibetan_Sign_Language = 'lsn'; + case Laos_Sign_Language = 'lso'; + case Panamanian_Sign_Language = 'lsp'; + case Aruop = 'lsr'; + case Lasi = 'lss'; + case Trinidad_and_Tobago_Sign_Language = 'lst'; + case Sivia_Sign_Language = 'lsv'; + case Seychelles_Sign_Language = 'lsw'; + case Mauritian_Sign_Language = 'lsy'; + case Late_Middle_Chinese = 'ltc'; + case Latgalian = 'ltg'; + case Thur = 'lth'; + case Leti_Indonesia = 'lti'; + case Latunde = 'ltn'; + case Tsotso = 'lto'; + case Tachoni = 'lts'; + case Latu = 'ltu'; + case Luxembourgish = 'ltz'; + case Luba_Lulua = 'lua'; + case Luba_Katanga = 'lub'; + case Aringa = 'luc'; + case Ludian = 'lud'; + case Luvale = 'lue'; + case Laua = 'luf'; + case Ganda = 'lug'; + case Luiseno = 'lui'; + case Luna = 'luj'; + case Lunanakha = 'luk'; + case Olu_bo = 'lul'; + case Luimbi = 'lum'; + case Lunda = 'lun'; + case Luo_Kenya_and_Tanzania = 'luo'; + case Lumbu = 'lup'; + case Lucumi = 'luq'; + case Laura = 'lur'; + case Lushai = 'lus'; + case Lushootseed = 'lut'; + case Lumba_Yakkha = 'luu'; + case Luwati = 'luv'; + case Luo_Cameroon = 'luw'; + case Luyia = 'luy'; + case Southern_Luri = 'luz'; + case Maku_a = 'lva'; + case Lavi = 'lvi'; + case Lavukaleve = 'lvk'; + case Lwel = 'lvl'; + case Standard_Latvian = 'lvs'; + case Levuka = 'lvu'; + case Lwalu = 'lwa'; + case Lewo_Eleng = 'lwe'; + case Wanga = 'lwg'; + case White_Lachi = 'lwh'; + case Eastern_Lawa = 'lwl'; + case Laomian = 'lwm'; + case Luwo = 'lwo'; + case Malawian_Sign_Language = 'lws'; + case Lewotobi = 'lwt'; + case Lawu = 'lwu'; + case Lewo = 'lww'; + case Lakurumau = 'lxm'; + case Layakha = 'lya'; + case Lyngngam = 'lyg'; + case Luyana = 'lyn'; + case Literary_Chinese = 'lzh'; + case Litzlitz = 'lzl'; + case Leinong_Naga = 'lzn'; + case Laz = 'lzz'; + case San_Jeronimo_Tecoatl_Mazatec = 'maa'; + case Yutanduchi_Mixtec = 'mab'; + case Madurese = 'mad'; + case Bo_Rukul = 'mae'; + case Mafa = 'maf'; + case Magahi = 'mag'; + case Marshallese = 'mah'; + case Maithili = 'mai'; + case Jalapa_De_Diaz_Mazatec = 'maj'; + case Makasar = 'mak'; + case Malayalam = 'mal'; + case Mam = 'mam'; + case Mandingo = 'man'; + case Chiquihuitlan_Mazatec = 'maq'; + case Marathi = 'mar'; + case Masai = 'mas'; + case San_Francisco_Matlatzinca = 'mat'; + case Huautla_Mazatec = 'mau'; + case Satere_Mawe = 'mav'; + case Mampruli = 'maw'; + case North_Moluccan_Malay = 'max'; + case Central_Mazahua = 'maz'; + case Higaonon = 'mba'; + case Western_Bukidnon_Manobo = 'mbb'; + case Macushi = 'mbc'; + case Dibabawon_Manobo = 'mbd'; + case Molale = 'mbe'; + case Baba_Malay = 'mbf'; + case Mangseng = 'mbh'; + case Ilianen_Manobo = 'mbi'; + case Nadeb = 'mbj'; + case Malol = 'mbk'; + case Maxakali = 'mbl'; + case Ombamba = 'mbm'; + case Macaguan = 'mbn'; + case Mbo_Cameroon = 'mbo'; + case Malayo = 'mbp'; + case Maisin = 'mbq'; + case Nukak_Maku = 'mbr'; + case Sarangani_Manobo = 'mbs'; + case Matigsalug_Manobo = 'mbt'; + case Mbula_Bwazza = 'mbu'; + case Mbulungish = 'mbv'; + case Maring = 'mbw'; + case Mari_East_Sepik_Province = 'mbx'; + case Memoni = 'mby'; + case Amoltepec_Mixtec = 'mbz'; + case Maca = 'mca'; + case Machiguenga = 'mcb'; + case Bitur = 'mcc'; + case Sharanahua = 'mcd'; + case Itundujia_Mixtec = 'mce'; + case Matses = 'mcf'; + case Mapoyo = 'mcg'; + case Maquiritari = 'mch'; + case Mese = 'mci'; + case Mvanip = 'mcj'; + case Mbunda = 'mck'; + case Macaguaje = 'mcl'; + case Malaccan_Creole_Portuguese = 'mcm'; + case Masana = 'mcn'; + case Coatlan_Mixe = 'mco'; + case Makaa = 'mcp'; + case Ese = 'mcq'; + case Menya = 'mcr'; + case Mambai = 'mcs'; + case Mengisa = 'mct'; + case Cameroon_Mambila = 'mcu'; + case Minanibai = 'mcv'; + case Mawa_Chad = 'mcw'; + case Mpiemo = 'mcx'; + case South_Watut = 'mcy'; + case Mawan = 'mcz'; + case Mada_Nigeria = 'mda'; + case Morigi = 'mdb'; + case Male_Papua_New_Guinea = 'mdc'; + case Mbum = 'mdd'; + case Maba_Chad = 'mde'; + case Moksha = 'mdf'; + case Massalat = 'mdg'; + case Maguindanaon = 'mdh'; + case Mamvu = 'mdi'; + case Mangbetu = 'mdj'; + case Mangbutu = 'mdk'; + case Maltese_Sign_Language = 'mdl'; + case Mayogo = 'mdm'; + case Mbati = 'mdn'; + case Mbala = 'mdp'; + case Mbole = 'mdq'; + case Mandar = 'mdr'; + case Maria_Papua_New_Guinea = 'mds'; + case Mbere = 'mdt'; + case Mboko = 'mdu'; + case Santa_Lucia_Monteverde_Mixtec = 'mdv'; + case Mbosi = 'mdw'; + case Dizin = 'mdx'; + case Male_Ethiopia = 'mdy'; + case Surui_Do_Para = 'mdz'; + case Menka = 'mea'; + case Ikobi = 'meb'; + case Marra = 'mec'; + case Melpa = 'med'; + case Mengen = 'mee'; + case Megam = 'mef'; + case Southwestern_Tlaxiaco_Mixtec = 'meh'; + case Midob = 'mei'; + case Meyah = 'mej'; + case Mekeo = 'mek'; + case Central_Melanau = 'mel'; + case Mangala = 'mem'; + case Mende_Sierra_Leone = 'men'; + case Kedah_Malay = 'meo'; + case Miriwoong = 'mep'; + case Merey = 'meq'; + case Meru = 'mer'; + case Masmaje = 'mes'; + case Mato = 'met'; + case Motu = 'meu'; + case Mano = 'mev'; + case Maaka = 'mew'; + case Hassaniyya = 'mey'; + case Menominee = 'mez'; + case Pattani_Malay = 'mfa'; + case Bangka = 'mfb'; + case Mba = 'mfc'; + case Mendankwe_Nkwen = 'mfd'; + case Morisyen = 'mfe'; + case Naki = 'mff'; + case Mogofin = 'mfg'; + case Matal = 'mfh'; + case Wandala = 'mfi'; + case Mefele = 'mfj'; + case North_Mofu = 'mfk'; + case Putai = 'mfl'; + case Marghi_South = 'mfm'; + case Cross_River_Mbembe = 'mfn'; + case Mbe = 'mfo'; + case Makassar_Malay = 'mfp'; + case Moba = 'mfq'; + case Marrithiyel = 'mfr'; + case Mexican_Sign_Language = 'mfs'; + case Mokerang = 'mft'; + case Mbwela = 'mfu'; + case Mandjak = 'mfv'; + case Mulaha = 'mfw'; + case Melo = 'mfx'; + case Mayo = 'mfy'; + case Mabaan = 'mfz'; + case Middle_Irish_900_1200 = 'mga'; + case Mararit = 'mgb'; + case Morokodo = 'mgc'; + case Moru = 'mgd'; + case Mango = 'mge'; + case Maklew = 'mgf'; + case Mpumpong = 'mgg'; + case Makhuwa_Meetto = 'mgh'; + case Lijili = 'mgi'; + case Abureni = 'mgj'; + case Mawes = 'mgk'; + case Maleu_Kilenge = 'mgl'; + case Mambae = 'mgm'; + case Mbangi = 'mgn'; + case Meta = 'mgo'; + case Eastern_Magar = 'mgp'; + case Malila = 'mgq'; + case Mambwe_Lungu = 'mgr'; + case Manda_Tanzania = 'mgs'; + case Mongol = 'mgt'; + case Mailu = 'mgu'; + case Matengo = 'mgv'; + case Matumbi = 'mgw'; + case Mbunga = 'mgy'; + case Mbugwe = 'mgz'; + case Manda_India = 'mha'; + case Mahongwe = 'mhb'; + case Mocho = 'mhc'; + case Mbugu = 'mhd'; + case Besisi = 'mhe'; + case Mamaa = 'mhf'; + case Margu = 'mhg'; + case Ma_di = 'mhi'; + case Mogholi = 'mhj'; + case Mungaka = 'mhk'; + case Mauwake = 'mhl'; + case Makhuwa_Moniga = 'mhm'; + case Mocheno = 'mhn'; + case Mashi_Zambia = 'mho'; + case Balinese_Malay = 'mhp'; + case Mandan = 'mhq'; + case Eastern_Mari = 'mhr'; + case Buru_Indonesia = 'mhs'; + case Mandahuaca = 'mht'; + case Digaro_Mishmi = 'mhu'; + case Mbukushu = 'mhw'; + case Maru = 'mhx'; + case Ma_anyan = 'mhy'; + case Mor_Mor_Islands = 'mhz'; + case Miami = 'mia'; + case Atatlahuca_Mixtec = 'mib'; + case Mi_kmaq = 'mic'; + case Mandaic = 'mid'; + case Ocotepec_Mixtec = 'mie'; + case Mofu_Gudur = 'mif'; + case San_Miguel_El_Grande_Mixtec = 'mig'; + case Chayuco_Mixtec = 'mih'; + case Chigmecatitlan_Mixtec = 'mii'; + case Abar = 'mij'; + case Mikasuki = 'mik'; + case Penoles_Mixtec = 'mil'; + case Alacatlatzala_Mixtec = 'mim'; + case Minangkabau = 'min'; + case Pinotepa_Nacional_Mixtec = 'mio'; + case Apasco_Apoala_Mixtec = 'mip'; + case Miskito = 'miq'; + case Isthmus_Mixe = 'mir'; + case Uncoded_languages = 'mis'; + case Southern_Puebla_Mixtec = 'mit'; + case Cacaloxtepec_Mixtec = 'miu'; + case Akoye = 'miw'; + case Mixtepec_Mixtec = 'mix'; + case Ayutla_Mixtec = 'miy'; + case Coatzospan_Mixtec = 'miz'; + case Makalero = 'mjb'; + case San_Juan_Colorado_Mixtec = 'mjc'; + case Northwest_Maidu = 'mjd'; + case Muskum = 'mje'; + case Tu = 'mjg'; + case Mwera_Nyasa = 'mjh'; + case Kim_Mun = 'mji'; + case Mawak = 'mjj'; + case Matukar = 'mjk'; + case Mandeali = 'mjl'; + case Medebur = 'mjm'; + case Ma_Papua_New_Guinea = 'mjn'; + case Malankuravan = 'mjo'; + case Malapandaram = 'mjp'; + case Malaryan = 'mjq'; + case Malavedan = 'mjr'; + case Miship = 'mjs'; + case Sauria_Paharia = 'mjt'; + case Manna_Dora = 'mju'; + case Mannan = 'mjv'; + case Karbi = 'mjw'; + case Mahali = 'mjx'; + case Mahican = 'mjy'; + case Majhi = 'mjz'; + case Mbre = 'mka'; + case Mal_Paharia = 'mkb'; + case Siliput = 'mkc'; + case Macedonian = 'mkd'; + case Mawchi = 'mke'; + case Miya = 'mkf'; + case Mak_China = 'mkg'; + case Dhatki = 'mki'; + case Mokilese = 'mkj'; + case Byep = 'mkk'; + case Mokole = 'mkl'; + case Moklen = 'mkm'; + case Kupang_Malay = 'mkn'; + case Mingang_Doso = 'mko'; + case Moikodi = 'mkp'; + case Bay_Miwok = 'mkq'; + case Malas = 'mkr'; + case Silacayoapan_Mixtec = 'mks'; + case Vamale = 'mkt'; + case Konyanka_Maninka = 'mku'; + case Mafea = 'mkv'; + case Kituba_Congo = 'mkw'; + case Kinamiging_Manobo = 'mkx'; + case East_Makian = 'mky'; + case Makasae = 'mkz'; + case Malo = 'mla'; + case Mbule = 'mlb'; + case Cao_Lan = 'mlc'; + case Manambu = 'mle'; + case Mal = 'mlf'; + case Malagasy = 'mlg'; + case Mape = 'mlh'; + case Malimpung = 'mli'; + case Miltu = 'mlj'; + case Ilwana = 'mlk'; + case Malua_Bay = 'mll'; + case Mulam = 'mlm'; + case Malango = 'mln'; + case Mlomp = 'mlo'; + case Bargam = 'mlp'; + case Western_Maninkakan = 'mlq'; + case Vame = 'mlr'; + case Masalit = 'mls'; + case Maltese = 'mlt'; + case To_abaita = 'mlu'; + case Motlav = 'mlv'; + case Moloko = 'mlw'; + case Malfaxal = 'mlx'; + case Malaynon = 'mlz'; + case Mama = 'mma'; + case Momina = 'mmb'; + case Michoacan_Mazahua = 'mmc'; + case Maonan = 'mmd'; + case Mae = 'mme'; + case Mundat = 'mmf'; + case North_Ambrym = 'mmg'; + case Mehinaku = 'mmh'; + case Musar = 'mmi'; + case Majhwar = 'mmj'; + case Mukha_Dora = 'mmk'; + case Man_Met = 'mml'; + case Maii = 'mmm'; + case Mamanwa = 'mmn'; + case Mangga_Buang = 'mmo'; + case Siawi = 'mmp'; + case Musak = 'mmq'; + case Western_Xiangxi_Miao = 'mmr'; + case Malalamai = 'mmt'; + case Mmaala = 'mmu'; + case Miriti = 'mmv'; + case Emae = 'mmw'; + case Madak = 'mmx'; + case Migaama = 'mmy'; + case Mabaale = 'mmz'; + case Mbula = 'mna'; + case Muna = 'mnb'; + case Manchu = 'mnc'; + case Monde = 'mnd'; + case Naba = 'mne'; + case Mundani = 'mnf'; + case Eastern_Mnong = 'mng'; + case Mono_Democratic_Republic_of_Congo = 'mnh'; + case Manipuri = 'mni'; + case Munji = 'mnj'; + case Mandinka = 'mnk'; + case Tiale = 'mnl'; + case Mapena = 'mnm'; + case Southern_Mnong = 'mnn'; + case Min_Bei_Chinese = 'mnp'; + case Minriq = 'mnq'; + case Mono_USA = 'mnr'; + case Mansi = 'mns'; + case Mer = 'mnu'; + case Rennell_Bellona = 'mnv'; + case Mon = 'mnw'; + case Manikion = 'mnx'; + case Manyawa = 'mny'; + case Moni = 'mnz'; + case Mwan = 'moa'; + case Mocovi = 'moc'; + case Mobilian = 'mod'; + case Innu = 'moe'; + case Mongondow = 'mog'; + case Mohawk = 'moh'; + case Mboi = 'moi'; + case Monzombo = 'moj'; + case Morori = 'mok'; + case Mangue = 'mom'; + case Mongolian = 'mon'; + case Monom = 'moo'; + case Mopan_Maya = 'mop'; + case Mor_Bomberai_Peninsula = 'moq'; + case Moro = 'mor'; + case Mossi = 'mos'; + case Bari_2 = 'mot'; + case Mogum = 'mou'; + case Mohave = 'mov'; + case Moi_Congo = 'mow'; + case Molima = 'mox'; + case Shekkacho = 'moy'; + case Mukulu = 'moz'; + case Mpoto = 'mpa'; + case Malak_Malak = 'mpb'; + case Mangarrayi = 'mpc'; + case Machinere = 'mpd'; + case Majang = 'mpe'; + case Marba = 'mpg'; + case Maung = 'mph'; + case Mpade = 'mpi'; + case Martu_Wangka = 'mpj'; + case Mbara_Chad = 'mpk'; + case Middle_Watut = 'mpl'; + case Yosondua_Mixtec = 'mpm'; + case Mindiri = 'mpn'; + case Miu = 'mpo'; + case Migabac = 'mpp'; + case Matis = 'mpq'; + case Vangunu = 'mpr'; + case Dadibi = 'mps'; + case Mian = 'mpt'; + case Makurap = 'mpu'; + case Mungkip = 'mpv'; + case Mapidian = 'mpw'; + case Misima_Panaeati = 'mpx'; + case Mapia = 'mpy'; + case Mpi = 'mpz'; + case Maba_Indonesia = 'mqa'; + case Mbuko = 'mqb'; + case Mangole = 'mqc'; + case Matepi = 'mqe'; + case Momuna = 'mqf'; + case Kota_Bangun_Kutai_Malay = 'mqg'; + case Tlazoyaltepec_Mixtec = 'mqh'; + case Mariri = 'mqi'; + case Mamasa = 'mqj'; + case Rajah_Kabunsuwan_Manobo = 'mqk'; + case Mbelime = 'mql'; + case South_Marquesan = 'mqm'; + case Moronene = 'mqn'; + case Modole = 'mqo'; + case Manipa = 'mqp'; + case Minokok = 'mqq'; + case Mander = 'mqr'; + case West_Makian = 'mqs'; + case Mok = 'mqt'; + case Mandari = 'mqu'; + case Mosimo = 'mqv'; + case Murupi = 'mqw'; + case Mamuju = 'mqx'; + case Manggarai = 'mqy'; + case Pano = 'mqz'; + case Mlabri = 'mra'; + case Marino = 'mrb'; + case Maricopa = 'mrc'; + case Western_Magar = 'mrd'; + case Martha_s_Vineyard_Sign_Language = 'mre'; + case Elseng = 'mrf'; + case Mising = 'mrg'; + case Mara_Chin = 'mrh'; + case Maori = 'mri'; + case Western_Mari = 'mrj'; + case Hmwaveke = 'mrk'; + case Mortlockese = 'mrl'; + case Merlav = 'mrm'; + case Cheke_Holo = 'mrn'; + case Mru = 'mro'; + case Morouas = 'mrp'; + case North_Marquesan = 'mrq'; + case Maria_India = 'mrr'; + case Maragus = 'mrs'; + case Marghi_Central = 'mrt'; + case Mono_Cameroon = 'mru'; + case Mangareva = 'mrv'; + case Maranao = 'mrw'; + case Maremgi = 'mrx'; + case Mandaya = 'mry'; + case Marind = 'mrz'; + case Malay_macrolanguage = 'msa'; + case Masbatenyo = 'msb'; + case Sankaran_Maninka = 'msc'; + case Yucatec_Maya_Sign_Language = 'msd'; + case Musey = 'mse'; + case Mekwei = 'msf'; + case Moraid = 'msg'; + case Masikoro_Malagasy = 'msh'; + case Sabah_Malay = 'msi'; + case Ma_Democratic_Republic_of_Congo = 'msj'; + case Mansaka = 'msk'; + case Molof = 'msl'; + case Agusan_Manobo = 'msm'; + case Vures = 'msn'; + case Mombum = 'mso'; + case Maritsaua = 'msp'; + case Caac = 'msq'; + case Mongolian_Sign_Language = 'msr'; + case West_Masela = 'mss'; + case Musom = 'msu'; + case Maslam = 'msv'; + case Mansoanka = 'msw'; + case Moresada = 'msx'; + case Aruamu = 'msy'; + case Momare = 'msz'; + case Cotabato_Manobo = 'mta'; + case Anyin_Morofo = 'mtb'; + case Munit = 'mtc'; + case Mualang = 'mtd'; + case Mono_Solomon_Islands = 'mte'; + case Murik_Papua_New_Guinea = 'mtf'; + case Una = 'mtg'; + case Munggui = 'mth'; + case Maiwa_Papua_New_Guinea = 'mti'; + case Moskona = 'mtj'; + case Mbe_2 = 'mtk'; + case Montol = 'mtl'; + case Mator = 'mtm'; + case Matagalpa = 'mtn'; + case Totontepec_Mixe = 'mto'; + case Wichi_Lhamtes_Nocten = 'mtp'; + case Muong = 'mtq'; + case Mewari = 'mtr'; + case Yora = 'mts'; + case Mota = 'mtt'; + case Tututepec_Mixtec = 'mtu'; + case Asaro_o = 'mtv'; + case Southern_Binukidnon = 'mtw'; + case Tidaa_Mixtec = 'mtx'; + case Nabi = 'mty'; + case Mundang = 'mua'; + case Mubi = 'mub'; + case Ajumbu = 'muc'; + case Mednyj_Aleut = 'mud'; + case Media_Lengua = 'mue'; + case Musgu = 'mug'; + case Mundu = 'muh'; + case Musi = 'mui'; + case Mabire = 'muj'; + case Mugom = 'muk'; + case Multiple_languages = 'mul'; + case Maiwala = 'mum'; + case Nyong = 'muo'; + case Malvi = 'mup'; + case Eastern_Xiangxi_Miao = 'muq'; + case Murle = 'mur'; + case Creek = 'mus'; + case Western_Muria = 'mut'; + case Yaaku = 'muu'; + case Muthuvan = 'muv'; + case Bo_Ung = 'mux'; + case Muyang = 'muy'; + case Mursi = 'muz'; + case Manam = 'mva'; + case Mattole = 'mvb'; + case Mamboru = 'mvd'; + case Marwari_Pakistan = 'mve'; + case Peripheral_Mongolian = 'mvf'; + case Yucuane_Mixtec = 'mvg'; + case Mulgi = 'mvh'; + case Miyako = 'mvi'; + case Mekmek = 'mvk'; + case Mbara_Australia = 'mvl'; + case Minaveha = 'mvn'; + case Marovo = 'mvo'; + case Duri = 'mvp'; + case Moere = 'mvq'; + case Marau = 'mvr'; + case Massep = 'mvs'; + case Mpotovoro = 'mvt'; + case Marfa = 'mvu'; + case Tagal_Murut = 'mvv'; + case Machinga = 'mvw'; + case Meoswar = 'mvx'; + case Indus_Kohistani = 'mvy'; + case Mesqan = 'mvz'; + case Mwatebu = 'mwa'; + case Juwal = 'mwb'; + case Are = 'mwc'; + case Mwera_Chimwera = 'mwe'; + case Murrinh_Patha = 'mwf'; + case Aiklep = 'mwg'; + case Mouk_Aria = 'mwh'; + case Labo = 'mwi'; + case Kita_Maninkakan = 'mwk'; + case Mirandese = 'mwl'; + case Sar = 'mwm'; + case Nyamwanga = 'mwn'; + case Central_Maewo = 'mwo'; + case Kala_Lagaw_Ya = 'mwp'; + case Mun_Chin = 'mwq'; + case Marwari = 'mwr'; + case Mwimbi_Muthambi = 'mws'; + case Moken = 'mwt'; + case Mittu = 'mwu'; + case Mentawai = 'mwv'; + case Hmong_Daw = 'mww'; + case Moingi = 'mwz'; + case Northwest_Oaxaca_Mixtec = 'mxa'; + case Tezoatlan_Mixtec = 'mxb'; + case Manyika = 'mxc'; + case Modang = 'mxd'; + case Mele_Fila = 'mxe'; + case Malgbe = 'mxf'; + case Mbangala = 'mxg'; + case Mvuba = 'mxh'; + case Mozarabic = 'mxi'; + case Miju_Mishmi = 'mxj'; + case Monumbo = 'mxk'; + case Maxi_Gbe = 'mxl'; + case Meramera = 'mxm'; + case Moi_Indonesia = 'mxn'; + case Mbowe = 'mxo'; + case Tlahuitoltepec_Mixe = 'mxp'; + case Juquila_Mixe = 'mxq'; + case Murik_Malaysia = 'mxr'; + case Huitepec_Mixtec = 'mxs'; + case Jamiltepec_Mixtec = 'mxt'; + case Mada_Cameroon = 'mxu'; + case Metlatonoc_Mixtec = 'mxv'; + case Namo = 'mxw'; + case Mahou = 'mxx'; + case Southeastern_Nochixtlan_Mixtec = 'mxy'; + case Central_Masela = 'mxz'; + case Burmese = 'mya'; + case Mbay = 'myb'; + case Mayeka = 'myc'; + case Myene = 'mye'; + case Bambassi = 'myf'; + case Manta = 'myg'; + case Makah = 'myh'; + case Mangayat = 'myj'; + case Mamara_Senoufo = 'myk'; + case Moma = 'myl'; + case Me_en = 'mym'; + case Anfillo = 'myo'; + case Piraha = 'myp'; + case Muniche = 'myr'; + case Mesmes = 'mys'; + case Munduruku = 'myu'; + case Erzya = 'myv'; + case Muyuw = 'myw'; + case Masaaba = 'myx'; + case Macuna = 'myy'; + case Classical_Mandaic = 'myz'; + case Santa_Maria_Zacatepec_Mixtec = 'mza'; + case Tumzabt = 'mzb'; + case Madagascar_Sign_Language = 'mzc'; + case Malimba = 'mzd'; + case Morawa = 'mze'; + case Monastic_Sign_Language = 'mzg'; + case Wichi_Lhamtes_Guisnay = 'mzh'; + case Ixcatlan_Mazatec = 'mzi'; + case Manya = 'mzj'; + case Nigeria_Mambila = 'mzk'; + case Mazatlan_Mixe = 'mzl'; + case Mumuye = 'mzm'; + case Mazanderani = 'mzn'; + case Matipuhy = 'mzo'; + case Movima = 'mzp'; + case Mori_Atas = 'mzq'; + case Marubo = 'mzr'; + case Macanese = 'mzs'; + case Mintil = 'mzt'; + case Inapang = 'mzu'; + case Manza = 'mzv'; + case Deg = 'mzw'; + case Mawayana = 'mzx'; + case Mozambican_Sign_Language = 'mzy'; + case Maiadomu = 'mzz'; + case Namla = 'naa'; + case Southern_Nambikuara = 'nab'; + case Narak = 'nac'; + case Naka_ela = 'nae'; + case Nabak = 'naf'; + case Naga_Pidgin = 'nag'; + case Nalu = 'naj'; + case Nakanai = 'nak'; + case Nalik = 'nal'; + case Ngan_gityemerri = 'nam'; + case Min_Nan_Chinese = 'nan'; + case Naaba = 'nao'; + case Neapolitan = 'nap'; + case Khoekhoe = 'naq'; + case Iguta = 'nar'; + case Naasioi = 'nas'; + case Cahungwarya = 'nat'; + case Nauru = 'nau'; + case Navajo = 'nav'; + case Nawuri = 'naw'; + case Nakwi = 'nax'; + case Ngarrindjeri = 'nay'; + case Coatepec_Nahuatl = 'naz'; + case Nyemba = 'nba'; + case Ndoe = 'nbb'; + case Chang_Naga = 'nbc'; + case Ngbinda = 'nbd'; + case Konyak_Naga = 'nbe'; + case Nagarchal = 'nbg'; + case Ngamo = 'nbh'; + case Mao_Naga = 'nbi'; + case Ngarinyman = 'nbj'; + case Nake = 'nbk'; + case South_Ndebele = 'nbl'; + case Ngbaka_Ma_bo = 'nbm'; + case Kuri = 'nbn'; + case Nkukoli = 'nbo'; + case Nnam = 'nbp'; + case Nggem = 'nbq'; + case Numana = 'nbr'; + case Namibian_Sign_Language = 'nbs'; + case Na = 'nbt'; + case Rongmei_Naga = 'nbu'; + case Ngamambo = 'nbv'; + case Southern_Ngbandi = 'nbw'; + case Ningera = 'nby'; + case Iyo = 'nca'; + case Central_Nicobarese = 'ncb'; + case Ponam = 'ncc'; + case Nachering = 'ncd'; + case Yale = 'nce'; + case Notsi = 'ncf'; + case Nisga_a = 'ncg'; + case Central_Huasteca_Nahuatl = 'nch'; + case Classical_Nahuatl = 'nci'; + case Northern_Puebla_Nahuatl = 'ncj'; + case Na_kara = 'nck'; + case Michoacan_Nahuatl = 'ncl'; + case Nambo = 'ncm'; + case Nauna = 'ncn'; + case Sibe = 'nco'; + case Northern_Katang = 'ncq'; + case Ncane = 'ncr'; + case Nicaraguan_Sign_Language = 'ncs'; + case Chothe_Naga = 'nct'; + case Chumburung = 'ncu'; + case Central_Puebla_Nahuatl = 'ncx'; + case Natchez = 'ncz'; + case Ndasa = 'nda'; + case Kenswei_Nsei = 'ndb'; + case Ndau = 'ndc'; + case Nde_Nsele_Nta = 'ndd'; + case North_Ndebele = 'nde'; + case Nadruvian = 'ndf'; + case Ndengereko = 'ndg'; + case Ndali = 'ndh'; + case Samba_Leko = 'ndi'; + case Ndamba = 'ndj'; + case Ndaka = 'ndk'; + case Ndolo = 'ndl'; + case Ndam = 'ndm'; + case Ngundi = 'ndn'; + case Ndonga = 'ndo'; + case Ndo = 'ndp'; + case Ndombe = 'ndq'; + case Ndoola = 'ndr'; + case Low_German = 'nds'; + case Ndunga = 'ndt'; + case Dugun = 'ndu'; + case Ndut = 'ndv'; + case Ndobo = 'ndw'; + case Nduga = 'ndx'; + case Lutos = 'ndy'; + case Ndogo = 'ndz'; + case Eastern_Ngad_a = 'nea'; + case Toura_Cote_d_Ivoire = 'neb'; + case Nedebang = 'nec'; + case Nde_Gbite = 'ned'; + case Nelemwa_Nixumwak = 'nee'; + case Nefamese = 'nef'; + case Negidal = 'neg'; + case Nyenkha = 'neh'; + case Neo_Hittite = 'nei'; + case Neko = 'nej'; + case Neku = 'nek'; + case Nemi = 'nem'; + case Nengone = 'nen'; + case Na_Meo = 'neo'; + case Nepali_macrolanguage = 'nep'; + case North_Central_Mixe = 'neq'; + case Yahadian = 'ner'; + case Bhoti_Kinnauri = 'nes'; + case Nete = 'net'; + case Neo = 'neu'; + case Nyaheun = 'nev'; + case Newari = 'new'; + case Neme = 'nex'; + case Neyo = 'ney'; + case Nez_Perce = 'nez'; + case Dhao = 'nfa'; + case Ahwai = 'nfd'; + case Ayiwo = 'nfl'; + case Nafaanra = 'nfr'; + case Mfumte = 'nfu'; + case Ngbaka = 'nga'; + case Northern_Ngbandi = 'ngb'; + case Ngombe_Democratic_Republic_of_Congo = 'ngc'; + case Ngando_Central_African_Republic = 'ngd'; + case Ngemba = 'nge'; + case Ngbaka_Manza = 'ngg'; + case N_ng = 'ngh'; + case Ngizim = 'ngi'; + case Ngie = 'ngj'; + case Dalabon = 'ngk'; + case Lomwe = 'ngl'; + case Ngatik_Men_s_Creole = 'ngm'; + case Ngwo = 'ngn'; + case Ngulu = 'ngp'; + case Ngurimi = 'ngq'; + case Engdewu = 'ngr'; + case Gvoko = 'ngs'; + case Kriang = 'ngt'; + case Guerrero_Nahuatl = 'ngu'; + case Nagumi = 'ngv'; + case Ngwaba = 'ngw'; + case Nggwahyi = 'ngx'; + case Tibea = 'ngy'; + case Ngungwel = 'ngz'; + case Nhanda = 'nha'; + case Beng = 'nhb'; + case Tabasco_Nahuatl = 'nhc'; + case Chiripa = 'nhd'; + case Eastern_Huasteca_Nahuatl = 'nhe'; + case Nhuwala = 'nhf'; + case Tetelcingo_Nahuatl = 'nhg'; + case Nahari = 'nhh'; + case Zacatlan_Ahuacatlan_Tepetzintla_Nahuatl = 'nhi'; + case Isthmus_Cosoleacaque_Nahuatl = 'nhk'; + case Morelos_Nahuatl = 'nhm'; + case Central_Nahuatl = 'nhn'; + case Takuu = 'nho'; + case Isthmus_Pajapan_Nahuatl = 'nhp'; + case Huaxcaleca_Nahuatl = 'nhq'; + case Naro = 'nhr'; + case Ometepec_Nahuatl = 'nht'; + case Noone = 'nhu'; + case Temascaltepec_Nahuatl = 'nhv'; + case Western_Huasteca_Nahuatl = 'nhw'; + case Isthmus_Mecayapan_Nahuatl = 'nhx'; + case Northern_Oaxaca_Nahuatl = 'nhy'; + case Santa_Maria_La_Alta_Nahuatl = 'nhz'; + case Nias = 'nia'; + case Nakame = 'nib'; + case Ngandi = 'nid'; + case Niellim = 'nie'; + case Nek = 'nif'; + case Ngalakgan = 'nig'; + case Nyiha_Tanzania = 'nih'; + case Nii = 'nii'; + case Ngaju = 'nij'; + case Southern_Nicobarese = 'nik'; + case Nila = 'nil'; + case Nilamba = 'nim'; + case Ninzo = 'nin'; + case Nganasan = 'nio'; + case Nandi = 'niq'; + case Nimboran = 'nir'; + case Nimi = 'nis'; + case Southeastern_Kolami = 'nit'; + case Niuean = 'niu'; + case Gilyak = 'niv'; + case Nimo = 'niw'; + case Hema = 'nix'; + case Ngiti = 'niy'; + case Ningil = 'niz'; + case Nzanyi = 'nja'; + case Nocte_Naga = 'njb'; + case Ndonde_Hamba = 'njd'; + case Lotha_Naga = 'njh'; + case Gudanji = 'nji'; + case Njen = 'njj'; + case Njalgulgule = 'njl'; + case Angami_Naga = 'njm'; + case Liangmai_Naga = 'njn'; + case Ao_Naga = 'njo'; + case Njerep = 'njr'; + case Nisa = 'njs'; + case Ndyuka_Trio_Pidgin = 'njt'; + case Ngadjunmaya = 'nju'; + case Kunyi = 'njx'; + case Njyem = 'njy'; + case Nyishi = 'njz'; + case Nkoya = 'nka'; + case Khoibu_Naga = 'nkb'; + case Nkongho = 'nkc'; + case Koireng = 'nkd'; + case Duke = 'nke'; + case Inpui_Naga = 'nkf'; + case Nekgini = 'nkg'; + case Khezha_Naga = 'nkh'; + case Thangal_Naga = 'nki'; + case Nakai = 'nkj'; + case Nokuku = 'nkk'; + case Namat = 'nkm'; + case Nkangala = 'nkn'; + case Nkonya = 'nko'; + case Niuatoputapu = 'nkp'; + case Nkami = 'nkq'; + case Nukuoro = 'nkr'; + case North_Asmat = 'nks'; + case Nyika_Tanzania = 'nkt'; + case Bouna_Kulango = 'nku'; + case Nyika_Malawi_and_Zambia = 'nkv'; + case Nkutu = 'nkw'; + case Nkoroo = 'nkx'; + case Nkari = 'nkz'; + case Ngombale = 'nla'; + case Nalca = 'nlc'; + case Dutch = 'nld'; + case East_Nyala = 'nle'; + case Gela = 'nlg'; + case Grangali = 'nli'; + case Nyali = 'nlj'; + case Ninia_Yali = 'nlk'; + case Nihali = 'nll'; + case Mankiyali = 'nlm'; + case Ngul = 'nlo'; + case Lao_Naga = 'nlq'; + case Nchumbulu = 'nlu'; + case Orizaba_Nahuatl = 'nlv'; + case Walangama = 'nlw'; + case Nahali = 'nlx'; + case Nyamal = 'nly'; + case Nalogo = 'nlz'; + case Maram_Naga = 'nma'; + case Big_Nambas = 'nmb'; + case Ngam = 'nmc'; + case Ndumu = 'nmd'; + case Mzieme_Naga = 'nme'; + case Tangkhul_Naga_India = 'nmf'; + case Kwasio = 'nmg'; + case Monsang_Naga = 'nmh'; + case Nyam = 'nmi'; + case Ngombe_Central_African_Republic = 'nmj'; + case Namakura = 'nmk'; + case Ndemli = 'nml'; + case Manangba = 'nmm'; + case Xoo = 'nmn'; + case Moyon_Naga = 'nmo'; + case Nimanbur = 'nmp'; + case Nambya = 'nmq'; + case Nimbari = 'nmr'; + case Letemboi = 'nms'; + case Namonuito = 'nmt'; + case Northeast_Maidu = 'nmu'; + case Ngamini = 'nmv'; + case Nimoa = 'nmw'; + case Nama_Papua_New_Guinea = 'nmx'; + case Namuyi = 'nmy'; + case Nawdm = 'nmz'; + case Nyangumarta = 'nna'; + case Nande = 'nnb'; + case Nancere = 'nnc'; + case West_Ambae = 'nnd'; + case Ngandyera = 'nne'; + case Ngaing = 'nnf'; + case Maring_Naga = 'nng'; + case Ngiemboon = 'nnh'; + case North_Nuaulu = 'nni'; + case Nyangatom = 'nnj'; + case Nankina = 'nnk'; + case Northern_Rengma_Naga = 'nnl'; + case Namia = 'nnm'; + case Ngete = 'nnn'; + case Norwegian_Nynorsk = 'nno'; + case Wancho_Naga = 'nnp'; + case Ngindo = 'nnq'; + case Narungga = 'nnr'; + case Nanticoke = 'nnt'; + case Dwang = 'nnu'; + case Nugunu_Australia = 'nnv'; + case Southern_Nuni = 'nnw'; + case Nyangga = 'nny'; + case Nda_nda = 'nnz'; + case Woun_Meu = 'noa'; + case Norwegian_Bokmal = 'nob'; + case Nuk = 'noc'; + case Northern_Thai = 'nod'; + case Nimadi = 'noe'; + case Nomane = 'nof'; + case Nogai = 'nog'; + case Nomu = 'noh'; + case Noiri = 'noi'; + case Nonuya = 'noj'; + case Nooksack = 'nok'; + case Nomlaki = 'nol'; + case Old_Norse = 'non'; + case Numanggang = 'nop'; + case Ngongo = 'noq'; + case Norwegian = 'nor'; + case Eastern_Nisu = 'nos'; + case Nomatsiguenga = 'not'; + case Ewage_Notu = 'nou'; + case Novial = 'nov'; + case Nyambo = 'now'; + case Noy = 'noy'; + case Nayi = 'noz'; + case Nar_Phu = 'npa'; + case Nupbikha = 'npb'; + case Ponyo_Gongwang_Naga = 'npg'; + case Phom_Naga = 'nph'; + case Nepali_individual_language = 'npi'; + case Southeastern_Puebla_Nahuatl = 'npl'; + case Mondropolon = 'npn'; + case Pochuri_Naga = 'npo'; + case Nipsan = 'nps'; + case Puimei_Naga = 'npu'; + case Noipx = 'npx'; + case Napu = 'npy'; + case Southern_Nago = 'nqg'; + case Kura_Ede_Nago = 'nqk'; + case Ngendelengo = 'nql'; + case Ndom = 'nqm'; + case Nen = 'nqn'; + case N_Ko = 'nqo'; + case Kyan_Karyaw_Naga = 'nqq'; + case Nteng = 'nqt'; + case Akyaung_Ari_Naga = 'nqy'; + case Ngom = 'nra'; + case Nara = 'nrb'; + case Noric = 'nrc'; + case Southern_Rengma_Naga = 'nre'; + case Jerriais = 'nrf'; + case Narango = 'nrg'; + case Chokri_Naga = 'nri'; + case Ngarla = 'nrk'; + case Ngarluma = 'nrl'; + case Narom = 'nrm'; + case Norn = 'nrn'; + case North_Picene = 'nrp'; + case Norra = 'nrr'; + case Northern_Kalapuya = 'nrt'; + case Narua = 'nru'; + case Ngurmbur = 'nrx'; + case Lala = 'nrz'; + case Sangtam_Naga = 'nsa'; + case Lower_Nossob = 'nsb'; + case Nshi = 'nsc'; + case Southern_Nisu = 'nsd'; + case Nsenga = 'nse'; + case Northwestern_Nisu = 'nsf'; + case Ngasa = 'nsg'; + case Ngoshie = 'nsh'; + case Nigerian_Sign_Language = 'nsi'; + case Naskapi = 'nsk'; + case Norwegian_Sign_Language = 'nsl'; + case Sumi_Naga = 'nsm'; + case Nehan = 'nsn'; + case Pedi = 'nso'; + case Nepalese_Sign_Language = 'nsp'; + case Northern_Sierra_Miwok = 'nsq'; + case Maritime_Sign_Language = 'nsr'; + case Nali = 'nss'; + case Tase_Naga = 'nst'; + case Sierra_Negra_Nahuatl = 'nsu'; + case Southwestern_Nisu = 'nsv'; + case Navut = 'nsw'; + case Nsongo = 'nsx'; + case Nasal = 'nsy'; + case Nisenan = 'nsz'; + case Northern_Tidung = 'ntd'; + case Nathembo = 'nte'; + case Ngantangarra = 'ntg'; + case Natioro = 'nti'; + case Ngaanyatjarra = 'ntj'; + case Ikoma_Nata_Isenye = 'ntk'; + case Nateni = 'ntm'; + case Ntomba = 'nto'; + case Northern_Tepehuan = 'ntp'; + case Delo = 'ntr'; + case Natugu = 'ntu'; + case Nottoway = 'ntw'; + case Tangkhul_Naga_Myanmar = 'ntx'; + case Mantsi = 'nty'; + case Natanzi = 'ntz'; + case Yuanga = 'nua'; + case Nukuini = 'nuc'; + case Ngala = 'nud'; + case Ngundu = 'nue'; + case Nusu = 'nuf'; + case Nungali = 'nug'; + case Ndunda = 'nuh'; + case Ngumbi = 'nui'; + case Nyole = 'nuj'; + case Nuu_chah_nulth = 'nuk'; + case Nusa_Laut = 'nul'; + case Niuafo_ou = 'num'; + case Anong = 'nun'; + case Nguon = 'nuo'; + case Nupe_Nupe_Tako = 'nup'; + case Nukumanu = 'nuq'; + case Nukuria = 'nur'; + case Nuer = 'nus'; + case Nung_Viet_Nam = 'nut'; + case Ngbundu = 'nuu'; + case Northern_Nuni = 'nuv'; + case Nguluwan = 'nuw'; + case Mehek = 'nux'; + case Nunggubuyu = 'nuy'; + case Tlamacazapa_Nahuatl = 'nuz'; + case Nasarian = 'nvh'; + case Namiae = 'nvm'; + case Nyokon = 'nvo'; + case Nawathinehena = 'nwa'; + case Nyabwa = 'nwb'; + case Classical_Newari = 'nwc'; + case Ngwe = 'nwe'; + case Ngayawung = 'nwg'; + case Southwest_Tanna = 'nwi'; + case Nyamusa_Molo = 'nwm'; + case Nauo = 'nwo'; + case Nawaru = 'nwr'; + case Ndwewe = 'nww'; + case Middle_Newar = 'nwx'; + case Nottoway_Meherrin = 'nwy'; + case Nauete = 'nxa'; + case Ngando_Democratic_Republic_of_Congo = 'nxd'; + case Nage = 'nxe'; + case Ngad_a = 'nxg'; + case Nindi = 'nxi'; + case Koki_Naga = 'nxk'; + case South_Nuaulu = 'nxl'; + case Numidian = 'nxm'; + case Ngawun = 'nxn'; + case Ndambomo = 'nxo'; + case Naxi = 'nxq'; + case Ninggerum = 'nxr'; + case Nafri = 'nxx'; + case Nyanja = 'nya'; + case Nyangbo = 'nyb'; + case Nyanga_li = 'nyc'; + case Nyore = 'nyd'; + case Nyengo = 'nye'; + case Giryama = 'nyf'; + case Nyindu = 'nyg'; + case Nyikina = 'nyh'; + case Ama_Sudan = 'nyi'; + case Nyanga = 'nyj'; + case Nyaneka = 'nyk'; + case Nyeu = 'nyl'; + case Nyamwezi = 'nym'; + case Nyankole = 'nyn'; + case Nyoro = 'nyo'; + case Nyang_i = 'nyp'; + case Nayini = 'nyq'; + case Nyiha_Malawi = 'nyr'; + case Nyungar = 'nys'; + case Nyawaygi = 'nyt'; + case Nyungwe = 'nyu'; + case Nyulnyul = 'nyv'; + case Nyaw = 'nyw'; + case Nganyaywana = 'nyx'; + case Nyakyusa_Ngonde = 'nyy'; + case Tigon_Mbembe = 'nza'; + case Njebi = 'nzb'; + case Nzadi = 'nzd'; + case Nzima = 'nzi'; + case Nzakara = 'nzk'; + case Zeme_Naga = 'nzm'; + case Dir_Nyamzak_Mbarimi = 'nzr'; + case New_Zealand_Sign_Language = 'nzs'; + case Teke_Nzikou = 'nzu'; + case Nzakambay = 'nzy'; + case Nanga_Dama_Dogon = 'nzz'; + case Orok = 'oaa'; + case Oroch = 'oac'; + case Old_Aramaic_up_to_700_BCE = 'oar'; + case Old_Avar = 'oav'; + case Obispeno = 'obi'; + case Southern_Bontok = 'obk'; + case Oblo = 'obl'; + case Moabite = 'obm'; + case Obo_Manobo = 'obo'; + case Old_Burmese = 'obr'; + case Old_Breton = 'obt'; + case Obulom = 'obu'; + case Ocaina = 'oca'; + case Old_Chinese = 'och'; + case Occitan_post_1500 = 'oci'; + case Old_Cham = 'ocm'; + case Old_Cornish = 'oco'; + case Atzingo_Matlatzinca = 'ocu'; + case Odut = 'oda'; + case Od = 'odk'; + case Old_Dutch = 'odt'; + case Odual = 'odu'; + case Ofo = 'ofo'; + case Old_Frisian = 'ofs'; + case Efutop = 'ofu'; + case Ogbia = 'ogb'; + case Ogbah = 'ogc'; + case Old_Georgian = 'oge'; + case Ogbogolo = 'ogg'; + case Khana = 'ogo'; + case Ogbronuagum = 'ogu'; + case Old_Hittite = 'oht'; + case Old_Hungarian = 'ohu'; + case Oirata = 'oia'; + case Okolie = 'oie'; + case Inebu_One = 'oin'; + case Northwestern_Ojibwa = 'ojb'; + case Central_Ojibwa = 'ojc'; + case Eastern_Ojibwa = 'ojg'; + case Ojibwa = 'oji'; + case Old_Japanese = 'ojp'; + case Severn_Ojibwa = 'ojs'; + case Ontong_Java = 'ojv'; + case Western_Ojibwa = 'ojw'; + case Okanagan = 'oka'; + case Okobo = 'okb'; + case Kobo = 'okc'; + case Okodia = 'okd'; + case Okpe_Southwestern_Edo = 'oke'; + case Koko_Babangk = 'okg'; + case Koresh_e_Rostam = 'okh'; + case Okiek = 'oki'; + case Oko_Juwoi = 'okj'; + case Kwamtim_One = 'okk'; + case Old_Kentish_Sign_Language = 'okl'; + case Middle_Korean_10th_16th_cent = 'okm'; + case Oki_No_Erabu = 'okn'; + case Old_Korean_3rd_9th_cent = 'oko'; + case Kirike = 'okr'; + case Oko_Eni_Osayen = 'oks'; + case Oku = 'oku'; + case Orokaiva = 'okv'; + case Okpe_Northwestern_Edo = 'okx'; + case Old_Khmer = 'okz'; + case Walungge = 'ola'; + case Mochi = 'old'; + case Olekha = 'ole'; + case Olkol = 'olk'; + case Oloma = 'olm'; + case Livvi = 'olo'; + case Olrat = 'olr'; + case Old_Lithuanian = 'olt'; + case Kuvale = 'olu'; + case Omaha_Ponca = 'oma'; + case East_Ambae = 'omb'; + case Mochica = 'omc'; + case Omagua = 'omg'; + case Omi = 'omi'; + case Omok = 'omk'; + case Ombo = 'oml'; + case Minoan = 'omn'; + case Utarmbung = 'omo'; + case Old_Manipuri = 'omp'; + case Old_Marathi = 'omr'; + case Omotik = 'omt'; + case Omurano = 'omu'; + case South_Tairora = 'omw'; + case Old_Mon = 'omx'; + case Old_Malay = 'omy'; + case Ona = 'ona'; + case Lingao = 'onb'; + case Oneida = 'one'; + case Olo = 'ong'; + case Onin = 'oni'; + case Onjob = 'onj'; + case Kabore_One = 'onk'; + case Onobasulu = 'onn'; + case Onondaga = 'ono'; + case Sartang = 'onp'; + case Northern_One = 'onr'; + case Ono = 'ons'; + case Ontenu = 'ont'; + case Unua = 'onu'; + case Old_Nubian = 'onw'; + case Onin_Based_Pidgin = 'onx'; + case Tohono_O_odham = 'ood'; + case Ong = 'oog'; + case Onge = 'oon'; + case Oorlams = 'oor'; + case Old_Ossetic = 'oos'; + case Okpamheri = 'opa'; + case Kopkaka = 'opk'; + case Oksapmin = 'opm'; + case Opao = 'opo'; + case Opata = 'opt'; + case Ofaye = 'opy'; + case Oroha = 'ora'; + case Orma = 'orc'; + case Orejon = 'ore'; + case Oring = 'org'; + case Oroqen = 'orh'; + case Oriya_macrolanguage = 'ori'; + case Oromo = 'orm'; + case Orang_Kanaq = 'orn'; + case Orokolo = 'oro'; + case Oruma = 'orr'; + case Orang_Seletar = 'ors'; + case Adivasi_Oriya = 'ort'; + case Ormuri = 'oru'; + case Old_Russian = 'orv'; + case Oro_Win = 'orw'; + case Oro = 'orx'; + case Odia = 'ory'; + case Ormu = 'orz'; + case Osage = 'osa'; + case Oscan = 'osc'; + case Osing = 'osi'; + case Old_Sundanese = 'osn'; + case Ososo = 'oso'; + case Old_Spanish = 'osp'; + case Ossetian = 'oss'; + case Osatu = 'ost'; + case Southern_One = 'osu'; + case Old_Saxon = 'osx'; + case Ottoman_Turkish_1500_1928 = 'ota'; + case Old_Tibetan = 'otb'; + case Ot_Danum = 'otd'; + case Mezquital_Otomi = 'ote'; + case Oti = 'oti'; + case Old_Turkish = 'otk'; + case Tilapa_Otomi = 'otl'; + case Eastern_Highland_Otomi = 'otm'; + case Tenango_Otomi = 'otn'; + case Queretaro_Otomi = 'otq'; + case Otoro = 'otr'; + case Estado_de_Mexico_Otomi = 'ots'; + case Temoaya_Otomi = 'ott'; + case Otuke = 'otu'; + case Ottawa = 'otw'; + case Texcatepec_Otomi = 'otx'; + case Old_Tamil = 'oty'; + case Ixtenco_Otomi = 'otz'; + case Tagargrent = 'oua'; + case Glio_Oubi = 'oub'; + case Oune = 'oue'; + case Old_Uighur = 'oui'; + case Ouma = 'oum'; + case Elfdalian = 'ovd'; + case Owiniga = 'owi'; + case Old_Welsh = 'owl'; + case Oy = 'oyb'; + case Oyda = 'oyd'; + case Wayampi = 'oym'; + case Oya_oya = 'oyy'; + case Koonzime = 'ozm'; + case Parecis = 'pab'; + case Pacoh = 'pac'; + case Paumari = 'pad'; + case Pagibete = 'pae'; + case Paranawat = 'paf'; + case Pangasinan = 'pag'; + case Tenharim = 'pah'; + case Pe = 'pai'; + case Parakana = 'pak'; + case Pahlavi = 'pal'; + case Pampanga = 'pam'; + case Panjabi = 'pan'; + case Northern_Paiute = 'pao'; + case Papiamento = 'pap'; + case Parya = 'paq'; + case Panamint = 'par'; + case Papasena = 'pas'; + case Palauan = 'pau'; + case Pakaasnovos = 'pav'; + case Pawnee = 'paw'; + case Pankarare = 'pax'; + case Pech = 'pay'; + case Pankararu = 'paz'; + case Paez = 'pbb'; + case Patamona = 'pbc'; + case Mezontla_Popoloca = 'pbe'; + case Coyotepec_Popoloca = 'pbf'; + case Paraujano = 'pbg'; + case E_napa_Woromaipu = 'pbh'; + case Parkwa = 'pbi'; + case Mak_Nigeria = 'pbl'; + case Puebla_Mazatec = 'pbm'; + case Kpasam = 'pbn'; + case Papel = 'pbo'; + case Badyara = 'pbp'; + case Pangwa = 'pbr'; + case Central_Pame = 'pbs'; + case Southern_Pashto = 'pbt'; + case Northern_Pashto = 'pbu'; + case Pnar = 'pbv'; + case Pyu_Papua_New_Guinea = 'pby'; + case Santa_Ines_Ahuatempan_Popoloca = 'pca'; + case Pear = 'pcb'; + case Bouyei = 'pcc'; + case Picard = 'pcd'; + case Ruching_Palaung = 'pce'; + case Paliyan = 'pcf'; + case Paniya = 'pcg'; + case Pardhan = 'pch'; + case Duruwa = 'pci'; + case Parenga = 'pcj'; + case Paite_Chin = 'pck'; + case Pardhi = 'pcl'; + case Nigerian_Pidgin = 'pcm'; + case Piti = 'pcn'; + case Pacahuara = 'pcp'; + case Pyapun = 'pcw'; + case Anam = 'pda'; + case Pennsylvania_German = 'pdc'; + case Pa_Di = 'pdi'; + case Podena = 'pdn'; + case Padoe = 'pdo'; + case Plautdietsch = 'pdt'; + case Kayan = 'pdu'; + case Peranakan_Indonesian = 'pea'; + case Eastern_Pomo = 'peb'; + case Mala_Papua_New_Guinea = 'ped'; + case Taje = 'pee'; + case Northeastern_Pomo = 'pef'; + case Pengo = 'peg'; + case Bonan = 'peh'; + case Chichimeca_Jonaz = 'pei'; + case Northern_Pomo = 'pej'; + case Penchal = 'pek'; + case Pekal = 'pel'; + case Phende = 'pem'; + case Old_Persian_ca_600_400_B_C = 'peo'; + case Kunja = 'pep'; + case Southern_Pomo = 'peq'; + case Iranian_Persian = 'pes'; + case Pemono = 'pev'; + case Petats = 'pex'; + case Petjo = 'pey'; + case Eastern_Penan = 'pez'; + case Paafang = 'pfa'; + case Pere = 'pfe'; + case Pfaelzisch = 'pfl'; + case Sudanese_Creole_Arabic = 'pga'; + case Gandhari = 'pgd'; + case Pangwali = 'pgg'; + case Pagi = 'pgi'; + case Rerep = 'pgk'; + case Primitive_Irish = 'pgl'; + case Paelignian = 'pgn'; + case Pangseng = 'pgs'; + case Pagu = 'pgu'; + case Papua_New_Guinean_Sign_Language = 'pgz'; + case Pa_Hng = 'pha'; + case Phudagi = 'phd'; + case Phuong = 'phg'; + case Phukha = 'phh'; + case Pahari = 'phj'; + case Phake = 'phk'; + case Phalura = 'phl'; + case Phimbi = 'phm'; + case Phoenician = 'phn'; + case Phunoi = 'pho'; + case Phana = 'phq'; + case Pahari_Potwari = 'phr'; + case Phu_Thai = 'pht'; + case Phuan = 'phu'; + case Pahlavani = 'phv'; + case Phangduwali = 'phw'; + case Pima_Bajo = 'pia'; + case Yine = 'pib'; + case Pinji = 'pic'; + case Piaroa = 'pid'; + case Piro = 'pie'; + case Pingelapese = 'pif'; + case Pisabo = 'pig'; + case Pitcairn_Norfolk = 'pih'; + case Pijao = 'pij'; + case Yom = 'pil'; + case Powhatan = 'pim'; + case Piame = 'pin'; + case Piapoco = 'pio'; + case Pero = 'pip'; + case Piratapuyo = 'pir'; + case Pijin = 'pis'; + case Pitta_Pitta = 'pit'; + case Pintupi_Luritja = 'piu'; + case Pileni = 'piv'; + case Pimbwe = 'piw'; + case Piu = 'pix'; + case Piya_Kwonci = 'piy'; + case Pije = 'piz'; + case Pitjantjatjara = 'pjt'; + case Ardhamagadhi_Prakrit = 'pka'; + case Pokomo = 'pkb'; + case Paekche = 'pkc'; + case Pak_Tong = 'pkg'; + case Pankhu = 'pkh'; + case Pakanha = 'pkn'; + case Pokoot = 'pko'; + case Pukapuka = 'pkp'; + case Attapady_Kurumba = 'pkr'; + case Pakistan_Sign_Language = 'pks'; + case Maleng = 'pkt'; + case Paku = 'pku'; + case Miani = 'pla'; + case Polonombauk = 'plb'; + case Central_Palawano = 'plc'; + case Polari = 'pld'; + case Palu_e = 'ple'; + case Pilaga = 'plg'; + case Paulohi = 'plh'; + case Pali = 'pli'; + case Kohistani_Shina = 'plk'; + case Shwe_Palaung = 'pll'; + case Palenquero = 'pln'; + case Oluta_Popoluca = 'plo'; + case Palaic = 'plq'; + case Palaka_Senoufo = 'plr'; + case San_Marcos_Tlacoyalco_Popoloca = 'pls'; + case Plateau_Malagasy = 'plt'; + case Palikur = 'plu'; + case Southwest_Palawano = 'plv'; + case Brooke_s_Point_Palawano = 'plw'; + case Bolyu = 'ply'; + case Paluan = 'plz'; + case Paama = 'pma'; + case Pambia = 'pmb'; + case Pallanganmiddang = 'pmd'; + case Pwaamei = 'pme'; + case Pamona = 'pmf'; + case Maharastri_Prakrit = 'pmh'; + case Northern_Pumi = 'pmi'; + case Southern_Pumi = 'pmj'; + case Lingua_Franca = 'pml'; + case Pomo = 'pmm'; + case Pam = 'pmn'; + case Pom = 'pmo'; + case Northern_Pame = 'pmq'; + case Paynamar = 'pmr'; + case Piemontese = 'pms'; + case Tuamotuan = 'pmt'; + case Plains_Miwok = 'pmw'; + case Poumei_Naga = 'pmx'; + case Papuan_Malay = 'pmy'; + case Southern_Pame = 'pmz'; + case Punan_Bah_Biau = 'pna'; + case Western_Panjabi = 'pnb'; + case Pannei = 'pnc'; + case Mpinda = 'pnd'; + case Western_Penan = 'pne'; + case Pangu = 'png'; + case Penrhyn = 'pnh'; + case Aoheng = 'pni'; + case Pinjarup = 'pnj'; + case Paunaka = 'pnk'; + case Paleni = 'pnl'; + case Punan_Batu_1 = 'pnm'; + case Pinai_Hagahai = 'pnn'; + case Panobo = 'pno'; + case Pancana = 'pnp'; + case Pana_Burkina_Faso = 'pnq'; + case Panim = 'pnr'; + case Ponosakan = 'pns'; + case Pontic = 'pnt'; + case Jiongnai_Bunu = 'pnu'; + case Pinigura = 'pnv'; + case Banyjima = 'pnw'; + case Phong_Kniang = 'pnx'; + case Pinyin = 'pny'; + case Pana_Central_African_Republic = 'pnz'; + case Poqomam = 'poc'; + case San_Juan_Atzingo_Popoloca = 'poe'; + case Poke = 'pof'; + case Potiguara = 'pog'; + case Poqomchi = 'poh'; + case Highland_Popoluca = 'poi'; + case Pokanga = 'pok'; + case Polish = 'pol'; + case Southeastern_Pomo = 'pom'; + case Pohnpeian = 'pon'; + case Central_Pomo = 'poo'; + case Pwapwa = 'pop'; + case Texistepec_Popoluca = 'poq'; + case Portuguese = 'por'; + case Sayula_Popoluca = 'pos'; + case Potawatomi = 'pot'; + case Upper_Guinea_Crioulo = 'pov'; + case San_Felipe_Otlaltepec_Popoloca = 'pow'; + case Polabian = 'pox'; + case Pogolo = 'poy'; + case Papi = 'ppe'; + case Paipai = 'ppi'; + case Uma = 'ppk'; + case Pipil = 'ppl'; + case Papuma = 'ppm'; + case Papapana = 'ppn'; + case Folopa = 'ppo'; + case Pelende = 'ppp'; + case Pei = 'ppq'; + case San_Luis_Temalacayuca_Popoloca = 'pps'; + case Pare = 'ppt'; + case Papora = 'ppu'; + case Pa_a = 'pqa'; + case Malecite_Passamaquoddy = 'pqm'; + case Parachi = 'prc'; + case Parsi_Dari = 'prd'; + case Principense = 'pre'; + case Paranan = 'prf'; + case Prussian = 'prg'; + case Porohanon = 'prh'; + case Paici = 'pri'; + case Parauk = 'prk'; + case Peruvian_Sign_Language = 'prl'; + case Kibiri = 'prm'; + case Prasuni = 'prn'; + case Old_Provencal_to_1500 = 'pro'; + case Asheninka_Perene = 'prq'; + case Puri = 'prr'; + case Dari = 'prs'; + case Phai = 'prt'; + case Puragi = 'pru'; + case Parawen = 'prw'; + case Purik = 'prx'; + case Providencia_Sign_Language = 'prz'; + case Asue_Awyu = 'psa'; + case Iranian_Sign_Language = 'psc'; + case Plains_Indian_Sign_Language = 'psd'; + case Central_Malay = 'pse'; + case Penang_Sign_Language = 'psg'; + case Southwest_Pashai = 'psh'; + case Southeast_Pashai = 'psi'; + case Puerto_Rican_Sign_Language = 'psl'; + case Pauserna = 'psm'; + case Panasuan = 'psn'; + case Polish_Sign_Language = 'pso'; + case Philippine_Sign_Language = 'psp'; + case Pasi = 'psq'; + case Portuguese_Sign_Language = 'psr'; + case Kaulong = 'pss'; + case Central_Pashto = 'pst'; + case Sauraseni_Prakrit = 'psu'; + case Port_Sandwich = 'psw'; + case Piscataway = 'psy'; + case Pai_Tavytera = 'pta'; + case Pataxo_Ha_Ha_Hae = 'pth'; + case Pindiini = 'pti'; + case Patani = 'ptn'; + case Zo_e = 'pto'; + case Patep = 'ptp'; + case Pattapu = 'ptq'; + case Piamatsina = 'ptr'; + case Enrekang = 'ptt'; + case Bambam = 'ptu'; + case Port_Vato = 'ptv'; + case Pentlatch = 'ptw'; + case Pathiya = 'pty'; + case Western_Highland_Purepecha = 'pua'; + case Purum = 'pub'; + case Punan_Merap = 'puc'; + case Punan_Aput = 'pud'; + case Puelche = 'pue'; + case Punan_Merah = 'puf'; + case Phuie = 'pug'; + case Puinave = 'pui'; + case Punan_Tubu = 'puj'; + case Puma = 'pum'; + case Puoc = 'puo'; + case Pulabu = 'pup'; + case Puquina = 'puq'; + case Purubora = 'pur'; + case Pushto = 'pus'; + case Putoh = 'put'; + case Punu = 'puu'; + case Puluwatese = 'puw'; + case Puare = 'pux'; + case Purisimeno = 'puy'; + case Pawaia = 'pwa'; + case Panawa = 'pwb'; + case Gapapaiwa = 'pwg'; + case Patwin = 'pwi'; + case Molbog = 'pwm'; + case Paiwan = 'pwn'; + case Pwo_Western_Karen = 'pwo'; + case Powari = 'pwr'; + case Pwo_Northern_Karen = 'pww'; + case Quetzaltepec_Mixe = 'pxm'; + case Pye_Krumen = 'pye'; + case Fyam = 'pym'; + case Poyanawa = 'pyn'; + case Paraguayan_Sign_Language = 'pys'; + case Puyuma = 'pyu'; + case Pyu_Myanmar = 'pyx'; + case Pyen = 'pyy'; + case Pesse = 'pze'; + case Pazeh = 'pzh'; + case Jejara_Naga = 'pzn'; + case Quapaw = 'qua'; + case Huallaga_Huanuco_Quechua = 'qub'; + case K_iche = 'quc'; + case Calderon_Highland_Quichua = 'qud'; + case Quechua = 'que'; + case Lambayeque_Quechua = 'quf'; + case Chimborazo_Highland_Quichua = 'qug'; + case South_Bolivian_Quechua = 'quh'; + case Quileute = 'qui'; + case Chachapoyas_Quechua = 'quk'; + case North_Bolivian_Quechua = 'qul'; + case Sipacapense = 'qum'; + case Quinault = 'qun'; + case Southern_Pastaza_Quechua = 'qup'; + case Quinqui = 'quq'; + case Yanahuanca_Pasco_Quechua = 'qur'; + case Santiago_del_Estero_Quichua = 'qus'; + case Sacapulteco = 'quv'; + case Tena_Lowland_Quichua = 'quw'; + case Yauyos_Quechua = 'qux'; + case Ayacucho_Quechua = 'quy'; + case Cusco_Quechua = 'quz'; + case Ambo_Pasco_Quechua = 'qva'; + case Cajamarca_Quechua = 'qvc'; + case Eastern_Apurimac_Quechua = 'qve'; + case Huamalies_Dos_de_Mayo_Huanuco_Quechua = 'qvh'; + case Imbabura_Highland_Quichua = 'qvi'; + case Loja_Highland_Quichua = 'qvj'; + case Cajatambo_North_Lima_Quechua = 'qvl'; + case Margos_Yarowilca_Lauricocha_Quechua = 'qvm'; + case North_Junin_Quechua = 'qvn'; + case Napo_Lowland_Quechua = 'qvo'; + case Pacaraos_Quechua = 'qvp'; + case San_Martin_Quechua = 'qvs'; + case Huaylla_Wanca_Quechua = 'qvw'; + case Queyu = 'qvy'; + case Northern_Pastaza_Quichua = 'qvz'; + case Corongo_Ancash_Quechua = 'qwa'; + case Classical_Quechua = 'qwc'; + case Huaylas_Ancash_Quechua = 'qwh'; + case Kuman_Russia = 'qwm'; + case Sihuas_Ancash_Quechua = 'qws'; + case Kwalhioqua_Tlatskanai = 'qwt'; + case Chiquian_Ancash_Quechua = 'qxa'; + case Chincha_Quechua = 'qxc'; + case Panao_Huanuco_Quechua = 'qxh'; + case Salasaca_Highland_Quichua = 'qxl'; + case Northern_Conchucos_Ancash_Quechua = 'qxn'; + case Southern_Conchucos_Ancash_Quechua = 'qxo'; + case Puno_Quechua = 'qxp'; + case Qashqa_i = 'qxq'; + case Canar_Highland_Quichua = 'qxr'; + case Southern_Qiang = 'qxs'; + case Santa_Ana_de_Tusi_Pasco_Quechua = 'qxt'; + case Arequipa_La_Union_Quechua = 'qxu'; + case Jauja_Wanca_Quechua = 'qxw'; + case Quenya = 'qya'; + case Quiripi = 'qyp'; + case Dungmali = 'raa'; + case Camling = 'rab'; + case Rasawa = 'rac'; + case Rade = 'rad'; + case Western_Meohang = 'raf'; + case Logooli = 'rag'; + case Rabha = 'rah'; + case Ramoaaina = 'rai'; + case Rajasthani = 'raj'; + case Tulu_Bohuai = 'rak'; + case Ralte = 'ral'; + case Canela = 'ram'; + case Riantana = 'ran'; + case Rao = 'rao'; + case Rapanui = 'rap'; + case Saam = 'raq'; + case Rarotongan = 'rar'; + case Tegali = 'ras'; + case Razajerdi = 'rat'; + case Raute = 'rau'; + case Sampang = 'rav'; + case Rawang = 'raw'; + case Rang = 'rax'; + case Rapa = 'ray'; + case Rahambuu = 'raz'; + case Rumai_Palaung = 'rbb'; + case Northern_Bontok = 'rbk'; + case Miraya_Bikol = 'rbl'; + case Barababaraba = 'rbp'; + case Reunion_Creole_French = 'rcf'; + case Rudbari = 'rdb'; + case Rerau = 'rea'; + case Rembong = 'reb'; + case Rejang_Kayan = 'ree'; + case Kara_Tanzania = 'reg'; + case Reli = 'rei'; + case Rejang = 'rej'; + case Rendille = 'rel'; + case Remo = 'rem'; + case Rengao = 'ren'; + case Rer_Bare = 'rer'; + case Reshe = 'res'; + case Retta = 'ret'; + case Reyesano = 'rey'; + case Roria = 'rga'; + case Romano_Greek = 'rge'; + case Rangkas = 'rgk'; + case Romagnol = 'rgn'; + case Resigaro = 'rgr'; + case Southern_Roglai = 'rgs'; + case Ringgou = 'rgu'; + case Rohingya = 'rhg'; + case Yahang = 'rhp'; + case Riang_India = 'ria'; + case Bribri_Sign_Language = 'rib'; + case Tarifit = 'rif'; + case Riang_Lang = 'ril'; + case Nyaturu = 'rim'; + case Nungu = 'rin'; + case Ribun = 'rir'; + case Ritharrngu = 'rit'; + case Riung = 'riu'; + case Rajong = 'rjg'; + case Raji = 'rji'; + case Rajbanshi = 'rjs'; + case Kraol = 'rka'; + case Rikbaktsa = 'rkb'; + case Rakahanga_Manihiki = 'rkh'; + case Rakhine = 'rki'; + case Marka = 'rkm'; + case Rangpuri = 'rkt'; + case Arakwal = 'rkw'; + case Rama = 'rma'; + case Rembarrnga = 'rmb'; + case Carpathian_Romani = 'rmc'; + case Traveller_Danish = 'rmd'; + case Angloromani = 'rme'; + case Kalo_Finnish_Romani = 'rmf'; + case Traveller_Norwegian = 'rmg'; + case Murkim = 'rmh'; + case Lomavren = 'rmi'; + case Romkun = 'rmk'; + case Baltic_Romani = 'rml'; + case Roma = 'rmm'; + case Balkan_Romani = 'rmn'; + case Sinte_Romani = 'rmo'; + case Rempi = 'rmp'; + case Calo = 'rmq'; + case Romanian_Sign_Language = 'rms'; + case Domari = 'rmt'; + case Tavringer_Romani = 'rmu'; + case Romanova = 'rmv'; + case Welsh_Romani = 'rmw'; + case Romam = 'rmx'; + case Vlax_Romani = 'rmy'; + case Marma = 'rmz'; + case Brunca_Sign_Language = 'rnb'; + case Ruund = 'rnd'; + case Ronga = 'rng'; + case Ranglong = 'rnl'; + case Roon = 'rnn'; + case Rongpo = 'rnp'; + case Nari_Nari = 'rnr'; + case Rungwa = 'rnw'; + case Tae = 'rob'; + case Cacgia_Roglai = 'roc'; + case Rogo = 'rod'; + case Ronji = 'roe'; + case Rombo = 'rof'; + case Northern_Roglai = 'rog'; + case Romansh = 'roh'; + case Romblomanon = 'rol'; + case Romany = 'rom'; + case Romanian = 'ron'; + case Rotokas = 'roo'; + case Kriol = 'rop'; + case Rongga = 'ror'; + case Runga = 'rou'; + case Dela_Oenale = 'row'; + case Repanbitip = 'rpn'; + case Rapting = 'rpt'; + case Ririo = 'rri'; + case Moriori = 'rrm'; + case Waima = 'rro'; + case Arritinngithigh = 'rrt'; + case Romano_Serbian = 'rsb'; + case Ruthenian = 'rsk'; + case Russian_Sign_Language = 'rsl'; + case Miriwoong_Sign_Language = 'rsm'; + case Rwandan_Sign_Language = 'rsn'; + case Rishiwa = 'rsw'; + case Rungtu_Chin = 'rtc'; + case Ratahan = 'rth'; + case Rotuman = 'rtm'; + case Yurats = 'rts'; + case Rathawi = 'rtw'; + case Gungu = 'rub'; + case Ruuli = 'ruc'; + case Rusyn = 'rue'; + case Luguru = 'ruf'; + case Roviana = 'rug'; + case Ruga = 'ruh'; + case Rufiji = 'rui'; + case Che = 'ruk'; + case Rundi = 'run'; + case Istro_Romanian = 'ruo'; + case Macedo_Romanian = 'rup'; + case Megleno_Romanian = 'ruq'; + case Russian = 'rus'; + case Rutul = 'rut'; + case Lanas_Lobu = 'ruu'; + case Mala_Nigeria = 'ruy'; + case Ruma = 'ruz'; + case Rawo = 'rwa'; + case Rwa = 'rwk'; + case Ruwila = 'rwl'; + case Amba_Uganda = 'rwm'; + case Rawa = 'rwo'; + case Marwari_India = 'rwr'; + case Ngardi = 'rxd'; + case Karuwali = 'rxw'; + case Northern_Amami_Oshima = 'ryn'; + case Yaeyama = 'rys'; + case Central_Okinawan = 'ryu'; + case Razihi = 'rzh'; + case Saba = 'saa'; + case Buglere = 'sab'; + case Meskwaki = 'sac'; + case Sandawe = 'sad'; + case Sabane = 'sae'; + case Safaliba = 'saf'; + case Sango = 'sag'; + case Yakut = 'sah'; + case Sahu = 'saj'; + case Sake = 'sak'; + case Samaritan_Aramaic = 'sam'; + case Sanskrit = 'san'; + case Sause = 'sao'; + case Samburu = 'saq'; + case Saraveca = 'sar'; + case Sasak = 'sas'; + case Santali = 'sat'; + case Saleman = 'sau'; + case Saafi_Saafi = 'sav'; + case Sawi = 'saw'; + case Sa = 'sax'; + case Saya = 'say'; + case Saurashtra = 'saz'; + case Ngambay = 'sba'; + case Simbo = 'sbb'; + case Kele_Papua_New_Guinea = 'sbc'; + case Southern_Samo = 'sbd'; + case Saliba = 'sbe'; + case Chabu = 'sbf'; + case Seget = 'sbg'; + case Sori_Harengan = 'sbh'; + case Seti = 'sbi'; + case Surbakhal = 'sbj'; + case Safwa = 'sbk'; + case Botolan_Sambal = 'sbl'; + case Sagala = 'sbm'; + case Sindhi_Bhil = 'sbn'; + case Sabum = 'sbo'; + case Sangu_Tanzania = 'sbp'; + case Sileibi = 'sbq'; + case Sembakung_Murut = 'sbr'; + case Subiya = 'sbs'; + case Kimki = 'sbt'; + case Stod_Bhoti = 'sbu'; + case Sabine = 'sbv'; + case Simba = 'sbw'; + case Seberuang = 'sbx'; + case Soli = 'sby'; + case Sara_Kaba = 'sbz'; + case Chut = 'scb'; + case Dongxiang = 'sce'; + case San_Miguel_Creole_French = 'scf'; + case Sanggau = 'scg'; + case Sakachep = 'sch'; + case Sri_Lankan_Creole_Malay = 'sci'; + case Sadri = 'sck'; + case Shina = 'scl'; + case Sicilian = 'scn'; + case Scots = 'sco'; + case Hyolmo = 'scp'; + case Sa_och = 'scq'; + case North_Slavey = 'scs'; + case Southern_Katang = 'sct'; + case Shumcho = 'scu'; + case Sheni = 'scv'; + case Sha = 'scw'; + case Sicel = 'scx'; + case Toraja_Sa_dan = 'sda'; + case Shabak = 'sdb'; + case Sassarese_Sardinian = 'sdc'; + case Surubu = 'sde'; + case Sarli = 'sdf'; + case Savi = 'sdg'; + case Southern_Kurdish = 'sdh'; + case Suundi = 'sdj'; + case Sos_Kundi = 'sdk'; + case Saudi_Arabian_Sign_Language = 'sdl'; + case Gallurese_Sardinian = 'sdn'; + case Bukar_Sadung_Bidayuh = 'sdo'; + case Sherdukpen = 'sdp'; + case Semandang = 'sdq'; + case Oraon_Sadri = 'sdr'; + case Sened = 'sds'; + case Shuadit = 'sdt'; + case Sarudu = 'sdu'; + case Sibu_Melanau = 'sdx'; + case Sallands = 'sdz'; + case Semai = 'sea'; + case Shempire_Senoufo = 'seb'; + case Sechelt = 'sec'; + case Sedang = 'sed'; + case Seneca = 'see'; + case Cebaara_Senoufo = 'sef'; + case Segeju = 'seg'; + case Sena = 'seh'; + case Seri = 'sei'; + case Sene = 'sej'; + case Sekani = 'sek'; + case Selkup = 'sel'; + case Nanerige_Senoufo = 'sen'; + case Suarmin = 'seo'; + case Sicite_Senoufo = 'sep'; + case Senara_Senoufo = 'seq'; + case Serrano = 'ser'; + case Koyraboro_Senni_Songhai = 'ses'; + case Sentani = 'set'; + case Serui_Laut = 'seu'; + case Nyarafolo_Senoufo = 'sev'; + case Sewa_Bay = 'sew'; + case Secoya = 'sey'; + case Senthang_Chin = 'sez'; + case Langue_des_signes_de_Belgique_Francophone = 'sfb'; + case Eastern_Subanen = 'sfe'; + case Small_Flowery_Miao = 'sfm'; + case South_African_Sign_Language = 'sfs'; + case Sehwi = 'sfw'; + case Old_Irish_to_900 = 'sga'; + case Mag_antsi_Ayta = 'sgb'; + case Kipsigis = 'sgc'; + case Surigaonon = 'sgd'; + case Segai = 'sge'; + case Swiss_German_Sign_Language = 'sgg'; + case Shughni = 'sgh'; + case Suga = 'sgi'; + case Surgujia = 'sgj'; + case Sangkong = 'sgk'; + case Singa = 'sgm'; + case Singpho = 'sgp'; + case Sangisari = 'sgr'; + case Samogitian = 'sgs'; + case Brokpake = 'sgt'; + case Salas = 'sgu'; + case Sebat_Bet_Gurage = 'sgw'; + case Sierra_Leone_Sign_Language = 'sgx'; + case Sanglechi = 'sgy'; + case Sursurunga = 'sgz'; + case Shall_Zwall = 'sha'; + case Ninam = 'shb'; + case Sonde = 'shc'; + case Kundal_Shahi = 'shd'; + case Sheko = 'she'; + case Shua = 'shg'; + case Shoshoni = 'shh'; + case Tachelhit = 'shi'; + case Shatt = 'shj'; + case Shilluk = 'shk'; + case Shendu = 'shl'; + case Shahrudi = 'shm'; + case Shan = 'shn'; + case Shanga = 'sho'; + case Shipibo_Conibo = 'shp'; + case Sala = 'shq'; + case Shi = 'shr'; + case Shuswap = 'shs'; + case Shasta = 'sht'; + case Chadian_Arabic = 'shu'; + case Shehri = 'shv'; + case Shwai = 'shw'; + case She = 'shx'; + case Tachawit = 'shy'; + case Syenara_Senoufo = 'shz'; + case Akkala_Sami = 'sia'; + case Sebop = 'sib'; + case Sidamo = 'sid'; + case Simaa = 'sie'; + case Siamou = 'sif'; + case Paasaal = 'sig'; + case Zire = 'sih'; + case Shom_Peng = 'sii'; + case Numbami = 'sij'; + case Sikiana = 'sik'; + case Tumulung_Sisaala = 'sil'; + case Mende_Papua_New_Guinea = 'sim'; + case Sinhala = 'sin'; + case Sikkimese = 'sip'; + case Sonia = 'siq'; + case Siri = 'sir'; + case Siuslaw = 'sis'; + case Sinagen = 'siu'; + case Sumariup = 'siv'; + case Siwai = 'siw'; + case Sumau = 'six'; + case Sivandi = 'siy'; + case Siwi = 'siz'; + case Epena = 'sja'; + case Sajau_Basap = 'sjb'; + case Kildin_Sami = 'sjd'; + case Pite_Sami = 'sje'; + case Assangori = 'sjg'; + case Kemi_Sami = 'sjk'; + case Sajalong = 'sjl'; + case Mapun = 'sjm'; + case Sindarin = 'sjn'; + case Xibe = 'sjo'; + case Surjapuri = 'sjp'; + case Siar_Lak = 'sjr'; + case Senhaja_De_Srair = 'sjs'; + case Ter_Sami = 'sjt'; + case Ume_Sami = 'sju'; + case Shawnee = 'sjw'; + case Skagit = 'ska'; + case Saek = 'skb'; + case Ma_Manda = 'skc'; + case Southern_Sierra_Miwok = 'skd'; + case Seke_Vanuatu = 'ske'; + case Sakirabia = 'skf'; + case Sakalava_Malagasy = 'skg'; + case Sikule = 'skh'; + case Sika = 'ski'; + case Seke_Nepal = 'skj'; + case Kutong = 'skm'; + case Kolibugan_Subanon = 'skn'; + case Seko_Tengah = 'sko'; + case Sekapan = 'skp'; + case Sininkere = 'skq'; + case Saraiki = 'skr'; + case Maia = 'sks'; + case Sakata = 'skt'; + case Sakao = 'sku'; + case Skou = 'skv'; + case Skepi_Creole_Dutch = 'skw'; + case Seko_Padang = 'skx'; + case Sikaiana = 'sky'; + case Sekar = 'skz'; + case Saliba_2 = 'slc'; + case Sissala = 'sld'; + case Sholaga = 'sle'; + case Swiss_Italian_Sign_Language = 'slf'; + case Selungai_Murut = 'slg'; + case Southern_Puget_Sound_Salish = 'slh'; + case Lower_Silesian = 'sli'; + case Saluma = 'slj'; + case Slovak = 'slk'; + case Salt_Yui = 'sll'; + case Pangutaran_Sama = 'slm'; + case Salinan = 'sln'; + case Lamaholot = 'slp'; + case Salar = 'slr'; + case Singapore_Sign_Language = 'sls'; + case Sila = 'slt'; + case Selaru = 'slu'; + case Slovenian = 'slv'; + case Sialum = 'slw'; + case Salampasu = 'slx'; + case Selayar = 'sly'; + case Ma_ya = 'slz'; + case Southern_Sami = 'sma'; + case Simbari = 'smb'; + case Som = 'smc'; + case Northern_Sami = 'sme'; + case Auwe = 'smf'; + case Simbali = 'smg'; + case Samei = 'smh'; + case Lule_Sami = 'smj'; + case Bolinao = 'smk'; + case Central_Sama = 'sml'; + case Musasa = 'smm'; + case Inari_Sami = 'smn'; + case Samoan = 'smo'; + case Samaritan = 'smp'; + case Samo = 'smq'; + case Simeulue = 'smr'; + case Skolt_Sami = 'sms'; + case Simte = 'smt'; + case Somray = 'smu'; + case Samvedi = 'smv'; + case Sumbawa = 'smw'; + case Samba = 'smx'; + case Semnani = 'smy'; + case Simeku = 'smz'; + case Shona = 'sna'; + case Sinaugoro = 'snc'; + case Sindhi = 'snd'; + case Bau_Bidayuh = 'sne'; + case Noon = 'snf'; + case Sanga_Democratic_Republic_of_Congo = 'sng'; + case Sensi = 'sni'; + case Riverain_Sango = 'snj'; + case Soninke = 'snk'; + case Sangil = 'snl'; + case Southern_Ma_di = 'snm'; + case Siona = 'snn'; + case Snohomish = 'sno'; + case Siane = 'snp'; + case Sangu_Gabon = 'snq'; + case Sihan = 'snr'; + case South_West_Bay = 'sns'; + case Senggi = 'snu'; + case Sa_ban = 'snv'; + case Selee = 'snw'; + case Sam = 'snx'; + case Saniyo_Hiyewe = 'sny'; + case Kou = 'snz'; + case Thai_Song = 'soa'; + case Sobei = 'sob'; + case So_Democratic_Republic_of_Congo = 'soc'; + case Songoora = 'sod'; + case Songomeno = 'soe'; + case Sogdian = 'sog'; + case Aka = 'soh'; + case Sonha = 'soi'; + case Soi = 'soj'; + case Sokoro = 'sok'; + case Solos = 'sol'; + case Somali = 'som'; + case Songo = 'soo'; + case Songe = 'sop'; + case Kanasi = 'soq'; + case Somrai = 'sor'; + case Seeku = 'sos'; + case Southern_Sotho = 'sot'; + case Southern_Thai = 'sou'; + case Sonsorol = 'sov'; + case Sowanda = 'sow'; + case Swo = 'sox'; + case Miyobe = 'soy'; + case Temi = 'soz'; + case Spanish = 'spa'; + case Sepa_Indonesia = 'spb'; + case Sape = 'spc'; + case Saep = 'spd'; + case Sepa_Papua_New_Guinea = 'spe'; + case Sian = 'spg'; + case Saponi = 'spi'; + case Sengo = 'spk'; + case Selepet = 'spl'; + case Akukem = 'spm'; + case Sanapana = 'spn'; + case Spokane = 'spo'; + case Supyire_Senoufo = 'spp'; + case Loreto_Ucayali_Spanish = 'spq'; + case Saparua = 'spr'; + case Saposa = 'sps'; + case Spiti_Bhoti = 'spt'; + case Sapuan = 'spu'; + case Sambalpuri = 'spv'; + case South_Picene = 'spx'; + case Sabaot = 'spy'; + case Shama_Sambuga = 'sqa'; + case Shau = 'sqh'; + case Albanian = 'sqi'; + case Albanian_Sign_Language = 'sqk'; + case Suma = 'sqm'; + case Susquehannock = 'sqn'; + case Sorkhei = 'sqo'; + case Sou = 'sqq'; + case Siculo_Arabic = 'sqr'; + case Sri_Lankan_Sign_Language = 'sqs'; + case Soqotri = 'sqt'; + case Squamish = 'squ'; + case Kufr_Qassem_Sign_Language_KQSL = 'sqx'; + case Saruga = 'sra'; + case Sora = 'srb'; + case Logudorese_Sardinian = 'src'; + case Sardinian = 'srd'; + case Sara = 'sre'; + case Nafi = 'srf'; + case Sulod = 'srg'; + case Sarikoli = 'srh'; + case Siriano = 'sri'; + case Serudung_Murut = 'srk'; + case Isirawa = 'srl'; + case Saramaccan = 'srm'; + case Sranan_Tongo = 'srn'; + case Campidanese_Sardinian = 'sro'; + case Serbian = 'srp'; + case Siriono = 'srq'; + case Serer = 'srr'; + case Sarsi = 'srs'; + case Sauri = 'srt'; + case Surui = 'sru'; + case Southern_Sorsoganon = 'srv'; + case Serua = 'srw'; + case Sirmauri = 'srx'; + case Sera = 'sry'; + case Shahmirzadi = 'srz'; + case Southern_Sama = 'ssb'; + case Suba_Simbiti = 'ssc'; + case Siroi = 'ssd'; + case Balangingi = 'sse'; + case Thao = 'ssf'; + case Seimat = 'ssg'; + case Shihhi_Arabic = 'ssh'; + case Sansi = 'ssi'; + case Sausi = 'ssj'; + case Sunam = 'ssk'; + case Western_Sisaala = 'ssl'; + case Semnam = 'ssm'; + case Waata = 'ssn'; + case Sissano = 'sso'; + case Spanish_Sign_Language = 'ssp'; + case So_a = 'ssq'; + case Swiss_French_Sign_Language = 'ssr'; + case So = 'sss'; + case Sinasina = 'sst'; + case Susuami = 'ssu'; + case Shark_Bay = 'ssv'; + case Swati = 'ssw'; + case Samberigi = 'ssx'; + case Saho = 'ssy'; + case Sengseng = 'ssz'; + case Settla = 'sta'; + case Northern_Subanen = 'stb'; + case Sentinel = 'std'; + case Liana_Seti = 'ste'; + case Seta = 'stf'; + case Trieng = 'stg'; + case Shelta = 'sth'; + case Bulo_Stieng = 'sti'; + case Matya_Samo = 'stj'; + case Arammba = 'stk'; + case Stellingwerfs = 'stl'; + case Setaman = 'stm'; + case Owa = 'stn'; + case Stoney = 'sto'; + case Southeastern_Tepehuan = 'stp'; + case Saterfriesisch = 'stq'; + case Straits_Salish = 'str'; + case Shumashti = 'sts'; + case Budeh_Stieng = 'stt'; + case Samtao = 'stu'; + case Silt_e = 'stv'; + case Satawalese = 'stw'; + case Siberian_Tatar = 'sty'; + case Sulka = 'sua'; + case Suku = 'sub'; + case Western_Subanon = 'suc'; + case Suena = 'sue'; + case Suganga = 'sug'; + case Suki = 'sui'; + case Shubi = 'suj'; + case Sukuma = 'suk'; + case Sundanese = 'sun'; + case Bouni = 'suo'; + case Tirmaga_Chai_Suri = 'suq'; + case Mwaghavul = 'sur'; + case Susu = 'sus'; + case Subtiaba = 'sut'; + case Puroik = 'suv'; + case Sumbwa = 'suw'; + case Sumerian = 'sux'; + case Suya = 'suy'; + case Sunwar = 'suz'; + case Svan = 'sva'; + case Ulau_Suain = 'svb'; + case Vincentian_Creole_English = 'svc'; + case Serili = 'sve'; + case Slovakian_Sign_Language = 'svk'; + case Slavomolisano = 'svm'; + case Savosavo = 'svs'; + case Skalvian = 'svx'; + case Swahili_macrolanguage = 'swa'; + case Maore_Comorian = 'swb'; + case Congo_Swahili = 'swc'; + case Swedish = 'swe'; + case Sere = 'swf'; + case Swabian = 'swg'; + case Swahili_individual_language = 'swh'; + case Sui = 'swi'; + case Sira = 'swj'; + case Malawi_Sena = 'swk'; + case Swedish_Sign_Language = 'swl'; + case Samosa = 'swm'; + case Sawknah = 'swn'; + case Shanenawa = 'swo'; + case Suau = 'swp'; + case Sharwa = 'swq'; + case Saweru = 'swr'; + case Seluwasan = 'sws'; + case Sawila = 'swt'; + case Suwawa = 'swu'; + case Shekhawati = 'swv'; + case Sowa = 'sww'; + case Suruaha = 'swx'; + case Sarua = 'swy'; + case Suba = 'sxb'; + case Sicanian = 'sxc'; + case Sighu = 'sxe'; + case Shuhi = 'sxg'; + case Southern_Kalapuya = 'sxk'; + case Selian = 'sxl'; + case Samre = 'sxm'; + case Sangir = 'sxn'; + case Sorothaptic = 'sxo'; + case Saaroa = 'sxr'; + case Sasaru = 'sxs'; + case Upper_Saxon = 'sxu'; + case Saxwe_Gbe = 'sxw'; + case Siang = 'sya'; + case Central_Subanen = 'syb'; + case Classical_Syriac = 'syc'; + case Seki = 'syi'; + case Sukur = 'syk'; + case Sylheti = 'syl'; + case Maya_Samo = 'sym'; + case Senaya = 'syn'; + case Suoy = 'syo'; + case Syriac = 'syr'; + case Sinyar = 'sys'; + case Kagate = 'syw'; + case Samay = 'syx'; + case Al_Sayyid_Bedouin_Sign_Language = 'syy'; + case Semelai = 'sza'; + case Ngalum = 'szb'; + case Semaq_Beri = 'szc'; + case Seze = 'sze'; + case Sengele = 'szg'; + case Silesian = 'szl'; + case Sula = 'szn'; + case Suabo = 'szp'; + case Solomon_Islands_Sign_Language = 'szs'; + case Isu_Fako_Division = 'szv'; + case Sawai = 'szw'; + case Sakizaya = 'szy'; + case Lower_Tanana = 'taa'; + case Tabassaran = 'tab'; + case Lowland_Tarahumara = 'tac'; + case Tause = 'tad'; + case Tariana = 'tae'; + case Tapirape = 'taf'; + case Tagoi = 'tag'; + case Tahitian = 'tah'; + case Eastern_Tamang = 'taj'; + case Tala = 'tak'; + case Tal = 'tal'; + case Tamil = 'tam'; + case Tangale = 'tan'; + case Yami = 'tao'; + case Taabwa = 'tap'; + case Tamasheq = 'taq'; + case Central_Tarahumara = 'tar'; + case Tay_Boi = 'tas'; + case Tatar = 'tat'; + case Upper_Tanana = 'tau'; + case Tatuyo = 'tav'; + case Tai = 'taw'; + case Tamki = 'tax'; + case Atayal = 'tay'; + case Tocho = 'taz'; + case Aikana = 'tba'; + case Takia = 'tbc'; + case Kaki_Ae = 'tbd'; + case Tanimbili = 'tbe'; + case Mandara = 'tbf'; + case North_Tairora = 'tbg'; + case Dharawal = 'tbh'; + case Gaam = 'tbi'; + case Tiang = 'tbj'; + case Calamian_Tagbanwa = 'tbk'; + case Tboli = 'tbl'; + case Tagbu = 'tbm'; + case Barro_Negro_Tunebo = 'tbn'; + case Tawala = 'tbo'; + case Taworta = 'tbp'; + case Tumtum = 'tbr'; + case Tanguat = 'tbs'; + case Tembo_Kitembo = 'tbt'; + case Tubar = 'tbu'; + case Tobo = 'tbv'; + case Tagbanwa = 'tbw'; + case Kapin = 'tbx'; + case Tabaru = 'tby'; + case Ditammari = 'tbz'; + case Ticuna = 'tca'; + case Tanacross = 'tcb'; + case Datooga = 'tcc'; + case Tafi = 'tcd'; + case Southern_Tutchone = 'tce'; + case Malinaltepec_Me_phaa = 'tcf'; + case Tamagario = 'tcg'; + case Turks_And_Caicos_Creole_English = 'tch'; + case Wara = 'tci'; + case Tchitchege = 'tck'; + case Taman_Myanmar = 'tcl'; + case Tanahmerah = 'tcm'; + case Tichurong = 'tcn'; + case Taungyo = 'tco'; + case Tawr_Chin = 'tcp'; + case Kaiy = 'tcq'; + case Torres_Strait_Creole = 'tcs'; + case T_en = 'tct'; + case Southeastern_Tarahumara = 'tcu'; + case Tecpatlan_Totonac = 'tcw'; + case Toda = 'tcx'; + case Tulu = 'tcy'; + case Thado_Chin = 'tcz'; + case Tagdal = 'tda'; + case Panchpargania = 'tdb'; + case Embera_Tado = 'tdc'; + case Tai_Nua = 'tdd'; + case Tiranige_Diga_Dogon = 'tde'; + case Talieng = 'tdf'; + case Western_Tamang = 'tdg'; + case Thulung = 'tdh'; + case Tomadino = 'tdi'; + case Tajio = 'tdj'; + case Tambas = 'tdk'; + case Sur = 'tdl'; + case Taruma = 'tdm'; + case Tondano = 'tdn'; + case Teme = 'tdo'; + case Tita = 'tdq'; + case Todrah = 'tdr'; + case Doutai = 'tds'; + case Tetun_Dili = 'tdt'; + case Toro = 'tdv'; + case Tandroy_Mahafaly_Malagasy = 'tdx'; + case Tadyawan = 'tdy'; + case Temiar = 'tea'; + case Tetete = 'teb'; + case Terik = 'tec'; + case Tepo_Krumen = 'ted'; + case Huehuetla_Tepehua = 'tee'; + case Teressa = 'tef'; + case Teke_Tege = 'teg'; + case Tehuelche = 'teh'; + case Torricelli = 'tei'; + case Ibali_Teke = 'tek'; + case Telugu = 'tel'; + case Timne = 'tem'; + case Tama_Colombia = 'ten'; + case Teso = 'teo'; + case Tepecano = 'tep'; + case Temein = 'teq'; + case Tereno = 'ter'; + case Tengger = 'tes'; + case Tetum = 'tet'; + case Soo = 'teu'; + case Teor = 'tev'; + case Tewa_USA = 'tew'; + case Tennet = 'tex'; + case Tulishi = 'tey'; + case Tetserret = 'tez'; + case Tofin_Gbe = 'tfi'; + case Tanaina = 'tfn'; + case Tefaro = 'tfo'; + case Teribe = 'tfr'; + case Ternate = 'tft'; + case Sagalla = 'tga'; + case Tobilung = 'tgb'; + case Tigak = 'tgc'; + case Ciwogai = 'tgd'; + case Eastern_Gorkha_Tamang = 'tge'; + case Chalikha = 'tgf'; + case Tobagonian_Creole_English = 'tgh'; + case Lawunuia = 'tgi'; + case Tagin = 'tgj'; + case Tajik = 'tgk'; + case Tagalog = 'tgl'; + case Tandaganon = 'tgn'; + case Sudest = 'tgo'; + case Tangoa = 'tgp'; + case Tring = 'tgq'; + case Tareng = 'tgr'; + case Nume = 'tgs'; + case Central_Tagbanwa = 'tgt'; + case Tanggu = 'tgu'; + case Tingui_Boto = 'tgv'; + case Tagwana_Senoufo = 'tgw'; + case Tagish = 'tgx'; + case Togoyo = 'tgy'; + case Tagalaka = 'tgz'; + case Thai = 'tha'; + case Kuuk_Thaayorre = 'thd'; + case Chitwania_Tharu = 'the'; + case Thangmi = 'thf'; + case Northern_Tarahumara = 'thh'; + case Tai_Long = 'thi'; + case Tharaka = 'thk'; + case Dangaura_Tharu = 'thl'; + case Aheu = 'thm'; + case Thachanadan = 'thn'; + case Thompson = 'thp'; + case Kochila_Tharu = 'thq'; + case Rana_Tharu = 'thr'; + case Thakali = 'ths'; + case Tahltan = 'tht'; + case Thuri = 'thu'; + case Tahaggart_Tamahaq = 'thv'; + case Tha = 'thy'; + case Tayart_Tamajeq = 'thz'; + case Tidikelt_Tamazight = 'tia'; + case Tira = 'tic'; + case Tifal = 'tif'; + case Tigre = 'tig'; + case Timugon_Murut = 'tih'; + case Tiene = 'tii'; + case Tilung = 'tij'; + case Tikar = 'tik'; + case Tillamook = 'til'; + case Timbe = 'tim'; + case Tindi = 'tin'; + case Teop = 'tio'; + case Trimuris = 'tip'; + case Tiefo = 'tiq'; + case Tigrinya = 'tir'; + case Masadiit_Itneg = 'tis'; + case Tinigua = 'tit'; + case Adasen = 'tiu'; + case Tiv = 'tiv'; + case Tiwi = 'tiw'; + case Southern_Tiwa = 'tix'; + case Tiruray = 'tiy'; + case Tai_Hongjin = 'tiz'; + case Tajuasohn = 'tja'; + case Tunjung = 'tjg'; + case Northern_Tujia = 'tji'; + case Tjungundji = 'tjj'; + case Tai_Laing = 'tjl'; + case Timucua = 'tjm'; + case Tonjon = 'tjn'; + case Temacine_Tamazight = 'tjo'; + case Tjupany = 'tjp'; + case Southern_Tujia = 'tjs'; + case Tjurruru = 'tju'; + case Djabwurrung = 'tjw'; + case Truka = 'tka'; + case Buksa = 'tkb'; + case Tukudede = 'tkd'; + case Takwane = 'tke'; + case Tukumanfed = 'tkf'; + case Tesaka_Malagasy = 'tkg'; + case Tokelau = 'tkl'; + case Takelma = 'tkm'; + case Toku_No_Shima = 'tkn'; + case Tikopia = 'tkp'; + case Tee = 'tkq'; + case Tsakhur = 'tkr'; + case Takestani = 'tks'; + case Kathoriya_Tharu = 'tkt'; + case Upper_Necaxa_Totonac = 'tku'; + case Mur_Pano = 'tkv'; + case Teanu = 'tkw'; + case Tangko = 'tkx'; + case Takua = 'tkz'; + case Southwestern_Tepehuan = 'tla'; + case Tobelo = 'tlb'; + case Yecuatla_Totonac = 'tlc'; + case Talaud = 'tld'; + case Telefol = 'tlf'; + case Tofanma = 'tlg'; + case Klingon = 'tlh'; + case Tlingit = 'tli'; + case Talinga_Bwisi = 'tlj'; + case Taloki = 'tlk'; + case Tetela = 'tll'; + case Tolomako = 'tlm'; + case Talondo = 'tln'; + case Talodi = 'tlo'; + case Filomena_Mata_Coahuitlan_Totonac = 'tlp'; + case Tai_Loi = 'tlq'; + case Talise = 'tlr'; + case Tambotalo = 'tls'; + case Sou_Nama = 'tlt'; + case Tulehu = 'tlu'; + case Taliabu = 'tlv'; + case Khehek = 'tlx'; + case Talysh = 'tly'; + case Tama_Chad = 'tma'; + case Katbol = 'tmb'; + case Tumak = 'tmc'; + case Haruai = 'tmd'; + case Tremembe = 'tme'; + case Toba_Maskoy = 'tmf'; + case Ternateno = 'tmg'; + case Tamashek = 'tmh'; + case Tutuba = 'tmi'; + case Samarokena = 'tmj'; + case Tamnim_Citak = 'tml'; + case Tai_Thanh = 'tmm'; + case Taman_Indonesia = 'tmn'; + case Temoq = 'tmo'; + case Tumleo = 'tmq'; + case Jewish_Babylonian_Aramaic_ca_200_1200_CE = 'tmr'; + case Tima = 'tms'; + case Tasmate = 'tmt'; + case Iau = 'tmu'; + case Tembo_Motembo = 'tmv'; + case Temuan = 'tmw'; + case Tami = 'tmy'; + case Tamanaku = 'tmz'; + case Tacana = 'tna'; + case Western_Tunebo = 'tnb'; + case Tanimuca_Retuara = 'tnc'; + case Angosturas_Tunebo = 'tnd'; + case Tobanga = 'tng'; + case Maiani = 'tnh'; + case Tandia = 'tni'; + case Kwamera = 'tnk'; + case Lenakel = 'tnl'; + case Tabla = 'tnm'; + case North_Tanna = 'tnn'; + case Toromono = 'tno'; + case Whitesands = 'tnp'; + case Taino = 'tnq'; + case Menik = 'tnr'; + case Tenis = 'tns'; + case Tontemboan = 'tnt'; + case Tay_Khang = 'tnu'; + case Tangchangya = 'tnv'; + case Tonsawang = 'tnw'; + case Tanema = 'tnx'; + case Tongwe = 'tny'; + case Ten_edn = 'tnz'; + case Toba = 'tob'; + case Coyutla_Totonac = 'toc'; + case Toma = 'tod'; + case Gizrra = 'tof'; + case Tonga_Nyasa = 'tog'; + case Gitonga = 'toh'; + case Tonga_Zambia = 'toi'; + case Tojolabal = 'toj'; + case Toki_Pona = 'tok'; + case Tolowa = 'tol'; + case Tombulu = 'tom'; + case Tonga_Tonga_Islands = 'ton'; + case Xicotepec_De_Juarez_Totonac = 'too'; + case Papantla_Totonac = 'top'; + case Toposa = 'toq'; + case Togbo_Vara_Banda = 'tor'; + case Highland_Totonac = 'tos'; + case Tho = 'tou'; + case Upper_Taromi = 'tov'; + case Jemez = 'tow'; + case Tobian = 'tox'; + case Topoiyo = 'toy'; + case To = 'toz'; + case Taupota = 'tpa'; + case Azoyu_Me_phaa = 'tpc'; + case Tippera = 'tpe'; + case Tarpia = 'tpf'; + case Kula = 'tpg'; + case Tok_Pisin = 'tpi'; + case Tapiete = 'tpj'; + case Tupinikin = 'tpk'; + case Tlacoapa_Me_phaa = 'tpl'; + case Tampulma = 'tpm'; + case Tupinamba = 'tpn'; + case Tai_Pao = 'tpo'; + case Pisaflores_Tepehua = 'tpp'; + case Tukpa = 'tpq'; + case Tupari = 'tpr'; + case Tlachichilco_Tepehua = 'tpt'; + case Tampuan = 'tpu'; + case Tanapag = 'tpv'; + case Acatepec_Me_phaa = 'tpx'; + case Trumai = 'tpy'; + case Tinputz = 'tpz'; + case Tembe = 'tqb'; + case Lehali = 'tql'; + case Turumsa = 'tqm'; + case Tenino = 'tqn'; + case Toaripi = 'tqo'; + case Tomoip = 'tqp'; + case Tunni = 'tqq'; + case Torona = 'tqr'; + case Western_Totonac = 'tqt'; + case Touo = 'tqu'; + case Tonkawa = 'tqw'; + case Tirahi = 'tra'; + case Terebu = 'trb'; + case Copala_Triqui = 'trc'; + case Turi = 'trd'; + case East_Tarangan = 'tre'; + case Trinidadian_Creole_English = 'trf'; + case Lishan_Didan = 'trg'; + case Turaka = 'trh'; + case Trio = 'tri'; + case Toram = 'trj'; + case Traveller_Scottish = 'trl'; + case Tregami = 'trm'; + case Trinitario = 'trn'; + case Tarao_Naga = 'tro'; + case Kok_Borok = 'trp'; + case San_Martin_Itunyoso_Triqui = 'trq'; + case Taushiro = 'trr'; + case Chicahuaxtla_Triqui = 'trs'; + case Tunggare = 'trt'; + case Turoyo = 'tru'; + case Sediq = 'trv'; + case Torwali = 'trw'; + case Tringgus_Sembaan_Bidayuh = 'trx'; + case Turung = 'try'; + case Tora = 'trz'; + case Tsaangi = 'tsa'; + case Tsamai = 'tsb'; + case Tswa = 'tsc'; + case Tsakonian = 'tsd'; + case Tunisian_Sign_Language = 'tse'; + case Tausug = 'tsg'; + case Tsuvan = 'tsh'; + case Tsimshian = 'tsi'; + case Tshangla = 'tsj'; + case Tseku = 'tsk'; + case Ts_un_Lao = 'tsl'; + case Turkish_Sign_Language = 'tsm'; + case Tswana = 'tsn'; + case Tsonga = 'tso'; + case Northern_Toussian = 'tsp'; + case Thai_Sign_Language = 'tsq'; + case Akei = 'tsr'; + case Taiwan_Sign_Language = 'tss'; + case Tondi_Songway_Kiini = 'tst'; + case Tsou = 'tsu'; + case Tsogo = 'tsv'; + case Tsishingini = 'tsw'; + case Mubami = 'tsx'; + case Tebul_Sign_Language = 'tsy'; + case Purepecha = 'tsz'; + case Tutelo = 'tta'; + case Gaa = 'ttb'; + case Tektiteko = 'ttc'; + case Tauade = 'ttd'; + case Bwanabwana = 'tte'; + case Tuotomb = 'ttf'; + case Tutong = 'ttg'; + case Upper_Ta_oih = 'tth'; + case Tobati = 'tti'; + case Tooro = 'ttj'; + case Totoro = 'ttk'; + case Totela = 'ttl'; + case Northern_Tutchone = 'ttm'; + case Towei = 'ttn'; + case Lower_Ta_oih = 'tto'; + case Tombelala = 'ttp'; + case Tawallammat_Tamajaq = 'ttq'; + case Tera = 'ttr'; + case Northeastern_Thai = 'tts'; + case Muslim_Tat = 'ttt'; + case Torau = 'ttu'; + case Titan = 'ttv'; + case Long_Wat = 'ttw'; + case Sikaritai = 'tty'; + case Tsum = 'ttz'; + case Wiarumus = 'tua'; + case Tubatulabal = 'tub'; + case Mutu = 'tuc'; + case Tuxa = 'tud'; + case Tuyuca = 'tue'; + case Central_Tunebo = 'tuf'; + case Tunia = 'tug'; + case Taulil = 'tuh'; + case Tupuri = 'tui'; + case Tugutil = 'tuj'; + case Turkmen = 'tuk'; + case Tula = 'tul'; + case Tumbuka = 'tum'; + case Tunica = 'tun'; + case Tucano = 'tuo'; + case Tedaga = 'tuq'; + case Turkish = 'tur'; + case Tuscarora = 'tus'; + case Tututni = 'tuu'; + case Turkana = 'tuv'; + case Tuxinawa = 'tux'; + case Tugen = 'tuy'; + case Turka = 'tuz'; + case Vaghua = 'tva'; + case Tsuvadi = 'tvd'; + case Te_un = 'tve'; + case Tulai = 'tvi'; + case Southeast_Ambrym = 'tvk'; + case Tuvalu = 'tvl'; + case Tela_Masbuar = 'tvm'; + case Tavoyan = 'tvn'; + case Tidore = 'tvo'; + case Taveta = 'tvs'; + case Tutsa_Naga = 'tvt'; + case Tunen = 'tvu'; + case Sedoa = 'tvw'; + case Taivoan = 'tvx'; + case Timor_Pidgin = 'tvy'; + case Twana = 'twa'; + case Western_Tawbuid = 'twb'; + case Teshenawa = 'twc'; + case Twents = 'twd'; + case Tewa_Indonesia = 'twe'; + case Northern_Tiwa = 'twf'; + case Tereweng = 'twg'; + case Tai_Don = 'twh'; + case Twi = 'twi'; + case Tawara = 'twl'; + case Tawang_Monpa = 'twm'; + case Twendi = 'twn'; + case Tswapong = 'two'; + case Ere = 'twp'; + case Tasawaq = 'twq'; + case Southwestern_Tarahumara = 'twr'; + case Turiwara = 'twt'; + case Termanu = 'twu'; + case Tuwari = 'tww'; + case Tewe = 'twx'; + case Tawoyan = 'twy'; + case Tombonuo = 'txa'; + case Tokharian_B = 'txb'; + case Tsetsaut = 'txc'; + case Totoli = 'txe'; + case Tangut = 'txg'; + case Thracian = 'txh'; + case Ikpeng = 'txi'; + case Tarjumo = 'txj'; + case Tomini = 'txm'; + case West_Tarangan = 'txn'; + case Toto = 'txo'; + case Tii = 'txq'; + case Tartessian = 'txr'; + case Tonsea = 'txs'; + case Citak = 'txt'; + case Kayapo = 'txu'; + case Tatana = 'txx'; + case Tanosy_Malagasy = 'txy'; + case Tauya = 'tya'; + case Kyanga = 'tye'; + case O_du = 'tyh'; + case Teke_Tsaayi = 'tyi'; + case Tai_Do = 'tyj'; + case Thu_Lao = 'tyl'; + case Kombai = 'tyn'; + case Thaypan = 'typ'; + case Tai_Daeng = 'tyr'; + case Tay_Sa_Pa = 'tys'; + case Tay_Tac = 'tyt'; + case Kua = 'tyu'; + case Tuvinian = 'tyv'; + case Teke_Tyee = 'tyx'; + case Tiyaa = 'tyy'; + case Tay = 'tyz'; + case Tanzanian_Sign_Language = 'tza'; + case Tzeltal = 'tzh'; + case Tz_utujil = 'tzj'; + case Talossan = 'tzl'; + case Central_Atlas_Tamazight = 'tzm'; + case Tugun = 'tzn'; + case Tzotzil = 'tzo'; + case Tabriak = 'tzx'; + case Uamue = 'uam'; + case Kuan = 'uan'; + case Tairuma = 'uar'; + case Ubang = 'uba'; + case Ubi = 'ubi'; + case Buhi_non_Bikol = 'ubl'; + case Ubir = 'ubr'; + case Umbu_Ungu = 'ubu'; + case Ubykh = 'uby'; + case Uda = 'uda'; + case Udihe = 'ude'; + case Muduga = 'udg'; + case Udi = 'udi'; + case Ujir = 'udj'; + case Wuzlam = 'udl'; + case Udmurt = 'udm'; + case Uduk = 'udu'; + case Kioko = 'ues'; + case Ufim = 'ufi'; + case Ugaritic = 'uga'; + case Kuku_Ugbanh = 'ugb'; + case Ughele = 'uge'; + case Kubachi = 'ugh'; + case Ugandan_Sign_Language = 'ugn'; + case Ugong = 'ugo'; + case Uruguayan_Sign_Language = 'ugy'; + case Uhami = 'uha'; + case Damal = 'uhn'; + case Uighur = 'uig'; + case Uisai = 'uis'; + case Iyive = 'uiv'; + case Tanjijili = 'uji'; + case Kaburi = 'uka'; + case Ukuriguma = 'ukg'; + case Ukhwejo = 'ukh'; + case Kui_India = 'uki'; + case Muak_Sa_aak = 'ukk'; + case Ukrainian_Sign_Language = 'ukl'; + case Ukpe_Bayobiri = 'ukp'; + case Ukwa = 'ukq'; + case Ukrainian = 'ukr'; + case Urubu_Kaapor_Sign_Language = 'uks'; + case Ukue = 'uku'; + case Kuku = 'ukv'; + case Ukwuani_Aboh_Ndoni = 'ukw'; + case Kuuk_Yak = 'uky'; + case Fungwa = 'ula'; + case Ulukwumi = 'ulb'; + case Ulch = 'ulc'; + case Lule = 'ule'; + case Usku = 'ulf'; + case Ulithian = 'uli'; + case Meriam_Mir = 'ulk'; + case Ullatan = 'ull'; + case Ulumanda = 'ulm'; + case Unserdeutsch = 'uln'; + case Uma_Lung = 'ulu'; + case Ulwa = 'ulw'; + case Buli = 'uly'; + case Umatilla = 'uma'; + case Umbundu = 'umb'; + case Marrucinian = 'umc'; + case Umbindhamu = 'umd'; + case Morrobalama = 'umg'; + case Ukit = 'umi'; + case Umon = 'umm'; + case Makyan_Naga = 'umn'; + case Umotina = 'umo'; + case Umpila = 'ump'; + case Umbugarla = 'umr'; + case Pendau = 'ums'; + case Munsee = 'umu'; + case North_Watut = 'una'; + case Undetermined = 'und'; + case Uneme = 'une'; + case Ngarinyin = 'ung'; + case Uni = 'uni'; + case Enawene_Nawe = 'unk'; + case Unami = 'unm'; + case Kurnai = 'unn'; + case Mundari = 'unr'; + case Unubahe = 'unu'; + case Munda = 'unx'; + case Unde_Kaili = 'unz'; + case Kulon = 'uon'; + case Umeda = 'upi'; + case Uripiv_Wala_Rano_Atchin = 'upv'; + case Urarina = 'ura'; + case Urubu_Kaapor = 'urb'; + case Urningangg = 'urc'; + case Urdu = 'urd'; + case Uru = 'ure'; + case Uradhi = 'urf'; + case Urigina = 'urg'; + case Urhobo = 'urh'; + case Urim = 'uri'; + case Urak_Lawoi = 'urk'; + case Urali = 'url'; + case Urapmin = 'urm'; + case Uruangnirin = 'urn'; + case Ura_Papua_New_Guinea = 'uro'; + case Uru_Pa_In = 'urp'; + case Lehalurup = 'urr'; + case Urat = 'urt'; + case Urumi = 'uru'; + case Uruava = 'urv'; + case Sop = 'urw'; + case Urimo = 'urx'; + case Orya = 'ury'; + case Uru_Eu_Wau_Wau = 'urz'; + case Usarufa = 'usa'; + case Ushojo = 'ush'; + case Usui = 'usi'; + case Usaghade = 'usk'; + case Uspanteco = 'usp'; + case us_Saare = 'uss'; + case Uya = 'usu'; + case Otank = 'uta'; + case Ute_Southern_Paiute = 'ute'; + case ut_Hun = 'uth'; + case Amba_Solomon_Islands = 'utp'; + case Etulo = 'utr'; + case Utu = 'utu'; + case Urum = 'uum'; + case Ura_Vanuatu = 'uur'; + case U = 'uuu'; + case West_Uvean = 'uve'; + case Uri = 'uvh'; + case Lote = 'uvl'; + case Kuku_Uwanh = 'uwa'; + case Doko_Uyanga = 'uya'; + case Uzbek = 'uzb'; + case Northern_Uzbek = 'uzn'; + case Southern_Uzbek = 'uzs'; + case Vaagri_Booli = 'vaa'; + case Vale = 'vae'; + case Vafsi = 'vaf'; + case Vagla = 'vag'; + case Varhadi_Nagpuri = 'vah'; + case Vai = 'vai'; + case Sekele = 'vaj'; + case Vehes = 'val'; + case Vanimo = 'vam'; + case Valman = 'van'; + case Vao = 'vao'; + case Vaiphei = 'vap'; + case Huarijio = 'var'; + case Vasavi = 'vas'; + case Vanuma = 'vau'; + case Varli = 'vav'; + case Wayu = 'vay'; + case Southeast_Babar = 'vbb'; + case Southwestern_Bontok = 'vbk'; + case Venetian = 'vec'; + case Veddah = 'ved'; + case Veluws = 'vel'; + case Vemgo_Mabas = 'vem'; + case Venda = 'ven'; + case Ventureno = 'veo'; + case Veps = 'vep'; + case Mom_Jango = 'ver'; + case Vaghri = 'vgr'; + case Vlaamse_Gebarentaal = 'vgt'; + case Virgin_Islands_Creole_English = 'vic'; + case Vidunda = 'vid'; + case Vietnamese = 'vie'; + case Vili = 'vif'; + case Viemo = 'vig'; + case Vilela = 'vil'; + case Vinza = 'vin'; + case Vishavan = 'vis'; + case Viti = 'vit'; + case Iduna = 'viv'; + case Bajjika = 'vjk'; + case Kariyarra = 'vka'; + case Kujarge = 'vkj'; + case Kaur = 'vkk'; + case Kulisusu = 'vkl'; + case Kamakan = 'vkm'; + case Koro_Nulu = 'vkn'; + case Kodeoha = 'vko'; + case Korlai_Creole_Portuguese = 'vkp'; + case Tenggarong_Kutai_Malay = 'vkt'; + case Kurrama = 'vku'; + case Koro_Zuba = 'vkz'; + case Valpei = 'vlp'; + case Vlaams = 'vls'; + case Martuyhunira = 'vma'; + case Barbaram = 'vmb'; + case Juxtlahuaca_Mixtec = 'vmc'; + case Mudu_Koraga = 'vmd'; + case East_Masela = 'vme'; + case Mainfrankisch = 'vmf'; + case Lungalunga = 'vmg'; + case Maraghei = 'vmh'; + case Miwa = 'vmi'; + case Ixtayutla_Mixtec = 'vmj'; + case Makhuwa_Shirima = 'vmk'; + case Malgana = 'vml'; + case Mitlatongo_Mixtec = 'vmm'; + case Soyaltepec_Mazatec = 'vmp'; + case Soyaltepec_Mixtec = 'vmq'; + case Marenje = 'vmr'; + case Moksela = 'vms'; + case Muluridyi = 'vmu'; + case Valley_Maidu = 'vmv'; + case Makhuwa = 'vmw'; + case Tamazola_Mixtec = 'vmx'; + case Ayautla_Mazatec = 'vmy'; + case Mazatlan_Mazatec = 'vmz'; + case Vano = 'vnk'; + case Vinmavis = 'vnm'; + case Vunapu = 'vnp'; + case Volapuk = 'vol'; + case Voro = 'vor'; + case Votic = 'vot'; + case Vera_a = 'vra'; + case Voro_2 = 'vro'; + case Varisi = 'vrs'; + case Burmbar = 'vrt'; + case Moldova_Sign_Language = 'vsi'; + case Venezuelan_Sign_Language = 'vsl'; + case Vedic_Sanskrit = 'vsn'; + case Valencian_Sign_Language = 'vsv'; + case Vitou = 'vto'; + case Vumbu = 'vum'; + case Vunjo = 'vun'; + case Vute = 'vut'; + case Awa_China = 'vwa'; + case Walla_Walla = 'waa'; + case Wab = 'wab'; + case Wasco_Wishram = 'wac'; + case Wamesa = 'wad'; + case Walser = 'wae'; + case Wakona = 'waf'; + case Wa_ema = 'wag'; + case Watubela = 'wah'; + case Wares = 'wai'; + case Waffa = 'waj'; + case Wolaytta = 'wal'; + case Wampanoag = 'wam'; + case Wan = 'wan'; + case Wappo = 'wao'; + case Wapishana = 'wap'; + case Wagiman = 'waq'; + case Waray_Philippines = 'war'; + case Washo = 'was'; + case Kaninuwa = 'wat'; + case Waura = 'wau'; + case Waka = 'wav'; + case Waiwai = 'waw'; + case Watam = 'wax'; + case Wayana = 'way'; + case Wampur = 'waz'; + case Warao = 'wba'; + case Wabo = 'wbb'; + case Waritai = 'wbe'; + case Wara_2 = 'wbf'; + case Wanda = 'wbh'; + case Vwanji = 'wbi'; + case Alagwa = 'wbj'; + case Waigali = 'wbk'; + case Wakhi = 'wbl'; + case Wa = 'wbm'; + case Warlpiri = 'wbp'; + case Waddar = 'wbq'; + case Wagdi = 'wbr'; + case West_Bengal_Sign_Language = 'wbs'; + case Warnman = 'wbt'; + case Wajarri = 'wbv'; + case Woi = 'wbw'; + case Yanomami = 'wca'; + case Waci_Gbe = 'wci'; + case Wandji = 'wdd'; + case Wadaginam = 'wdg'; + case Wadjiginy = 'wdj'; + case Wadikali = 'wdk'; + case Wendat = 'wdt'; + case Wadjigu = 'wdu'; + case Wadjabangayi = 'wdy'; + case Wewaw = 'wea'; + case We_Western = 'wec'; + case Wedau = 'wed'; + case Wergaia = 'weg'; + case Weh = 'weh'; + case Kiunum = 'wei'; + case Weme_Gbe = 'wem'; + case Wemale = 'weo'; + case Westphalien = 'wep'; + case Weri = 'wer'; + case Cameroon_Pidgin = 'wes'; + case Perai = 'wet'; + case Rawngtu_Chin = 'weu'; + case Wejewa = 'wew'; + case Yafi = 'wfg'; + case Wagaya = 'wga'; + case Wagawaga = 'wgb'; + case Wangkangurru = 'wgg'; + case Wahgi = 'wgi'; + case Waigeo = 'wgo'; + case Wirangu = 'wgu'; + case Warrgamay = 'wgy'; + case Sou_Upaa = 'wha'; + case North_Wahgi = 'whg'; + case Wahau_Kenyah = 'whk'; + case Wahau_Kayan = 'whu'; + case Southern_Toussian = 'wib'; + case Wichita = 'wic'; + case Wik_Epa = 'wie'; + case Wik_Keyangan = 'wif'; + case Wik_Ngathan = 'wig'; + case Wik_Me_anha = 'wih'; + case Minidien = 'wii'; + case Wik_Iiyanh = 'wij'; + case Wikalkan = 'wik'; + case Wilawila = 'wil'; + case Wik_Mungkan = 'wim'; + case Ho_Chunk = 'win'; + case Wirafed = 'wir'; + case Wiru = 'wiu'; + case Vitu = 'wiv'; + case Wiyot = 'wiy'; + case Waja = 'wja'; + case Warji = 'wji'; + case Kw_adza = 'wka'; + case Kumbaran = 'wkb'; + case Wakde = 'wkd'; + case Kalanadi = 'wkl'; + case Keerray_Woorroong = 'wkr'; + case Kunduvadi = 'wku'; + case Wakawaka = 'wkw'; + case Wangkayutyuru = 'wky'; + case Walio = 'wla'; + case Mwali_Comorian = 'wlc'; + case Wolane = 'wle'; + case Kunbarlang = 'wlg'; + case Welaun = 'wlh'; + case Waioli = 'wli'; + case Wailaki = 'wlk'; + case Wali_Sudan = 'wll'; + case Middle_Welsh = 'wlm'; + case Walloon = 'wln'; + case Wolio = 'wlo'; + case Wailapa = 'wlr'; + case Wallisian = 'wls'; + case Wuliwuli = 'wlu'; + case Wichi_Lhamtes_Vejoz = 'wlv'; + case Walak = 'wlw'; + case Wali_Ghana = 'wlx'; + case Waling = 'wly'; + case Mawa_Nigeria = 'wma'; + case Wambaya = 'wmb'; + case Wamas = 'wmc'; + case Mamainde = 'wmd'; + case Wambule = 'wme'; + case Western_Minyag = 'wmg'; + case Waima_a = 'wmh'; + case Wamin = 'wmi'; + case Maiwa_Indonesia = 'wmm'; + case Waamwang = 'wmn'; + case Wom_Papua_New_Guinea = 'wmo'; + case Wambon = 'wms'; + case Walmajarri = 'wmt'; + case Mwani = 'wmw'; + case Womo = 'wmx'; + case Mokati = 'wnb'; + case Wantoat = 'wnc'; + case Wandarang = 'wnd'; + case Waneci = 'wne'; + case Wanggom = 'wng'; + case Ndzwani_Comorian = 'wni'; + case Wanukaka = 'wnk'; + case Wanggamala = 'wnm'; + case Wunumara = 'wnn'; + case Wano = 'wno'; + case Wanap = 'wnp'; + case Usan = 'wnu'; + case Wintu = 'wnw'; + case Wanyi = 'wny'; + case Kuwema = 'woa'; + case We_Northern = 'wob'; + case Wogeo = 'woc'; + case Wolani = 'wod'; + case Woleaian = 'woe'; + case Gambian_Wolof = 'wof'; + case Wogamusin = 'wog'; + case Kamang = 'woi'; + case Longto = 'wok'; + case Wolof = 'wol'; + case Wom_Nigeria = 'wom'; + case Wongo = 'won'; + case Manombai = 'woo'; + case Woria = 'wor'; + case Hanga_Hundi = 'wos'; + case Wawonii = 'wow'; + case Weyto = 'woy'; + case Maco = 'wpc'; + case Waluwarra = 'wrb'; + case Warungu = 'wrg'; + case Wiradjuri = 'wrh'; + case Wariyangga = 'wri'; + case Garrwa = 'wrk'; + case Warlmanpa = 'wrl'; + case Warumungu = 'wrm'; + case Warnang = 'wrn'; + case Worrorra = 'wro'; + case Waropen = 'wrp'; + case Wardaman = 'wrr'; + case Waris = 'wrs'; + case Waru = 'wru'; + case Waruna = 'wrv'; + case Gugu_Warra = 'wrw'; + case Wae_Rana = 'wrx'; + case Merwari = 'wry'; + case Waray_Australia = 'wrz'; + case Warembori = 'wsa'; + case Adilabad_Gondi = 'wsg'; + case Wusi = 'wsi'; + case Waskia = 'wsk'; + case Owenia = 'wsr'; + case Wasa = 'wss'; + case Wasu = 'wsu'; + case Wotapuri_Katarqalai = 'wsv'; + case Matambwe = 'wtb'; + case Watiwa = 'wtf'; + case Wathawurrung = 'wth'; + case Berta = 'wti'; + case Watakataui = 'wtk'; + case Mewati = 'wtm'; + case Wotu = 'wtw'; + case Wikngenchera = 'wua'; + case Wunambal = 'wub'; + case Wudu = 'wud'; + case Wutunhua = 'wuh'; + case Silimo = 'wul'; + case Wumbvu = 'wum'; + case Bungu = 'wun'; + case Wurrugu = 'wur'; + case Wutung = 'wut'; + case Wu_Chinese = 'wuu'; + case Wuvulu_Aua = 'wuv'; + case Wulna = 'wux'; + case Wauyai = 'wuy'; + case Waama = 'wwa'; + case Wakabunga = 'wwb'; + case Wetamut = 'wwo'; + case Warrwa = 'wwr'; + case Wawa = 'www'; + case Waxianghua = 'wxa'; + case Wardandi = 'wxw'; + case Wangaaybuwan_Ngiyambaa = 'wyb'; + case Woiwurrung = 'wyi'; + case Wymysorys = 'wym'; + case Wyandot = 'wyn'; + case Wayoro = 'wyr'; + case Western_Fijian = 'wyy'; + case Andalusian_Arabic = 'xaa'; + case Sambe = 'xab'; + case Kachari = 'xac'; + case Adai = 'xad'; + case Aequian = 'xae'; + case Aghwan = 'xag'; + case Kaimbe = 'xai'; + case Ararandewara = 'xaj'; + case Maku = 'xak'; + case Kalmyk = 'xal'; + case Xam = 'xam'; + case Xamtanga = 'xan'; + case Khao = 'xao'; + case Apalachee = 'xap'; + case Aquitanian = 'xaq'; + case Karami = 'xar'; + case Kamas = 'xas'; + case Katawixi = 'xat'; + case Kauwera = 'xau'; + case Xavante = 'xav'; + case Kawaiisu = 'xaw'; + case Kayan_Mahakam = 'xay'; + case Lower_Burdekin = 'xbb'; + case Bactrian = 'xbc'; + case Bindal = 'xbd'; + case Bigambal = 'xbe'; + case Bunganditj = 'xbg'; + case Kombio = 'xbi'; + case Birrpayi = 'xbj'; + case Middle_Breton = 'xbm'; + case Kenaboi = 'xbn'; + case Bolgarian = 'xbo'; + case Bibbulman = 'xbp'; + case Kambera = 'xbr'; + case Kambiwa = 'xbw'; + case Batjala = 'xby'; + case Cumbric = 'xcb'; + case Camunic = 'xcc'; + case Celtiberian = 'xce'; + case Cisalpine_Gaulish = 'xcg'; + case Chemakum = 'xch'; + case Classical_Armenian = 'xcl'; + case Comecrudo = 'xcm'; + case Cotoname = 'xcn'; + case Chorasmian = 'xco'; + case Carian = 'xcr'; + case Classical_Tibetan = 'xct'; + case Curonian = 'xcu'; + case Chuvantsy = 'xcv'; + case Coahuilteco = 'xcw'; + case Cayuse = 'xcy'; + case Darkinyung = 'xda'; + case Dacian = 'xdc'; + case Dharuk = 'xdk'; + case Edomite = 'xdm'; + case Kwandu = 'xdo'; + case Kaitag = 'xdq'; + case Malayic_Dayak = 'xdy'; + case Eblan = 'xeb'; + case Hdi = 'xed'; + case Xegwi = 'xeg'; + case Kelo = 'xel'; + case Kembayan = 'xem'; + case Epi_Olmec = 'xep'; + case Xerente = 'xer'; + case Kesawai = 'xes'; + case Xeta = 'xet'; + case Keoru_Ahia = 'xeu'; + case Faliscan = 'xfa'; + case Galatian = 'xga'; + case Gbin = 'xgb'; + case Gudang = 'xgd'; + case Gabrielino_Fernandeno = 'xgf'; + case Goreng = 'xgg'; + case Garingbal = 'xgi'; + case Galindan = 'xgl'; + case Dharumbal = 'xgm'; + case Garza = 'xgr'; + case Unggumi = 'xgu'; + case Guwa = 'xgw'; + case Harami = 'xha'; + case Hunnic = 'xhc'; + case Hadrami = 'xhd'; + case Khetrani = 'xhe'; + case Middle_Khmer_1400_to_1850_CE = 'xhm'; + case Xhosa = 'xho'; + case Hernican = 'xhr'; + case Hattic = 'xht'; + case Hurrian = 'xhu'; + case Khua = 'xhv'; + case Iberian = 'xib'; + case Xiri = 'xii'; + case Illyrian = 'xil'; + case Xinca = 'xin'; + case Xiriana = 'xir'; + case Kisan = 'xis'; + case Indus_Valley_Language = 'xiv'; + case Xipaya = 'xiy'; + case Minjungbal = 'xjb'; + case Jaitmatang = 'xjt'; + case Kalkoti = 'xka'; + case Northern_Nago = 'xkb'; + case Kho_ini = 'xkc'; + case Mendalam_Kayan = 'xkd'; + case Kereho = 'xke'; + case Khengkha = 'xkf'; + case Kagoro = 'xkg'; + case Kenyan_Sign_Language = 'xki'; + case Kajali = 'xkj'; + case Kachok = 'xkk'; + case Mainstream_Kenyah = 'xkl'; + case Kayan_River_Kayan = 'xkn'; + case Kiorr = 'xko'; + case Kabatei = 'xkp'; + case Koroni = 'xkq'; + case Xakriaba = 'xkr'; + case Kumbewaha = 'xks'; + case Kantosi = 'xkt'; + case Kaamba = 'xku'; + case Kgalagadi = 'xkv'; + case Kembra = 'xkw'; + case Karore = 'xkx'; + case Uma_Lasan = 'xky'; + case Kurtokha = 'xkz'; + case Kamula = 'xla'; + case Loup_B = 'xlb'; + case Lycian = 'xlc'; + case Lydian = 'xld'; + case Lemnian = 'xle'; + case Ligurian_Ancient = 'xlg'; + case Liburnian = 'xli'; + case Alanic = 'xln'; + case Loup_A = 'xlo'; + case Lepontic = 'xlp'; + case Lusitanian = 'xls'; + case Cuneiform_Luwian = 'xlu'; + case Elymian = 'xly'; + case Mushungulu = 'xma'; + case Mbonga = 'xmb'; + case Makhuwa_Marrevone = 'xmc'; + case Mbudum = 'xmd'; + case Median = 'xme'; + case Mingrelian = 'xmf'; + case Mengaka = 'xmg'; + case Kugu_Muminh = 'xmh'; + case Majera = 'xmj'; + case Ancient_Macedonian = 'xmk'; + case Malaysian_Sign_Language = 'xml'; + case Manado_Malay = 'xmm'; + case Manichaean_Middle_Persian = 'xmn'; + case Morerebi = 'xmo'; + case Kuku_Mu_inh = 'xmp'; + case Kuku_Mangk = 'xmq'; + case Meroitic = 'xmr'; + case Moroccan_Sign_Language = 'xms'; + case Matbat = 'xmt'; + case Kamu = 'xmu'; + case Antankarana_Malagasy = 'xmv'; + case Tsimihety_Malagasy = 'xmw'; + case Salawati = 'xmx'; + case Mayaguduna = 'xmy'; + case Mori_Bawah = 'xmz'; + case Ancient_North_Arabian = 'xna'; + case Kanakanabu = 'xnb'; + case Middle_Mongolian = 'xng'; + case Kuanhua = 'xnh'; + case Ngarigu = 'xni'; + case Ngoni_Tanzania = 'xnj'; + case Nganakarti = 'xnk'; + case Ngumbarl = 'xnm'; + case Northern_Kankanay = 'xnn'; + case Anglo_Norman = 'xno'; + case Ngoni_Mozambique = 'xnq'; + case Kangri = 'xnr'; + case Kanashi = 'xns'; + case Narragansett = 'xnt'; + case Nukunul = 'xnu'; + case Nyiyaparli = 'xny'; + case Kenzi = 'xnz'; + case O_chi_chi = 'xoc'; + case Kokoda = 'xod'; + case Soga = 'xog'; + case Kominimung = 'xoi'; + case Xokleng = 'xok'; + case Komo_Sudan = 'xom'; + case Konkomba = 'xon'; + case Xukuru = 'xoo'; + case Kopar = 'xop'; + case Korubo = 'xor'; + case Kowaki = 'xow'; + case Pirriya = 'xpa'; + case Northeastern_Tasmanian = 'xpb'; + case Pecheneg = 'xpc'; + case Oyster_Bay_Tasmanian = 'xpd'; + case Liberia_Kpelle = 'xpe'; + case Southeast_Tasmanian = 'xpf'; + case Phrygian = 'xpg'; + case North_Midlands_Tasmanian = 'xph'; + case Pictish = 'xpi'; + case Mpalitjanh = 'xpj'; + case Kulina_Pano = 'xpk'; + case Port_Sorell_Tasmanian = 'xpl'; + case Pumpokol = 'xpm'; + case Kapinawa = 'xpn'; + case Pochutec = 'xpo'; + case Puyo_Paekche = 'xpp'; + case Mohegan_Pequot = 'xpq'; + case Parthian = 'xpr'; + case Pisidian = 'xps'; + case Punthamara = 'xpt'; + case Punic = 'xpu'; + case Northern_Tasmanian = 'xpv'; + case Northwestern_Tasmanian = 'xpw'; + case Southwestern_Tasmanian = 'xpx'; + case Puyo = 'xpy'; + case Bruny_Island_Tasmanian = 'xpz'; + case Karakhanid = 'xqa'; + case Qatabanian = 'xqt'; + case Kraho = 'xra'; + case Eastern_Karaboro = 'xrb'; + case Gundungurra = 'xrd'; + case Kreye = 'xre'; + case Minang = 'xrg'; + case Krikati_Timbira = 'xri'; + case Armazic = 'xrm'; + case Arin = 'xrn'; + case Raetic = 'xrr'; + case Aranama_Tamique = 'xrt'; + case Marriammu = 'xru'; + case Karawa = 'xrw'; + case Sabaean = 'xsa'; + case Sambal = 'xsb'; + case Scythian = 'xsc'; + case Sidetic = 'xsd'; + case Sempan = 'xse'; + case Shamang = 'xsh'; + case Sio = 'xsi'; + case Subi = 'xsj'; + case South_Slavey = 'xsl'; + case Kasem = 'xsm'; + case Sanga_Nigeria = 'xsn'; + case Solano = 'xso'; + case Silopi = 'xsp'; + case Makhuwa_Saka = 'xsq'; + case Sherpa = 'xsr'; + case Sanuma = 'xsu'; + case Sudovian = 'xsv'; + case Saisiyat = 'xsy'; + case Alcozauca_Mixtec = 'xta'; + case Chazumba_Mixtec = 'xtb'; + case Katcha_Kadugli_Miri = 'xtc'; + case Diuxi_Tilantongo_Mixtec = 'xtd'; + case Ketengban = 'xte'; + case Transalpine_Gaulish = 'xtg'; + case Yitha_Yitha = 'xth'; + case Sinicahua_Mixtec = 'xti'; + case San_Juan_Teita_Mixtec = 'xtj'; + case Tijaltepec_Mixtec = 'xtl'; + case Magdalena_Penasco_Mixtec = 'xtm'; + case Northern_Tlaxiaco_Mixtec = 'xtn'; + case Tokharian_A = 'xto'; + case San_Miguel_Piedras_Mixtec = 'xtp'; + case Tumshuqese = 'xtq'; + case Early_Tripuri = 'xtr'; + case Sindihui_Mixtec = 'xts'; + case Tacahua_Mixtec = 'xtt'; + case Cuyamecalco_Mixtec = 'xtu'; + case Thawa = 'xtv'; + case Tawande = 'xtw'; + case Yoloxochitl_Mixtec = 'xty'; + case Alu_Kurumba = 'xua'; + case Betta_Kurumba = 'xub'; + case Umiida = 'xud'; + case Kunigami = 'xug'; + case Jennu_Kurumba = 'xuj'; + case Ngunawal = 'xul'; + case Umbrian = 'xum'; + case Unggaranggu = 'xun'; + case Kuo = 'xuo'; + case Upper_Umpqua = 'xup'; + case Urartian = 'xur'; + case Kuthant = 'xut'; + case Kxoe = 'xuu'; + case Venetic = 'xve'; + case Kamviri = 'xvi'; + case Vandalic = 'xvn'; + case Volscian = 'xvo'; + case Vestinian = 'xvs'; + case Kwaza = 'xwa'; + case Woccon = 'xwc'; + case Wadi_Wadi = 'xwd'; + case Xwela_Gbe = 'xwe'; + case Kwegu = 'xwg'; + case Wajuk = 'xwj'; + case Wangkumara = 'xwk'; + case Western_Xwla_Gbe = 'xwl'; + case Written_Oirat = 'xwo'; + case Kwerba_Mamberamo = 'xwr'; + case Wotjobaluk = 'xwt'; + case Wemba_Wemba = 'xww'; + case Boro_Ghana = 'xxb'; + case Ke_o = 'xxk'; + case Minkin = 'xxm'; + case Koropo = 'xxr'; + case Tambora = 'xxt'; + case Yaygir = 'xya'; + case Yandjibara = 'xyb'; + case Mayi_Yapi = 'xyj'; + case Mayi_Kulan = 'xyk'; + case Yalakalore = 'xyl'; + case Mayi_Thakurti = 'xyt'; + case Yorta_Yorta = 'xyy'; + case Zhang_Zhung = 'xzh'; + case Zemgalian = 'xzm'; + case Ancient_Zapotec = 'xzp'; + case Yaminahua = 'yaa'; + case Yuhup = 'yab'; + case Pass_Valley_Yali = 'yac'; + case Yagua = 'yad'; + case Pume = 'yae'; + case Yaka_Democratic_Republic_of_Congo = 'yaf'; + case Yamana = 'yag'; + case Yazgulyam = 'yah'; + case Yagnobi = 'yai'; + case Banda_Yangere = 'yaj'; + case Yakama = 'yak'; + case Yalunka = 'yal'; + case Yamba = 'yam'; + case Mayangna = 'yan'; + case Yao = 'yao'; + case Yapese = 'yap'; + case Yaqui = 'yaq'; + case Yabarana = 'yar'; + case Nugunu_Cameroon = 'yas'; + case Yambeta = 'yat'; + case Yuwana = 'yau'; + case Yangben = 'yav'; + case Yawalapiti = 'yaw'; + case Yauma = 'yax'; + case Agwagwune = 'yay'; + case Lokaa = 'yaz'; + case Yala = 'yba'; + case Yemba = 'ybb'; + case West_Yugur = 'ybe'; + case Yakha = 'ybh'; + case Yamphu = 'ybi'; + case Hasha = 'ybj'; + case Bokha = 'ybk'; + case Yukuben = 'ybl'; + case Yaben = 'ybm'; + case Yabaana = 'ybn'; + case Yabong = 'ybo'; + case Yawiyo = 'ybx'; + case Yaweyuha = 'yby'; + case Chesu = 'ych'; + case Lolopo = 'ycl'; + case Yucuna = 'ycn'; + case Chepya = 'ycp'; + case Yilan_Creole = 'ycr'; + case Yanda = 'yda'; + case Eastern_Yiddish = 'ydd'; + case Yangum_Dey = 'yde'; + case Yidgha = 'ydg'; + case Yoidik = 'ydk'; + case Ravula = 'yea'; + case Yeniche = 'yec'; + case Yimas = 'yee'; + case Yeni = 'yei'; + case Yevanic = 'yej'; + case Yela = 'yel'; + case Tarok = 'yer'; + case Nyankpa = 'yes'; + case Yetfa = 'yet'; + case Yerukula = 'yeu'; + case Yapunda = 'yev'; + case Yeyi = 'yey'; + case Malyangapa = 'yga'; + case Yiningayi = 'ygi'; + case Yangum_Gel = 'ygl'; + case Yagomi = 'ygm'; + case Gepo = 'ygp'; + case Yagaria = 'ygr'; + case Yol_u_Sign_Language = 'ygs'; + case Yugul = 'ygu'; + case Yagwoia = 'ygw'; + case Baha_Buyang = 'yha'; + case Judeo_Iraqi_Arabic = 'yhd'; + case Hlepho_Phowa = 'yhl'; + case Yan_nhangu_Sign_Language = 'yhs'; + case Yinggarda = 'yia'; + case Yiddish = 'yid'; + case Ache_2 = 'yif'; + case Wusa_Nasu = 'yig'; + case Western_Yiddish = 'yih'; + case Yidiny = 'yii'; + case Yindjibarndi = 'yij'; + case Dongshanba_Lalo = 'yik'; + case Yindjilandji = 'yil'; + case Yimchungru_Naga = 'yim'; + case Riang_Lai = 'yin'; + case Pholo = 'yip'; + case Miqie = 'yiq'; + case North_Awyu = 'yir'; + case Yis = 'yis'; + case Eastern_Lalu = 'yit'; + case Awu = 'yiu'; + case Northern_Nisu = 'yiv'; + case Axi_Yi = 'yix'; + case Azhe = 'yiz'; + case Yakan = 'yka'; + case Northern_Yukaghir = 'ykg'; + case Khamnigan_Mongol = 'ykh'; + case Yoke = 'yki'; + case Yakaikeke = 'ykk'; + case Khlula = 'ykl'; + case Kap = 'ykm'; + case Kua_nsi = 'ykn'; + case Yasa = 'yko'; + case Yekora = 'ykr'; + case Kathu = 'ykt'; + case Kuamasi = 'yku'; + case Yakoma = 'yky'; + case Yaul = 'yla'; + case Yaleba = 'ylb'; + case Yele = 'yle'; + case Yelogu = 'ylg'; + case Angguruk_Yali = 'yli'; + case Yil = 'yll'; + case Limi = 'ylm'; + case Langnian_Buyang = 'yln'; + case Naluo_Yi = 'ylo'; + case Yalarnnga = 'ylr'; + case Aribwaung = 'ylu'; + case Nyalayu = 'yly'; + case Yambes = 'ymb'; + case Southern_Muji = 'ymc'; + case Muda = 'ymd'; + case Yameo = 'yme'; + case Yamongeri = 'ymg'; + case Mili = 'ymh'; + case Moji = 'ymi'; + case Makwe = 'ymk'; + case Iamalele = 'yml'; + case Maay = 'ymm'; + case Yamna = 'ymn'; + case Yangum_Mon = 'ymo'; + case Yamap = 'ymp'; + case Qila_Muji = 'ymq'; + case Malasar = 'ymr'; + case Mysian = 'yms'; + case Northern_Muji = 'ymx'; + case Muzi = 'ymz'; + case Aluo = 'yna'; + case Yandruwandha = 'ynd'; + case Lang_e = 'yne'; + case Yango = 'yng'; + case Naukan_Yupik = 'ynk'; + case Yangulam = 'ynl'; + case Yana = 'ynn'; + case Yong = 'yno'; + case Yendang = 'ynq'; + case Yansi = 'yns'; + case Yahuna = 'ynu'; + case Yoba = 'yob'; + case Yogad = 'yog'; + case Yonaguni = 'yoi'; + case Yokuts = 'yok'; + case Yola = 'yol'; + case Yombe = 'yom'; + case Yongkom = 'yon'; + case Yoruba = 'yor'; + case Yotti = 'yot'; + case Yoron = 'yox'; + case Yoy = 'yoy'; + case Phala = 'ypa'; + case Labo_Phowa = 'ypb'; + case Phola = 'ypg'; + case Phupha = 'yph'; + case Phuma = 'ypm'; + case Ani_Phowa = 'ypn'; + case Alo_Phola = 'ypo'; + case Phupa = 'ypp'; + case Phuza = 'ypz'; + case Yerakai = 'yra'; + case Yareba = 'yrb'; + case Yaoure = 'yre'; + case Nenets = 'yrk'; + case Nhengatu = 'yrl'; + case Yirrk_Mel = 'yrm'; + case Yerong = 'yrn'; + case Yaroame = 'yro'; + case Yarsun = 'yrs'; + case Yarawata = 'yrw'; + case Yarluyandi = 'yry'; + case Yassic = 'ysc'; + case Samatao = 'ysd'; + case Sonaga = 'ysg'; + case Yugoslavian_Sign_Language = 'ysl'; + case Myanmar_Sign_Language = 'ysm'; + case Sani = 'ysn'; + case Nisi_China = 'yso'; + case Southern_Lolopo = 'ysp'; + case Sirenik_Yupik = 'ysr'; + case Yessan_Mayo = 'yss'; + case Sanie = 'ysy'; + case Talu = 'yta'; + case Tanglang = 'ytl'; + case Thopho = 'ytp'; + case Yout_Wam = 'ytw'; + case Yatay = 'yty'; + case Yucateco = 'yua'; + case Yugambal = 'yub'; + case Yuchi = 'yuc'; + case Judeo_Tripolitanian_Arabic = 'yud'; + case Yue_Chinese = 'yue'; + case Havasupai_Walapai_Yavapai = 'yuf'; + case Yug = 'yug'; + case Yuruti = 'yui'; + case Karkar_Yuri = 'yuj'; + case Yuki = 'yuk'; + case Yulu = 'yul'; + case Quechan = 'yum'; + case Bena_Nigeria = 'yun'; + case Yukpa = 'yup'; + case Yuqui = 'yuq'; + case Yurok = 'yur'; + case Yopno = 'yut'; + case Yau_Morobe_Province = 'yuw'; + case Southern_Yukaghir = 'yux'; + case East_Yugur = 'yuy'; + case Yuracare = 'yuz'; + case Yawa = 'yva'; + case Yavitero = 'yvt'; + case Kalou = 'ywa'; + case Yinhawangka = 'ywg'; + case Western_Lalu = 'ywl'; + case Yawanawa = 'ywn'; + case Wuding_Luquan_Yi = 'ywq'; + case Yawuru = 'ywr'; + case Xishanba_Lalo = 'ywt'; + case Wumeng_Nasu = 'ywu'; + case Yawarawarga = 'yww'; + case Mayawali = 'yxa'; + case Yagara = 'yxg'; + case Yardliyawarra = 'yxl'; + case Yinwum = 'yxm'; + case Yuyu = 'yxu'; + case Yabula_Yabula = 'yxy'; + case Yir_Yoront = 'yyr'; + case Yau_Sandaun_Province = 'yyu'; + case Ayizi = 'yyz'; + case E_ma_Buyang = 'yzg'; + case Zokhuo = 'yzk'; + case Sierra_de_Juarez_Zapotec = 'zaa'; + case Western_Tlacolula_Valley_Zapotec = 'zab'; + case Ocotlan_Zapotec = 'zac'; + case Cajonos_Zapotec = 'zad'; + case Yareni_Zapotec = 'zae'; + case Ayoquesco_Zapotec = 'zaf'; + case Zaghawa = 'zag'; + case Zangwal = 'zah'; + case Isthmus_Zapotec = 'zai'; + case Zaramo = 'zaj'; + case Zanaki = 'zak'; + case Zauzou = 'zal'; + case Miahuatlan_Zapotec = 'zam'; + case Ozolotepec_Zapotec = 'zao'; + case Zapotec = 'zap'; + case Aloapam_Zapotec = 'zaq'; + case Rincon_Zapotec = 'zar'; + case Santo_Domingo_Albarradas_Zapotec = 'zas'; + case Tabaa_Zapotec = 'zat'; + case Zangskari = 'zau'; + case Yatzachi_Zapotec = 'zav'; + case Mitla_Zapotec = 'zaw'; + case Xadani_Zapotec = 'zax'; + case Zayse_Zergulla = 'zay'; + case Zari = 'zaz'; + case Balaibalan = 'zba'; + case Central_Berawan = 'zbc'; + case East_Berawan = 'zbe'; + case Blissymbols = 'zbl'; + case Batui = 'zbt'; + case Bu_Bauchi_State = 'zbu'; + case West_Berawan = 'zbw'; + case Coatecas_Altas_Zapotec = 'zca'; + case Las_Delicias_Zapotec = 'zcd'; + case Central_Hongshuihe_Zhuang = 'zch'; + case Ngazidja_Comorian = 'zdj'; + case Zeeuws = 'zea'; + case Zenag = 'zeg'; + case Eastern_Hongshuihe_Zhuang = 'zeh'; + case Zeem = 'zem'; + case Zenaga = 'zen'; + case Kinga = 'zga'; + case Guibei_Zhuang = 'zgb'; + case Standard_Moroccan_Tamazight = 'zgh'; + case Minz_Zhuang = 'zgm'; + case Guibian_Zhuang = 'zgn'; + case Magori = 'zgr'; + case Zhuang = 'zha'; + case Zhaba = 'zhb'; + case Dai_Zhuang = 'zhd'; + case Zhire = 'zhi'; + case Nong_Zhuang = 'zhn'; + case Chinese = 'zho'; + case Zhoa = 'zhw'; + case Zia = 'zia'; + case Zimbabwe_Sign_Language = 'zib'; + case Zimakani = 'zik'; + case Zialo = 'zil'; + case Mesme = 'zim'; + case Zinza = 'zin'; + case Zigula = 'ziw'; + case Zizilivakan = 'ziz'; + case Kaimbulawa = 'zka'; + case Kadu = 'zkd'; + case Koguryo = 'zkg'; + case Khorezmian = 'zkh'; + case Karankawa = 'zkk'; + case Kanan = 'zkn'; + case Kott = 'zko'; + case Sao_Paulo_Kaingang = 'zkp'; + case Zakhring = 'zkr'; + case Kitan = 'zkt'; + case Kaurna = 'zku'; + case Krevinian = 'zkv'; + case Khazar = 'zkz'; + case Zula = 'zla'; + case Liujiang_Zhuang = 'zlj'; + case Malay_individual_language = 'zlm'; + case Lianshan_Zhuang = 'zln'; + case Liuqian_Zhuang = 'zlq'; + case Zul = 'zlu'; + case Manda_Australia = 'zma'; + case Zimba = 'zmb'; + case Margany = 'zmc'; + case Maridan = 'zmd'; + case Mangerr = 'zme'; + case Mfinu = 'zmf'; + case Marti_Ke = 'zmg'; + case Makolkol = 'zmh'; + case Negeri_Sembilan_Malay = 'zmi'; + case Maridjabin = 'zmj'; + case Mandandanyi = 'zmk'; + case Matngala = 'zml'; + case Marimanindji = 'zmm'; + case Mbangwe = 'zmn'; + case Molo = 'zmo'; + case Mpuono = 'zmp'; + case Mituku = 'zmq'; + case Maranunggu = 'zmr'; + case Mbesa = 'zms'; + case Maringarr = 'zmt'; + case Muruwari = 'zmu'; + case Mbariman_Gudhinma = 'zmv'; + case Mbo_Democratic_Republic_of_Congo = 'zmw'; + case Bomitaba = 'zmx'; + case Mariyedi = 'zmy'; + case Mbandja = 'zmz'; + case Zan_Gula = 'zna'; + case Zande_individual_language = 'zne'; + case Mang = 'zng'; + case Manangkari = 'znk'; + case Mangas = 'zns'; + case Copainala_Zoque = 'zoc'; + case Chimalapa_Zoque = 'zoh'; + case Zou = 'zom'; + case Asuncion_Mixtepec_Zapotec = 'zoo'; + case Tabasco_Zoque = 'zoq'; + case Rayon_Zoque = 'zor'; + case Francisco_Leon_Zoque = 'zos'; + case Lachiguiri_Zapotec = 'zpa'; + case Yautepec_Zapotec = 'zpb'; + case Choapan_Zapotec = 'zpc'; + case Southeastern_Ixtlan_Zapotec = 'zpd'; + case Petapa_Zapotec = 'zpe'; + case San_Pedro_Quiatoni_Zapotec = 'zpf'; + case Guevea_De_Humboldt_Zapotec = 'zpg'; + case Totomachapan_Zapotec = 'zph'; + case Santa_Maria_Quiegolani_Zapotec = 'zpi'; + case Quiavicuzas_Zapotec = 'zpj'; + case Tlacolulita_Zapotec = 'zpk'; + case Lachixio_Zapotec = 'zpl'; + case Mixtepec_Zapotec = 'zpm'; + case Santa_Ines_Yatzechi_Zapotec = 'zpn'; + case Amatlan_Zapotec = 'zpo'; + case El_Alto_Zapotec = 'zpp'; + case Zoogocho_Zapotec = 'zpq'; + case Santiago_Xanica_Zapotec = 'zpr'; + case Coatlan_Zapotec = 'zps'; + case San_Vicente_Coatlan_Zapotec = 'zpt'; + case Yalalag_Zapotec = 'zpu'; + case Chichicapan_Zapotec = 'zpv'; + case Zaniza_Zapotec = 'zpw'; + case San_Baltazar_Loxicha_Zapotec = 'zpx'; + case Mazaltepec_Zapotec = 'zpy'; + case Texmelucan_Zapotec = 'zpz'; + case Qiubei_Zhuang = 'zqe'; + case Kara_Korea = 'zra'; + case Mirgan = 'zrg'; + case Zerenkel = 'zrn'; + case Zaparo = 'zro'; + case Zarphatic = 'zrp'; + case Mairasi = 'zrs'; + case Sarasira = 'zsa'; + case Kaskean = 'zsk'; + case Zambian_Sign_Language = 'zsl'; + case Standard_Malay = 'zsm'; + case Southern_Rincon_Zapotec = 'zsr'; + case Sukurum = 'zsu'; + case Elotepec_Zapotec = 'zte'; + case Xanaguia_Zapotec = 'ztg'; + case Lapaguia_Guivini_Zapotec = 'ztl'; + case San_Agustin_Mixtepec_Zapotec = 'ztm'; + case Santa_Catarina_Albarradas_Zapotec = 'ztn'; + case Loxicha_Zapotec = 'ztp'; + case Quioquitani_Quieri_Zapotec = 'ztq'; + case Tilquiapan_Zapotec = 'zts'; + case Tejalapan_Zapotec = 'ztt'; + case Guila_Zapotec = 'ztu'; + case Zaachila_Zapotec = 'ztx'; + case Yatee_Zapotec = 'zty'; + case Tokano = 'zuh'; + case Zulu = 'zul'; + case Kumzari = 'zum'; + case Zuni = 'zun'; + case Zumaya = 'zuy'; + case Zay = 'zwa'; + case No_linguistic_content = 'zxx'; + case Yongbei_Zhuang = 'zyb'; + case Yang_Zhuang = 'zyg'; + case Youjiang_Zhuang = 'zyj'; + case Yongnan_Zhuang = 'zyn'; + case Zyphe_Chin = 'zyp'; + case Zaza = 'zza'; + case Zuojiang_Zhuang = 'zzj'; + +} + +class LanguageName {} + +class BackedEnum { + static public function fromName(string $s, string $t):mixed { + return null; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10834.php b/tests/PHPStan/Analyser/data/bug-10834.php new file mode 100644 index 0000000000..69efb18635 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10834.php @@ -0,0 +1,21 @@ + $b + */ + public function doFoo($b): void + { + assertType('non-falsy-string', '@' . $b); + } + + /** + * @param int|false $b + */ + public function doFoo2($b): void + { + assertType('non-falsy-string', '@' . $b); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-10893.php b/tests/PHPStan/Analyser/data/bug-10893.php new file mode 100644 index 0000000000..469c8956bd --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10893.php @@ -0,0 +1,20 @@ +format('u')); + assertType('int', (int)$value->format('u')); + assertType('bool', (int)$value->format('u') !== 0); + assertType('non-falsy-string', $nonfalsy); + assertType('int', (int)$nonfalsy); + assertType('bool', (int)$nonfalsy !== 0); + + return (int) $value->format('u') !== 0; +} diff --git a/tests/PHPStan/Analyser/data/bug-10922.php b/tests/PHPStan/Analyser/data/bug-10922.php new file mode 100644 index 0000000000..62ee393f7b --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10922.php @@ -0,0 +1,43 @@ + $array */ + public function sayHello(array $array): void + { + foreach ($array as $key => $item) { + $array[$key]['bar'] = ''; + } + assertType("array", $array); + } + + /** @param array $array */ + public function sayHello2(array $array): void + { + if (count($array) > 0) { + return; + } + + foreach ($array as $key => $item) { + $array[$key]['bar'] = ''; + } + assertType("array{}", $array); + } + + /** @param array $array */ + public function sayHello3(array $array): void + { + if (count($array) === 0) { + return; + } + + foreach ($array as $key => $item) { + $array[$key]['bar'] = ''; + } + assertType("non-empty-array", $array); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10952.php b/tests/PHPStan/Analyser/data/bug-10952.php new file mode 100644 index 0000000000..d25c03b1fe --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10952.php @@ -0,0 +1,32 @@ + + */ + public function getArray(): array + { + return array_fill(0, random_int(0, 10), 'test'); + } + + public function test(): void + { + $array = $this->getArray(); + + if (count($array) > 1) { + assertType('non-empty-array', $array); + } else { + assertType('array', $array); + } + + match (true) { + count($array) > 1 => assertType('non-empty-array', $array), + default => assertType('array', $array), + }; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-10952b.php b/tests/PHPStan/Analyser/data/bug-10952b.php new file mode 100644 index 0000000000..b02b89ac50 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10952b.php @@ -0,0 +1,41 @@ +getString(); + + if (1 < mb_strlen($string)) { + assertType('non-empty-string', $string); + } else { + assertType("string", $string); + } + + if (mb_strlen($string) > 1) { + assertType('non-empty-string', $string); + } else { + assertType("string", $string); + } + + if (2 < mb_strlen($string)) { + assertType('non-falsy-string', $string); + } else { + assertType("string", $string); + } + + match (true) { + mb_strlen($string) > 0 => assertType('non-empty-string', $string), + default => assertType("''", $string), + }; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-2378.php b/tests/PHPStan/Analyser/data/bug-2378.php index d5984a6364..a05de0f302 100644 --- a/tests/PHPStan/Analyser/data/bug-2378.php +++ b/tests/PHPStan/Analyser/data/bug-2378.php @@ -17,7 +17,7 @@ public function doFoo( assertType('array{\'a\', \'b\', \'c\', \'d\'}', range('a', 'd')); assertType('array{\'a\', \'c\', \'e\', \'g\', \'i\'}', range('a', 'i', 2)); - assertType('array', range($s, $s)); + assertType('list', range($s, $s)); } } diff --git a/tests/PHPStan/Analyser/data/bug-2580.php b/tests/PHPStan/Analyser/data/bug-2580.php new file mode 100644 index 0000000000..98d5a8160c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-2580.php @@ -0,0 +1,16 @@ + $typeName + * @param mixed $value + */ +function cast($value, string $typeName): void { + if (is_object($value) && get_class($value) === $typeName) { + assertType('T of object (function Bug2580\cast(), argument)', $value); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-2600-php8.php b/tests/PHPStan/Analyser/data/bug-2600-php8.php index 5df2b73736..9dd9838ca8 100644 --- a/tests/PHPStan/Analyser/data/bug-2600-php8.php +++ b/tests/PHPStan/Analyser/data/bug-2600-php8.php @@ -1,6 +1,6 @@ ', $args); } /** @@ -39,7 +39,7 @@ public function doLorem(...$x) { public function doIpsum($x = null) { $args = func_get_args(); assertType('mixed', $x); - assertType('array', $args); + assertType('list', $args); } } @@ -51,7 +51,7 @@ class Bar public function doFoo($x = null) { $args = func_get_args(); assertType('string|null', $x); - assertType('array', $args); + assertType('list', $args); } /** diff --git a/tests/PHPStan/Analyser/data/bug-2600.php b/tests/PHPStan/Analyser/data/bug-2600.php index 07ca71fd77..11c341582b 100644 --- a/tests/PHPStan/Analyser/data/bug-2600.php +++ b/tests/PHPStan/Analyser/data/bug-2600.php @@ -12,7 +12,7 @@ class Foo public function doFoo($x = null) { $args = func_get_args(); assertType('mixed', $x); - assertType('array', $args); + assertType('list', $args); } /** @@ -26,20 +26,20 @@ public function doBar($x = null) { * @param mixed $x */ public function doBaz(...$x) { - assertType('array', $x); + assertType('list', $x); } /** * @param mixed ...$x */ public function doLorem(...$x) { - assertType('array', $x); + assertType('list', $x); } public function doIpsum($x = null) { $args = func_get_args(); assertType('mixed', $x); - assertType('array', $args); + assertType('list', $args); } } @@ -51,7 +51,7 @@ class Bar public function doFoo($x = null) { $args = func_get_args(); assertType('string|null', $x); - assertType('array', $args); + assertType('list', $args); } /** @@ -65,24 +65,24 @@ public function doBar($x = null) { * @param string $x */ public function doBaz(...$x) { - assertType('array', $x); + assertType('list', $x); } /** * @param string ...$x */ public function doLorem(...$x) { - assertType('array', $x); + assertType('list', $x); } } function foo($x, string ...$y): void { assertType('mixed', $x); - assertType('array', $y); + assertType('list', $y); } function ($x, string ...$y): void { assertType('mixed', $x); - assertType('array', $y); + assertType('list', $y); }; diff --git a/tests/PHPStan/Analyser/data/bug-2863.php b/tests/PHPStan/Analyser/data/bug-2863.php index 5f70c4795a..1e81b90d1d 100644 --- a/tests/PHPStan/Analyser/data/bug-2863.php +++ b/tests/PHPStan/Analyser/data/bug-2863.php @@ -5,7 +5,7 @@ use function PHPStan\Testing\assertType; $result = json_decode('{"a":5}'); -assertType('int', json_last_error()); +assertType('0|1|2|3|4|5|6|7|8|9|10', json_last_error()); assertType('string', json_last_error_msg()); if (json_last_error() !== JSON_ERROR_NONE || json_last_error_msg() !== 'No error') { @@ -17,7 +17,7 @@ // $result2 = json_decode(''); -assertType('int', json_last_error()); +assertType('0|1|2|3|4|5|6|7|8|9|10', json_last_error()); assertType('string', json_last_error_msg()); if (json_last_error() !== JSON_ERROR_NONE || json_last_error_msg() !== 'No error') { @@ -29,7 +29,7 @@ // $result3 = json_encode([]); -assertType('int', json_last_error()); +assertType('0|1|2|3|4|5|6|7|8|9|10', json_last_error()); assertType('string', json_last_error_msg()); if (json_last_error() !== JSON_ERROR_NONE || json_last_error_msg() !== 'No error') { diff --git a/tests/PHPStan/Analyser/data/bug-3009.php b/tests/PHPStan/Analyser/data/bug-3009.php index f6b66ca4ee..969efdc372 100644 --- a/tests/PHPStan/Analyser/data/bug-3009.php +++ b/tests/PHPStan/Analyser/data/bug-3009.php @@ -15,14 +15,14 @@ public function createRedirectRequest(string $redirectUri): ?string return null; } - assertType('array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $redirectUrlParts); + assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $redirectUrlParts); if (true === array_key_exists('query', $redirectUrlParts)) { - assertType('array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query: string, fragment?: string}', $redirectUrlParts); + assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query: string, fragment?: string}', $redirectUrlParts); $redirectServer['QUERY_STRING'] = $redirectUrlParts['query']; } - assertType('array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $redirectUrlParts); + assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $redirectUrlParts); return 'foo'; } diff --git a/tests/PHPStan/Analyser/data/bug-3013.php b/tests/PHPStan/Analyser/data/bug-3013.php new file mode 100644 index 0000000000..7039b43910 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-3013.php @@ -0,0 +1,59 @@ +', $foo); + + $bar = $this->intOrNull(); + assertType('int|null', $bar); + + if (in_array($bar, $foo, true)) { + assertType('non-empty-array', $foo); + assertType('int', $bar); + return; + } + assertType('array', $foo); + assertType('int|null', $bar); + + if (in_array($bar, $foo, true) === true) { + assertType('int', $bar); + return; + } + assertType('array', $foo); + assertType('int|null', $bar); + } + + + public function intOrNull(): ?int + { + return rand() === 2 ? null : rand(); + } + + /** + * @param array{0: 1, 1?: 2} $foo + */ + public function testArrayKeyExists($foo): void + { + assertType("array{0: 1, 1?: 2}", $foo); + + $bar = 1; + assertType("1", $bar); + + if (array_key_exists($bar, $foo) === true) { + assertType("array{1, 2}", $foo); + assertType("1", $bar); + return; + } + + assertType("array{1}", $foo); + assertType("1", $bar); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-3019.php b/tests/PHPStan/Analyser/data/bug-3019.php new file mode 100644 index 0000000000..1ea4949c3f --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-3019.php @@ -0,0 +1,35 @@ +sayHi()); - assertType('int', $hw->sayHello()); + assertType('string', $hw->sayHello()); }; interface DecoratorInterface diff --git a/tests/PHPStan/Analyser/data/bug-3269.php b/tests/PHPStan/Analyser/data/bug-3269.php index f7089246d9..4a0d7fef71 100644 --- a/tests/PHPStan/Analyser/data/bug-3269.php +++ b/tests/PHPStan/Analyser/data/bug-3269.php @@ -20,7 +20,7 @@ public static function bar(array $intervalGroups): void } } - assertType("array", $borders); + assertType("list", $borders); foreach ($borders as $border) { assertType("array{version: string, operator: string, side: 'end'|'start'}", $border); diff --git a/tests/PHPStan/Analyser/data/bug-3312.php b/tests/PHPStan/Analyser/data/bug-3312.php new file mode 100644 index 0000000000..669ea3976f --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-3312.php @@ -0,0 +1,12 @@ + 'een', 'two' => 'twee', 'three' => 'drie']; + usort($arr, 'strcmp'); + assertType("non-empty-list<'drie'|'een'|'twee'>", $arr); +} diff --git a/tests/PHPStan/Analyser/data/bug-3351.php b/tests/PHPStan/Analyser/data/bug-3351.php index 2de71cddc2..468597455b 100644 --- a/tests/PHPStan/Analyser/data/bug-3351.php +++ b/tests/PHPStan/Analyser/data/bug-3351.php @@ -1,5 +1,7 @@ combine($a, $b); - assertType('array|false', $c); + assertType("array<'a'|'b'|'c', 1|2|3>|false", $c); assertType('array{a: 1, b: 2, c: 3}', array_combine($a, $b)); } diff --git a/tests/PHPStan/Analyser/data/bug-3677.php b/tests/PHPStan/Analyser/data/bug-3677.php index 0280d14773..e01f685ec3 100644 --- a/tests/PHPStan/Analyser/data/bug-3677.php +++ b/tests/PHPStan/Analyser/data/bug-3677.php @@ -61,8 +61,7 @@ public function sayGoodbye(): void { [$first, $second] = $this->getValue(); if ($first || $second) { - // this assert passes but the ternary breaks the next assert - // assertType(Field::class, $first ?: $second); + assertType(Field::class, $first ?: $second); assertType(Field::class, $first ?? $second); } } @@ -71,8 +70,7 @@ public function sayGoodbye2(): void { [$first, $second] = $this->getValue(); if ($first || $second) { - // this assert passes but the ternary breaks the next assert - // assertType(Field::class, $first ? $first : $second); + assertType(Field::class, $first ? $first : $second); assertType(Field::class, $first ?? $second); } } diff --git a/tests/PHPStan/Analyser/data/bug-3789.php b/tests/PHPStan/Analyser/data/bug-3789.php new file mode 100644 index 0000000000..133ce49b50 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-3789.php @@ -0,0 +1,23 @@ +', $lengths); + assertType('non-empty-list', $lengths); } public static function getInt(): int diff --git a/tests/PHPStan/Analyser/data/bug-3961-php8.php b/tests/PHPStan/Analyser/data/bug-3961-php8.php index 93b19e3857..fadfcd7209 100644 --- a/tests/PHPStan/Analyser/data/bug-3961-php8.php +++ b/tests/PHPStan/Analyser/data/bug-3961-php8.php @@ -9,13 +9,13 @@ class Foo public function doFoo(string $v, string $d, $m): void { - assertType('non-empty-array', explode('.', $v)); + assertType('non-empty-list', explode('.', $v)); assertType('*NEVER*', explode('', $v)); - assertType('array', explode('.', $v, -2)); - assertType('non-empty-array', explode('.', $v, 0)); - assertType('non-empty-array', explode('.', $v, 1)); - assertType('non-empty-array', explode($d, $v)); - assertType('non-empty-array', explode($m, $v)); + assertType('list', explode('.', $v, -2)); + assertType('non-empty-list', explode('.', $v, 0)); + assertType('non-empty-list', explode('.', $v, 1)); + assertType('list', explode($d, $v)); + assertType('list', explode($m, $v)); } } diff --git a/tests/PHPStan/Analyser/data/bug-3961.php b/tests/PHPStan/Analyser/data/bug-3961.php index 35a1a2ba11..5482601dd4 100644 --- a/tests/PHPStan/Analyser/data/bug-3961.php +++ b/tests/PHPStan/Analyser/data/bug-3961.php @@ -9,13 +9,13 @@ class Foo public function doFoo(string $v, string $d, $m): void { - assertType('non-empty-array', explode('.', $v)); + assertType('non-empty-list', explode('.', $v)); assertType('false', explode('', $v)); - assertType('array', explode('.', $v, -2)); - assertType('non-empty-array', explode('.', $v, 0)); - assertType('non-empty-array', explode('.', $v, 1)); - assertType('non-empty-array|false', explode($d, $v)); - assertType('(non-empty-array|false)', explode($m, $v)); + assertType('list', explode('.', $v, -2)); + assertType('non-empty-list', explode('.', $v, 0)); + assertType('non-empty-list', explode('.', $v, 1)); + assertType('list|false', explode($d, $v)); + assertType('(list|false)', explode($m, $v)); } } diff --git a/tests/PHPStan/Analyser/data/bug-3981.php b/tests/PHPStan/Analyser/data/bug-3981.php index 2a1929cf1a..5656b80018 100644 --- a/tests/PHPStan/Analyser/data/bug-3981.php +++ b/tests/PHPStan/Analyser/data/bug-3981.php @@ -13,8 +13,8 @@ class Foo */ public function doFoo(string $s, string $nonEmptyString): void { - assertType('string|false', strtok($s, ' ')); - assertType('string', strtok($nonEmptyString, ' ')); + assertType('non-empty-string|false', strtok($s, ' ')); + assertType('non-empty-string', strtok($nonEmptyString, ' ')); assertType('false', strtok('', ' ')); assertType('non-empty-string', $nonEmptyString[0]); diff --git a/tests/PHPStan/Analyser/data/bug-3993.php b/tests/PHPStan/Analyser/data/bug-3993.php index 4c106dbab8..e472a0d68c 100644 --- a/tests/PHPStan/Analyser/data/bug-3993.php +++ b/tests/PHPStan/Analyser/data/bug-3993.php @@ -17,7 +17,7 @@ public function doFoo($arguments) array_shift($arguments); - assertType('mixed~null', $arguments); + assertType('array', $arguments); assertType('int<0, max>', count($arguments)); } diff --git a/tests/PHPStan/Analyser/data/bug-4091.php b/tests/PHPStan/Analyser/data/bug-4091.php index 0361c4eb4e..ba691e03b1 100644 --- a/tests/PHPStan/Analyser/data/bug-4091.php +++ b/tests/PHPStan/Analyser/data/bug-4091.php @@ -6,5 +6,5 @@ if (mt_rand(0,10) > 3) { echo 'Fizz'; - assertType('int', mt_rand(0,10)); + assertType('int<0, 10>', mt_rand(0,10)); } diff --git a/tests/PHPStan/Analyser/data/bug-4099.php b/tests/PHPStan/Analyser/data/bug-4099.php index d37e69c6ed..0a8d1b4b48 100644 --- a/tests/PHPStan/Analyser/data/bug-4099.php +++ b/tests/PHPStan/Analyser/data/bug-4099.php @@ -30,7 +30,7 @@ function arrayHint(array $arr): void assertType('*NEVER*', $arr); assertNativeType('array&hasOffset(\'key\')', $arr); assertType('*NEVER*', $arr['key']); - assertNativeType('mixed', $arr['key']); + assertNativeType("mixed~hasOffset('inner')", $arr['key']); throw new \Exception('need key.inner'); } diff --git a/tests/PHPStan/Analyser/data/bug-4117.php b/tests/PHPStan/Analyser/data/bug-4117.php index 4b5cbcb847..d1bca857c3 100644 --- a/tests/PHPStan/Analyser/data/bug-4117.php +++ b/tests/PHPStan/Analyser/data/bug-4117.php @@ -30,14 +30,14 @@ public function getIterator(): ArrayIterator public function broken(int $key) { $item = $this->items[$key] ?? null; - assertType('T (class Bug4117Types\GenericList, argument)|null', $item); + assertType('T of mixed~null (class Bug4117Types\GenericList, argument)|null', $item); if ($item) { assertType("T of mixed~0|0.0|''|'0'|array{}|false|null (class Bug4117Types\GenericList, argument)", $item); } else { - assertType("(array{}&T (class Bug4117Types\GenericList, argument))|(0.0&T (class Bug4117Types\GenericList, argument))|(0&T (class Bug4117Types\GenericList, argument))|(''&T (class Bug4117Types\GenericList, argument))|('0'&T (class Bug4117Types\GenericList, argument))|(T (class Bug4117Types\GenericList, argument)&false)|null", $item); + assertType("(array{}&T of mixed~null (class Bug4117Types\GenericList, argument))|(0.0&T of mixed~null (class Bug4117Types\GenericList, argument))|(0&T of mixed~null (class Bug4117Types\GenericList, argument))|(''&T of mixed~null (class Bug4117Types\GenericList, argument))|('0'&T of mixed~null (class Bug4117Types\GenericList, argument))|(T of mixed~null (class Bug4117Types\GenericList, argument)&false)|null", $item); } - assertType('T (class Bug4117Types\GenericList, argument)|null', $item); + assertType('T of mixed~null (class Bug4117Types\GenericList, argument)|null', $item); return $item; } @@ -48,7 +48,7 @@ public function broken(int $key) public function works(int $key) { $item = $this->items[$key] ?? null; - assertType('T (class Bug4117Types\GenericList, argument)|null', $item); + assertType('T of mixed~null (class Bug4117Types\GenericList, argument)|null', $item); return $item; } diff --git a/tests/PHPStan/Analyser/data/bug-4188.php b/tests/PHPStan/Analyser/data/bug-4188.php index 07a44f9458..3a7c514215 100644 --- a/tests/PHPStan/Analyser/data/bug-4188.php +++ b/tests/PHPStan/Analyser/data/bug-4188.php @@ -1,6 +1,6 @@ = 7.4 -namespace Bug4188; +namespace Bug4188Types; interface A {} interface B {} @@ -18,7 +18,7 @@ function ($param): bool { return $param instanceof B; }, ); - assertType('array', $filtered); + assertType('array', $filtered); $this->onlyB($filtered); } @@ -30,7 +30,7 @@ public function setShort(array $data): void $data, fn($param): bool => $param instanceof B, ); - assertType('array', $filtered); + assertType('array', $filtered); $this->onlyB($filtered); } diff --git a/tests/PHPStan/Analyser/data/bug-4207.php b/tests/PHPStan/Analyser/data/bug-4207.php index 0a7ae998d4..756eef4c9f 100644 --- a/tests/PHPStan/Analyser/data/bug-4207.php +++ b/tests/PHPStan/Analyser/data/bug-4207.php @@ -5,6 +5,6 @@ use function PHPStan\Testing\assertType; function (): void { - assertType('non-empty-array>', range(1, 10000)); - assertType('non-empty-array>', range(10000, 1)); + assertType('non-empty-list>', range(1, 10000)); + assertType('non-empty-list>', range(10000, 1)); }; diff --git a/tests/PHPStan/Analyser/data/bug-4302b.php b/tests/PHPStan/Analyser/data/bug-4302b.php new file mode 100644 index 0000000000..ff24b6a689 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4302b.php @@ -0,0 +1,20 @@ +', array_keys($meters)); - assertType('non-empty-array', array_values($meters)); + assertType('non-empty-list<(int|string)>', array_keys($meters)); + assertType('non-empty-list', array_values($meters)); }; diff --git a/tests/PHPStan/Analyser/data/bug-4434.php b/tests/PHPStan/Analyser/data/bug-4434.php index 7e0b97d7fb..a1f3bea048 100644 --- a/tests/PHPStan/Analyser/data/bug-4434.php +++ b/tests/PHPStan/Analyser/data/bug-4434.php @@ -13,8 +13,8 @@ public function testSendEmailToLog(): void assertType('int<5, max>', PHP_MAJOR_VERSION); assertType('int<5, max>', \PHP_MAJOR_VERSION); if (PHP_MAJOR_VERSION === 7) { - assertType('int', PHP_MAJOR_VERSION); - assertType('int', \PHP_MAJOR_VERSION); + assertType('7', PHP_MAJOR_VERSION); + assertType('7', \PHP_MAJOR_VERSION); } else { assertType('int<5, 6>|int<8, max>', PHP_MAJOR_VERSION); assertType('int<5, 6>|int<8, max>', \PHP_MAJOR_VERSION); @@ -31,8 +31,8 @@ public function testSendEmailToLog(): void assertType('int<5, max>', PHP_MAJOR_VERSION); assertType('int<5, max>', \PHP_MAJOR_VERSION); if (PHP_MAJOR_VERSION === 100) { - assertType('int', PHP_MAJOR_VERSION); - assertType('int', \PHP_MAJOR_VERSION); + assertType('100', PHP_MAJOR_VERSION); + assertType('100', \PHP_MAJOR_VERSION); } else { assertType('int<5, 99>|int<101, max>', PHP_MAJOR_VERSION); assertType('int<5, 99>|int<101, max>', \PHP_MAJOR_VERSION); diff --git a/tests/PHPStan/Analyser/data/bug-4498.php b/tests/PHPStan/Analyser/data/bug-4498.php index 19e878c763..ad07baa3db 100644 --- a/tests/PHPStan/Analyser/data/bug-4498.php +++ b/tests/PHPStan/Analyser/data/bug-4498.php @@ -38,7 +38,7 @@ public function fcn(iterable $iterable): iterable public function bar(iterable $iterable): iterable { if (is_array($iterable)) { - assertType('array', $iterable); + assertType('array<((int&TKey (method Bug4498\Foo::bar(), argument))|(string&TKey (method Bug4498\Foo::bar(), argument))), TValue (method Bug4498\Foo::bar(), argument)>', $iterable); return $iterable; } diff --git a/tests/PHPStan/Analyser/data/bug-4499.php b/tests/PHPStan/Analyser/data/bug-4499.php index 046da4efa1..fe65958259 100644 --- a/tests/PHPStan/Analyser/data/bug-4499.php +++ b/tests/PHPStan/Analyser/data/bug-4499.php @@ -11,7 +11,7 @@ class Foo function thing(array $things) : void{ switch(count($things)){ case 1: - assertType('non-empty-array', $things); + assertType('array{int}', $things); assertType('int', array_shift($things)); } } diff --git a/tests/PHPStan/Analyser/data/bug-4504.php b/tests/PHPStan/Analyser/data/bug-4504.php index f70d3c9567..ceab5de4e2 100644 --- a/tests/PHPStan/Analyser/data/bug-4504.php +++ b/tests/PHPStan/Analyser/data/bug-4504.php @@ -14,7 +14,7 @@ public function sayHello($models): void assertType('Bug4504TypeInference\A', $v); } - assertType('array{}|Iterator', $models); + assertType('Iterator', $models); } } diff --git a/tests/PHPStan/Analyser/data/bug-4545.php b/tests/PHPStan/Analyser/data/bug-4545.php index a7162e9f79..e7f48619cd 100644 --- a/tests/PHPStan/Analyser/data/bug-4545.php +++ b/tests/PHPStan/Analyser/data/bug-4545.php @@ -33,7 +33,7 @@ function compareMaps(Map $firstMap, Map $secondMap, Closure $comparator): Set foreach ($intersect as $key) { assertType('TValue1 (method Bug4545\Foo::compareMaps(), argument)', $firstMap->get($key)); assertType('TValue2 (method Bug4545\Foo::compareMaps(), argument)', $secondMap->get($key)); - assertType('int|TValue2 (method Bug4545\Foo::compareMaps(), argument)', $secondMap->get($key, 1)); + assertType('1|TValue2 (method Bug4545\Foo::compareMaps(), argument)', $secondMap->get($key, 1)); } return $keys; diff --git a/tests/PHPStan/Analyser/data/bug-4565.php b/tests/PHPStan/Analyser/data/bug-4565.php new file mode 100644 index 0000000000..af941f8098 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4565.php @@ -0,0 +1,19 @@ + ''] + $variables['attributes']; + assertType('non-empty-array', $attributes); + if (!empty($variables['button'])) { + assertType('non-empty-array', $attributes); + $attributes['type'] = 'button'; + assertType("hasOffsetValue('type', 'button')&non-empty-array", $attributes); + unset($attributes['href']); + assertType("array&hasOffsetValue('type', 'button')", $attributes); + } + assertType('array', $attributes); + return $attributes; +} diff --git a/tests/PHPStan/Analyser/data/bug-4587.php b/tests/PHPStan/Analyser/data/bug-4587.php index b0643a055e..644b9d7b79 100644 --- a/tests/PHPStan/Analyser/data/bug-4587.php +++ b/tests/PHPStan/Analyser/data/bug-4587.php @@ -16,7 +16,7 @@ public function a(): void return $result; }, $results); - assertType('array', $type); + assertType('list', $type); } public function b(): void @@ -32,6 +32,6 @@ public function b(): void return $result; }, $results); - assertType('array', $type); + assertType('list', $type); } } diff --git a/tests/PHPStan/Analyser/data/bug-4606.php b/tests/PHPStan/Analyser/data/bug-4606.php index 1cf9cf4a32..aa06628417 100644 --- a/tests/PHPStan/Analyser/data/bug-4606.php +++ b/tests/PHPStan/Analyser/data/bug-4606.php @@ -11,7 +11,7 @@ */ assertType(Foo::class, $this); -assertType('array', $assigned); +assertType('list', $assigned); /** diff --git a/tests/PHPStan/Analyser/data/bug-4657.php b/tests/PHPStan/Analyser/data/bug-4657.php index 3cc65c9ed9..db175d8a6d 100644 --- a/tests/PHPStan/Analyser/data/bug-4657.php +++ b/tests/PHPStan/Analyser/data/bug-4657.php @@ -20,3 +20,39 @@ function (): void { assertType('null', $other); assertNativeType('null', $other); }; + +function (): void { + $value = null; + $other = null; + $callback = function () use (&$value, &$other) : void { + if (rand(0, 1)) { + $value = new DateTime(); + } + }; + $callback(); + + assertType('DateTime|null', $value); + assertNativeType('DateTime|null', $value); + + assertType('null', $other); + assertNativeType('null', $other); +}; + +function (): void { + $value = null; + $other = null; + $callback = function () use (&$value, &$other) : void { + if (rand(0, 1)) { + return; + } + + $value = new DateTime(); + }; + $callback(); + + assertType('DateTime|null', $value); + assertNativeType('DateTime|null', $value); + + assertType('null', $other); + assertNativeType('null', $other); +}; diff --git a/tests/PHPStan/Analyser/data/bug-4711.php b/tests/PHPStan/Analyser/data/bug-4711.php index 8d25957907..46bc69565f 100644 --- a/tests/PHPStan/Analyser/data/bug-4711.php +++ b/tests/PHPStan/Analyser/data/bug-4711.php @@ -12,8 +12,8 @@ function x(string $string): void { return; } - assertType('non-empty-array', explode($string, '')); - assertType('non-empty-array', explode($string[0], '')); + assertType('non-empty-list', explode($string, '')); + assertType('non-empty-list', explode($string[0], '')); } } diff --git a/tests/PHPStan/Analyser/data/bug-4733.php b/tests/PHPStan/Analyser/data/bug-4733.php index 39961cc464..dec6f9bd3b 100644 --- a/tests/PHPStan/Analyser/data/bug-4733.php +++ b/tests/PHPStan/Analyser/data/bug-4733.php @@ -23,6 +23,23 @@ public function getDescription(?\DateTimeImmutable $start, ?string $someObject): assertType('string', $someObject); } + public function getDescriptionn(?\DateTimeImmutable $start, ?string $someObject): void + { + if ($start !== null && $someObject !== null) { + return; + } + + // $start === null || $someObject === null + + if ($start === null) { + return; + } + + // $start !== null therefore $someObject === null + + assertType('null', $someObject); + } + public function getDescription2(?\DateTimeImmutable $start, ?string $someObject): void { if ($start !== null || $someObject !== null) { diff --git a/tests/PHPStan/Analyser/data/bug-4885.php b/tests/PHPStan/Analyser/data/bug-4885.php index 6f5a4e64cd..c047e0e41e 100644 --- a/tests/PHPStan/Analyser/data/bug-4885.php +++ b/tests/PHPStan/Analyser/data/bug-4885.php @@ -1,6 +1,6 @@ = 8.0 -namespace Bug4885; +namespace Bug4885Types; use function PHPStan\Testing\assertType; diff --git a/tests/PHPStan/Analyser/data/bug-4902-php8.php b/tests/PHPStan/Analyser/data/bug-4902-php8.php index 016ac2586f..046f2ac338 100644 --- a/tests/PHPStan/Analyser/data/bug-4902-php8.php +++ b/tests/PHPStan/Analyser/data/bug-4902-php8.php @@ -1,6 +1,6 @@ = 7.4 -namespace Bug4902; +namespace Bug4902Php8; use function PHPStan\Testing\assertType; @@ -44,10 +44,10 @@ function wrap($value): Wrapper * @param Wrapper ...$wrappers */ function unwrapAllAndWrapAgain(Wrapper ...$wrappers): void { - assertType('array', array_map(function (Wrapper $item) { + assertType('array', array_map(function (Wrapper $item) { return $this->unwrap($item); }, $wrappers)); - assertType('array', array_map(fn (Wrapper $item) => $this->unwrap($item), $wrappers)); + assertType('array', array_map(fn (Wrapper $item) => $this->unwrap($item), $wrappers)); } } diff --git a/tests/PHPStan/Analyser/data/bug-4902.php b/tests/PHPStan/Analyser/data/bug-4902.php index e56cd001dd..8e553747ac 100644 --- a/tests/PHPStan/Analyser/data/bug-4902.php +++ b/tests/PHPStan/Analyser/data/bug-4902.php @@ -44,10 +44,10 @@ function wrap($value): Wrapper * @param Wrapper ...$wrappers */ function unwrapAllAndWrapAgain(Wrapper ...$wrappers): void { - assertType('array', array_map(function (Wrapper $item) { + assertType('list', array_map(function (Wrapper $item) { return $this->unwrap($item); }, $wrappers)); - assertType('array', array_map(fn (Wrapper $item) => $this->unwrap($item), $wrappers)); + assertType('list', array_map(fn (Wrapper $item) => $this->unwrap($item), $wrappers)); } } diff --git a/tests/PHPStan/Analyser/data/bug-4907.php b/tests/PHPStan/Analyser/data/bug-4907.php new file mode 100644 index 0000000000..242aa29cc7 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4907.php @@ -0,0 +1,15 @@ + $foo) { + // ... + } + + assertType('5|6|7', $foo); +} diff --git a/tests/PHPStan/Analyser/data/bug-5086.php b/tests/PHPStan/Analyser/data/bug-5086.php new file mode 100644 index 0000000000..6018447ba7 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5086.php @@ -0,0 +1,26 @@ +doFoo())) { + return; + } + + assertType(stdClass::class, $obj); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-5091.php b/tests/PHPStan/Analyser/data/bug-5091.php new file mode 100644 index 0000000000..626843d9cf --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5091.php @@ -0,0 +1,175 @@ + '']; + } + } +} + +namespace Bug5091 { + + /** + * @phpstan-type MyType array{foobar: string} + */ + trait MyTrait + { + /** + * @return array + */ + public function MyMethod(): array + { + return [['foobar' => 'foo']]; + } + } + + class MyClass + { + use MyTrait; + } + + /** + * @phpstan-type TypeArrayAjaxResponse array{ + * message : string, + * status : int, + * success : bool, + * value : null|float|int|string, + * } + */ + trait MyTrait2 + { + /** @return TypeArrayAjaxResponse */ + protected function getAjaxResponse(): array + { + return [ + "message" => "test", + "status" => 200, + "success" => true, + "value" => 5, + ]; + } + } + + class MyController + { + use MyTrait2; + } + + + /** + * @phpstan-type X string + */ + class Types {} + + /** + * @phpstan-import-type X from Types + */ + trait t { + /** @return X */ + public function getX() { + return "123"; + } + } + + class aClass + { + use t; + } + + /** + * @phpstan-import-type X from Types + */ + class Z { + /** @return X */ + public function getX() { // works as expected + return "123"; + } + } + + /** + * @phpstan-type SomePhpstanType array{ + * property: mixed + * } + */ + trait TraitWithType + { + /** + * @phpstan-return SomePhpstanType + */ + protected function get(): array + { + return [ + 'property' => 'something', + ]; + } + } + + /** + * @phpstan-import-type SomePhpstanType from TraitWithType + */ + class ClassWithTraitWithType + { + use TraitWithType; + + /** + * @phpstan-return SomePhpstanType + */ + public function SomeMethod(): array + { + return $this->get(); + } + } + + /** + * @phpstan-type FooJson array{bar: string} + */ + trait Foo { + /** + * @phpstan-return FooJson + */ + public function sayHello(\DateTime $date): array + { + return [ + 'bar'=> 'baz' + ]; + } + } + + /** + * @phpstan-import-type FooJson from Foo + */ + class HelloWorld + { + use Foo; + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-5172.php b/tests/PHPStan/Analyser/data/bug-5172.php new file mode 100644 index 0000000000..63519d8ca7 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5172.php @@ -0,0 +1,28 @@ += 8.0 + +namespace Bug5172; + +use function PHPStan\Testing\assertType; + +class Period +{ + public mixed $from; + public mixed $to; + + public function year(): ?int + { + assertType('mixed', $this->from); + assertType('mixed', $this->to); + + // let's say $this->from === null && $model->to === null + + if ($this->from?->year !== $this->to?->year) { + return null; + } + + assertType('mixed', $this->from); + assertType('mixed', $this->to); + + return $this->from?->year; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-5287-php81.php b/tests/PHPStan/Analyser/data/bug-5287-php81.php index 7884bbd6c6..c0eef41b30 100644 --- a/tests/PHPStan/Analyser/data/bug-5287-php81.php +++ b/tests/PHPStan/Analyser/data/bug-5287-php81.php @@ -1,6 +1,6 @@ ', $arrSpread); + assertType('list', $arrSpread); } /** @@ -19,7 +19,7 @@ function foo(array $arr): void function foo2(array $arr): void { $arrSpread = [...$arr]; - assertType('array', $arrSpread); + assertType('list', $arrSpread); } /** @@ -28,13 +28,13 @@ function foo2(array $arr): void function foo3(array $arr): void { $arrSpread = [...$arr]; - assertType('non-empty-array', $arrSpread); + assertType('non-empty-list', $arrSpread); } /** * @param non-empty-array $arr */ -function foo3(array $arr): void +function foo4(array $arr): void { $arrSpread = [...$arr]; assertType('non-empty-array', $arrSpread); @@ -43,7 +43,7 @@ function foo3(array $arr): void /** * @param non-empty-array $arr */ -function foo4(array $arr): void +function foo5(array $arr): void { $arrSpread = [...$arr]; assertType('non-empty-array', $arrSpread); diff --git a/tests/PHPStan/Analyser/data/bug-5287.php b/tests/PHPStan/Analyser/data/bug-5287.php index dab904a7f1..cf1cda0791 100644 --- a/tests/PHPStan/Analyser/data/bug-5287.php +++ b/tests/PHPStan/Analyser/data/bug-5287.php @@ -10,7 +10,7 @@ function foo(array $arr): void { $arrSpread = [...$arr]; - assertType('array', $arrSpread); + assertType('list', $arrSpread); } /** @@ -19,7 +19,7 @@ function foo(array $arr): void function foo2(array $arr): void { $arrSpread = [...$arr]; - assertType('array', $arrSpread); + assertType('list', $arrSpread); } /** @@ -28,25 +28,25 @@ function foo2(array $arr): void function foo3(array $arr): void { $arrSpread = [...$arr]; - assertType('non-empty-array', $arrSpread); + assertType('non-empty-list', $arrSpread); } /** * @param non-empty-array $arr */ -function foo3(array $arr): void +function foo4(array $arr): void { $arrSpread = [...$arr]; - assertType('non-empty-array', $arrSpread); + assertType('non-empty-list', $arrSpread); } /** * @param non-empty-array $arr */ -function foo4(array $arr): void +function foo5(array $arr): void { $arrSpread = [...$arr]; - assertType('non-empty-array', $arrSpread); + assertType('non-empty-list', $arrSpread); } /** diff --git a/tests/PHPStan/Analyser/data/bug-5312.php b/tests/PHPStan/Analyser/data/bug-5312.php new file mode 100644 index 0000000000..95428adc4b --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5312.php @@ -0,0 +1,14 @@ + + */ +interface Updatable +{ + /** + * @param T $object + */ + public function update(Updatable $object): void; +} diff --git a/tests/PHPStan/Analyser/data/bug-5316.php b/tests/PHPStan/Analyser/data/bug-5316.php index b12f0be55b..13dbf2a179 100644 --- a/tests/PHPStan/Analyser/data/bug-5316.php +++ b/tests/PHPStan/Analyser/data/bug-5316.php @@ -20,6 +20,6 @@ function (): void { foreach ($array as $name => $elements) { assertType('bool', count($elements) > 0); - assertType('array', $elements); + assertType('list<1|2|3>', $elements); } }; diff --git a/tests/PHPStan/Analyser/data/bug-5390.php b/tests/PHPStan/Analyser/data/bug-5390.php new file mode 100644 index 0000000000..57332bebe3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5390.php @@ -0,0 +1,19 @@ +b->someMethod(); + } +} +/** @mixin A */ +class B +{ + +} diff --git a/tests/PHPStan/Analyser/data/bug-5668.php b/tests/PHPStan/Analyser/data/bug-5668.php index 5633ce0ab0..65f66601bd 100644 --- a/tests/PHPStan/Analyser/data/bug-5668.php +++ b/tests/PHPStan/Analyser/data/bug-5668.php @@ -7,7 +7,6 @@ class Foo { - /** * @param array $in */ @@ -27,9 +26,11 @@ function has2(array $in): void /** * @param non-empty-array $in */ - function has3(array $in): void + function has3(array $in, string $s): void { assertType('bool', in_array('test', $in, true)); + assertType('bool', in_array(rand() ? 'test' : 'bar', $in, true)); + assertType('bool', in_array($s, $in, true)); } @@ -41,4 +42,12 @@ function has4(array $in): void assertType('true', in_array('test', $in, true)); } + /** + * @param non-empty-array $in + */ + function has5(array $in): void + { + assertType('bool', in_array('test', $in, true)); + } + } diff --git a/tests/PHPStan/Analyser/data/bug-5698-php7.php b/tests/PHPStan/Analyser/data/bug-5698-php7.php index 10d2ceb899..84deaabdcb 100644 --- a/tests/PHPStan/Analyser/data/bug-5698-php7.php +++ b/tests/PHPStan/Analyser/data/bug-5698-php7.php @@ -9,8 +9,8 @@ class FooPHP7 { function foo(int ...$foo): void { - assertType('array', $foo); - assertNativeType('array', $foo); + assertType('list', $foo); + assertNativeType('list', $foo); } } diff --git a/tests/PHPStan/Analyser/data/bug-5782b-php7.php b/tests/PHPStan/Analyser/data/bug-5782b-php7.php new file mode 100644 index 0000000000..c25319df95 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5782b-php7.php @@ -0,0 +1,27 @@ + $iterable + * @param-out iterable $iterable + */ + public static function act(iterable &$iterable): void + { + } +} + +function doFoo() { + /** @var HelloWorld[] $a */ + $a = []; + + assertType('array', $a); + IterableHelper::act($a); + assertType('iterable', $a); + +} + + diff --git a/tests/PHPStan/Analyser/data/bug-5843.php b/tests/PHPStan/Analyser/data/bug-5843.php index e7397de3ae..9b244969dd 100644 --- a/tests/PHPStan/Analyser/data/bug-5843.php +++ b/tests/PHPStan/Analyser/data/bug-5843.php @@ -15,7 +15,7 @@ function doFoo(object $object): void assertType(\DateTime::class, $object); break; case \Throwable::class: - assertType(\Throwable::class, $object); + assertType('Throwable', $object); break; } } @@ -29,7 +29,7 @@ function doFoo(object $object): void { match ($object::class) { \DateTime::class => assertType(\DateTime::class, $object), - \Throwable::class => assertType(\Throwable::class, $object), + \Throwable::class => assertType('Throwable', $object), }; } diff --git a/tests/PHPStan/Analyser/data/bug-5961.php b/tests/PHPStan/Analyser/data/bug-5961.php new file mode 100644 index 0000000000..f38c663014 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5961.php @@ -0,0 +1,11 @@ +|null */ + private ?Generator $nullableGenerator; + + /** @var Generator */ + private Generator $regularGenerator; + + public function iterate() : void{ + foreach($this->nullableGenerator as $object){ + assertType(Block::class, $object); + } + + foreach($this->regularGenerator as $object){ + assertType(Block::class, $object); + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6070.php b/tests/PHPStan/Analyser/data/bug-6070.php index ac1355eee0..aa0d318458 100644 --- a/tests/PHPStan/Analyser/data/bug-6070.php +++ b/tests/PHPStan/Analyser/data/bug-6070.php @@ -16,7 +16,7 @@ public function getNonEmptyArray(): array { $nonEmptyArray[] = 1; } - assertType('non-empty-array>', $nonEmptyArray); + assertType('non-empty-list>', $nonEmptyArray); return $nonEmptyArray; } diff --git a/tests/PHPStan/Analyser/data/bug-6138.php b/tests/PHPStan/Analyser/data/bug-6138.php index e2a353905d..f26da16055 100644 --- a/tests/PHPStan/Analyser/data/bug-6138.php +++ b/tests/PHPStan/Analyser/data/bug-6138.php @@ -24,6 +24,6 @@ shuffle( $associative ); shuffle( $unordered ); -assertType( 'non-empty-array', array_keys( $indexed ) ); -assertType( 'non-empty-array', array_keys( $associative ) ); -assertType( 'non-empty-array', array_keys( $unordered ) ); +assertType( 'non-empty-list<0|1|2>', array_keys( $indexed ) ); +assertType( 'non-empty-list<0|1|2>', array_keys( $associative ) ); +assertType( 'non-empty-list<0|1|2>', array_keys( $unordered ) ); diff --git a/tests/PHPStan/Analyser/data/bug-6160.php b/tests/PHPStan/Analyser/data/bug-6160.php index 9470109e79..b0ac5850d1 100644 --- a/tests/PHPStan/Analyser/data/bug-6160.php +++ b/tests/PHPStan/Analyser/data/bug-6160.php @@ -16,10 +16,10 @@ public static function split($flags = 0){ public static function test(): void { - self::split(94561); // should error - self::split(PREG_SPLIT_NO_EMPTY); // should work - self::split(PREG_SPLIT_DELIM_CAPTURE); // should work - self::split(PREG_SPLIT_NO_EMPTY_COPY); // should work - self::split("sdf"); // should error + $a = self::split(94561); // should error + $a = self::split(PREG_SPLIT_NO_EMPTY); // should work + $a = self::split(PREG_SPLIT_DELIM_CAPTURE); // should work + $a = self::split(PREG_SPLIT_NO_EMPTY_COPY); // should work + $a = self::split("sdf"); // should error } } diff --git a/tests/PHPStan/Analyser/data/bug-6196.php b/tests/PHPStan/Analyser/data/bug-6196.php new file mode 100644 index 0000000000..183476932d --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6196.php @@ -0,0 +1,27 @@ + zlib_decode("aaaaaaa"))); +}; diff --git a/tests/PHPStan/Analyser/data/bug-6265.php b/tests/PHPStan/Analyser/data/bug-6265.php new file mode 100644 index 0000000000..a50d962c3f --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6265.php @@ -0,0 +1,67 @@ +comments['#' . $comment['thread_parentid']])) + { + if (in_array('#' . $comment['parentid'], $lv1_keys)) + { + if (!$match) + { + for ($ii = 0;$ii < count($lv2_keys);$ii++) + { + if (!$match3) + { + for ($iii = 0;$iii < count($lv3_keys_all);$iii++) + { + if (!$match4) + { + for ($iiii = 0;$iiii < count($lv4_keys_all);$iiii++) + { + if (!$match5) + { + for ($i6 = 0;$i6 < count($lv5_keys_all);$i6++) + { + if (!$match6) + { + for ($i7 = 0;$i7 < count($lv6_keys_all);$i7++) + { + if (!$match7) + { + for ($i8 = 0;$i8 < count($lv7_keys_all);$i8++) + { + if (!$match8) + { + for ($i9 = 0;$i9 < count($lv8_keys_all);$i9++) + { + if (!$match9) + { + + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + return true; + } + } + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-6294.php b/tests/PHPStan/Analyser/data/bug-6294.php new file mode 100644 index 0000000000..8a363c1bae --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6294.php @@ -0,0 +1,39 @@ + $classString + * @phpstan-return HelloWorld1|null + */ + public function sayHello(object $object, $classString): ?object + { + if ($classString === get_class($object)) { + assertType(HelloWorld1::class, $object); + + return $object; + } + + return null; + } + + /** + * @phpstan-param HelloWorld1 $object + * @phpstan-return HelloWorld1|null + */ + public function sayHello2(object $object, object $object2): ?object + { + if (get_class($object2) === get_class($object)) { + assertType(HelloWorld1::class, $object); + + return $object; + } + + return null; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6305.php b/tests/PHPStan/Analyser/data/bug-6305.php index 5cf5d353b4..89bfea9c62 100644 --- a/tests/PHPStan/Analyser/data/bug-6305.php +++ b/tests/PHPStan/Analyser/data/bug-6305.php @@ -1,6 +1,6 @@ ', new Set([E::A, E::B])); + assertType('Ds\Set', new Set([E::A, E::B])); } } diff --git a/tests/PHPStan/Analyser/data/bug-6442.php b/tests/PHPStan/Analyser/data/bug-6442.php index 413826daa7..2bcd2309ff 100644 --- a/tests/PHPStan/Analyser/data/bug-6442.php +++ b/tests/PHPStan/Analyser/data/bug-6442.php @@ -17,7 +17,7 @@ class B extends A use T; } -new class() extends B +$a = new class() extends B { use T; }; diff --git a/tests/PHPStan/Analyser/data/bug-6462.php b/tests/PHPStan/Analyser/data/bug-6462.php new file mode 100644 index 0000000000..84c475d48a --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6462.php @@ -0,0 +1,57 @@ +getThis()); +assertType('Bug6462\Child', $child->getThis()); + +if ($base instanceof \Traversable) { + assertType('Bug6462\Base&Traversable', $base->getThis()); +} + +if ($child instanceof \Traversable) { + assertType('Bug6462\Child&Traversable', $child->getThis()); +} + +if ($fixedChild instanceof \Traversable) { + assertType('Bug6462\FixedChild&Traversable', $fixedChild->getThis()); +} diff --git a/tests/PHPStan/Analyser/data/bug-6505.php b/tests/PHPStan/Analyser/data/bug-6505.php index 0771caea9f..c5d41849d5 100644 --- a/tests/PHPStan/Analyser/data/bug-6505.php +++ b/tests/PHPStan/Analyser/data/bug-6505.php @@ -133,7 +133,7 @@ class Example public function getFactories(): void { - assertType('Bug6505\Collection>', new Collection($this->factories)); + assertType('Bug6505\Collection>', new Collection($this->factories)); } } diff --git a/tests/PHPStan/Analyser/data/bug-6613.php b/tests/PHPStan/Analyser/data/bug-6613.php new file mode 100644 index 0000000000..29898f7532 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6613.php @@ -0,0 +1,11 @@ +format('u')); +}; diff --git a/tests/PHPStan/Analyser/data/bug-6633.php b/tests/PHPStan/Analyser/data/bug-6633.php new file mode 100644 index 0000000000..3689f53ee9 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6633.php @@ -0,0 +1,75 @@ +name . $this->version; + } +} + +class ServiceRedis +{ + public function __construct( + private string $name, + private string $version, + private bool $persistent, + ) {} + + public function touchAll() : string{ + return $this->persistent ? $this->name : $this->version; + } +} + +function test(?string $type = NULL) : void { + $types = [ + 'solr' => [ + 'label' => 'SOLR Search', + 'data_class' => CreateServiceSolrData::class, + 'to_entity' => function (CreateServiceSolrData $data) { + assert($data->name !== NULL && $data->version !== NULL, "Incorrect form validation"); + return new ServiceSolr($data->name, $data->version); + }, + ], + 'redis' => [ + 'label' => 'Redis', + 'data_class' => CreateServiceRedisData::class, + 'to_entity' => function (CreateServiceRedisData $data) { + assert($data->name !== NULL && $data->version !== NULL && $data->persistent !== NULL, "Incorrect form validation"); + return new ServiceRedis($data->name, $data->version, $data->persistent); + }, + ], + ]; + + if ($type === NULL || !isset($types[$type])) { + throw new \RuntimeException("404 or choice form here"); + } + + $data = new $types[$type]['data_class'](); + + $service = $types[$type]['to_entity']($data); + + assertType('Bug6633\ServiceRedis|Bug6633\ServiceSolr', $service); +} diff --git a/tests/PHPStan/Analyser/data/bug-6654.php b/tests/PHPStan/Analyser/data/bug-6654.php index ad7fd9e1eb..99508b2d6b 100644 --- a/tests/PHPStan/Analyser/data/bug-6654.php +++ b/tests/PHPStan/Analyser/data/bug-6654.php @@ -8,12 +8,12 @@ class Foo { function doFoo() { $data = ''; $flags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR; - assertType('string',json_encode($data, $flags)); + assertType('non-empty-string',json_encode($data, $flags)); if (rand(0, 1)) { $flags |= JSON_FORCE_OBJECT; } - assertType('string', json_encode($data, $flags)); + assertType('non-empty-string', json_encode($data, $flags)); } } diff --git a/tests/PHPStan/Analyser/data/bug-6695.php b/tests/PHPStan/Analyser/data/bug-6695.php index 094c142ce7..396548a4aa 100644 --- a/tests/PHPStan/Analyser/data/bug-6695.php +++ b/tests/PHPStan/Analyser/data/bug-6695.php @@ -11,7 +11,7 @@ enum Foo: int public function toCollection(): void { - assertType('Bug6695\Collection', $this->collect(self::cases())); + assertType('Bug6695\Collection', $this->collect(self::cases())); } /** diff --git a/tests/PHPStan/Analyser/data/bug-6842.php b/tests/PHPStan/Analyser/data/bug-6842.php index ee3b1232c4..7565f71c85 100644 --- a/tests/PHPStan/Analyser/data/bug-6842.php +++ b/tests/PHPStan/Analyser/data/bug-6842.php @@ -30,6 +30,32 @@ public function getScheduledEvents( } } + /** + * @template T of \DateTimeInterface|\DateTime|\DateTimeImmutable + * + * @param T $startDate + * @param T $endDate + * + * @return \Iterator + */ + public function getScheduledEvents2( + \DateTimeInterface $startDate, + \DateTimeInterface $endDate + ): \Iterator { + $interval = \DateInterval::createFromDateString('1 day'); + + /** @var \DatePeriod<\DateTimeInterface, \DateTimeInterface, null>&iterable $datePeriod */ + $datePeriod = new \DatePeriod($startDate, $interval, $endDate); + + foreach ($datePeriod as $dateTime) { + $scheduledEvent = $this->createScheduledEventFromSchedule($dateTime); + + if ($scheduledEvent >= $startDate) { + yield $scheduledEvent; + } + } + } + /** * @template T of \DateTimeInterface * diff --git a/tests/PHPStan/Analyser/data/bug-6859.php b/tests/PHPStan/Analyser/data/bug-6859.php index 912395e5e9..f12e61342f 100644 --- a/tests/PHPStan/Analyser/data/bug-6859.php +++ b/tests/PHPStan/Analyser/data/bug-6859.php @@ -11,14 +11,14 @@ class HelloWorld public function keys($body) { if (array_key_exists("someParam", $body)) { - assertType('non-empty-array', array_keys($body)); + assertType('non-empty-list<(int|string)>', array_keys($body)); $someKeys = array_filter( array_keys($body), fn ($key) => preg_match("/^somePattern[0-9]+$/", $key) ); - assertType('array', $someKeys); + assertType('array, (int|string)>', $someKeys); if (count($someKeys) > 0) { return 1; @@ -30,7 +30,7 @@ public function keys($body) public function values($body) { if (array_key_exists("someParam", $body)) { - assertType('non-empty-array', array_values($body)); + assertType('non-empty-list', array_values($body)); } } } diff --git a/tests/PHPStan/Analyser/data/bug-7012.php b/tests/PHPStan/Analyser/data/bug-7012.php index 536c646980..26ad352969 100644 --- a/tests/PHPStan/Analyser/data/bug-7012.php +++ b/tests/PHPStan/Analyser/data/bug-7012.php @@ -9,6 +9,7 @@ enum Foo function test(Foo $f = Foo::BAR): void { + echo 'test'; } function test2(): void diff --git a/tests/PHPStan/Analyser/data/bug-7031.php b/tests/PHPStan/Analyser/data/bug-7031.php index e1cd193b1e..a325a67d1f 100644 --- a/tests/PHPStan/Analyser/data/bug-7031.php +++ b/tests/PHPStan/Analyser/data/bug-7031.php @@ -2,6 +2,8 @@ namespace Bug7031; +use function PHPStan\Testing\assertType; + class SomeKey {} function () { diff --git a/tests/PHPStan/Analyser/data/bug-7068.php b/tests/PHPStan/Analyser/data/bug-7068.php index e82bfaa342..97c0bda6d9 100644 --- a/tests/PHPStan/Analyser/data/bug-7068.php +++ b/tests/PHPStan/Analyser/data/bug-7068.php @@ -18,8 +18,8 @@ function merge(array ...$arrays): array { public function doFoo(): void { - assertType('array', $this->merge([1, 2], [3, 4], [5])); - assertType('array', $this->merge([1, 2], ['foo', 'bar'])); + assertType('array<1|2|3|4|5>', $this->merge([1, 2], [3, 4], [5])); + assertType('array<1|2|\'bar\'|\'foo\'>', $this->merge([1, 2], ['foo', 'bar'])); } } diff --git a/tests/PHPStan/Analyser/data/bug-7078.php b/tests/PHPStan/Analyser/data/bug-7078.php index 11b688fc15..5287dcc7cf 100644 --- a/tests/PHPStan/Analyser/data/bug-7078.php +++ b/tests/PHPStan/Analyser/data/bug-7078.php @@ -33,5 +33,5 @@ public function get(TypeDefault ...$type); function (Param $p) { $result = $p->get(new TypeDefault(1), new TypeDefault('a')); - assertType('int|string', $result); + assertType('1|\'a\'', $result); }; diff --git a/tests/PHPStan/Analyser/data/bug-7110.php b/tests/PHPStan/Analyser/data/bug-7110.php new file mode 100644 index 0000000000..9a81a94852 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7110.php @@ -0,0 +1,39 @@ +', $b); - assertType('array', $c); - assertType('array', $d); + assertType('list', $b); + assertType('list', $c); + assertType('list', $d); } } diff --git a/tests/PHPStan/Analyser/data/bug-7140.php b/tests/PHPStan/Analyser/data/bug-7140.php index 211516c042..ede86286ad 100644 --- a/tests/PHPStan/Analyser/data/bug-7140.php +++ b/tests/PHPStan/Analyser/data/bug-7140.php @@ -41,5 +41,6 @@ function foo(array $arr): void if (isset($arr['k_' . $i])) { } + echo 'test'; } } diff --git a/tests/PHPStan/Analyser/data/bug-7153.php b/tests/PHPStan/Analyser/data/bug-7153.php index 973beecf24..902764f977 100644 --- a/tests/PHPStan/Analyser/data/bug-7153.php +++ b/tests/PHPStan/Analyser/data/bug-7153.php @@ -17,6 +17,7 @@ function bleh(): ?string function blih(string $blah, string $bleh): void { + echo 'test'; } function () { diff --git a/tests/PHPStan/Analyser/data/bug-7162.php b/tests/PHPStan/Analyser/data/bug-7162.php new file mode 100644 index 0000000000..87815e4acb --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7162.php @@ -0,0 +1,35 @@ += 8.1 + +namespace Bug7162; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + + /** + * @param class-string<\BackedEnum> $enumClassString + */ + public static function casesWithLabel(string $enumClassString): void + { + foreach ($enumClassString::cases() as $unitEnum) { + assertType('BackedEnum', $unitEnum); + } + } +} + +enum Test{ + case ONE; +} + +/** + * @phpstan-template TEnum of \UnitEnum + * @phpstan-param TEnum $case + */ +function dumpCases(\UnitEnum $case) : void{ + assertType('array', $case::cases()); +} + +function dumpCases2(Test $case) : void{ + assertType('array{Bug7162\\Test::ONE}', $case::cases()); +} diff --git a/tests/PHPStan/Analyser/data/bug-7176.php b/tests/PHPStan/Analyser/data/bug-7176.php index f07a144d9a..05d1de958c 100644 --- a/tests/PHPStan/Analyser/data/bug-7176.php +++ b/tests/PHPStan/Analyser/data/bug-7176.php @@ -1,6 +1,6 @@ = 8.1 -namespace Bug7176; +namespace Bug7176Types; use function PHPStan\Testing\assertType; @@ -14,16 +14,16 @@ enum Suit function test(Suit $x): string { if ($x === Suit::Clubs) { - assertType('Bug7176\Suit::Clubs', $x); + assertType('Bug7176Types\Suit::Clubs', $x); return 'WORKS'; } - assertType('Bug7176\Suit~Bug7176\Suit::Clubs', $x); + assertType('Bug7176Types\Suit~Bug7176Types\Suit::Clubs', $x); if (in_array($x, [Suit::Spades], true)) { - assertType('Bug7176\Suit::Spades', $x); + assertType('Bug7176Types\Suit::Spades', $x); return 'DOES NOT WORK'; } - assertType('Bug7176\Suit~Bug7176\Suit::Clubs|Bug7176\Suit::Spades', $x); + assertType('Bug7176Types\Suit~Bug7176Types\Suit::Clubs|Bug7176Types\Suit::Spades', $x); return match ($x) { Suit::Hearts => 'a', diff --git a/tests/PHPStan/Analyser/data/bug-7215.php b/tests/PHPStan/Analyser/data/bug-7215.php index 10cff32204..4e278c3bb1 100644 --- a/tests/PHPStan/Analyser/data/bug-7215.php +++ b/tests/PHPStan/Analyser/data/bug-7215.php @@ -21,6 +21,6 @@ function keysAsString(array $array): array } function () { - assertType('array', keysAsString([])); - assertType('non-empty-array', keysAsString(['' => ''])); + assertType('list', keysAsString([])); + assertType('non-empty-list', keysAsString(['' => ''])); }; diff --git a/tests/PHPStan/Analyser/data/bug-7239-php8.php b/tests/PHPStan/Analyser/data/bug-7239-php8.php new file mode 100644 index 0000000000..11103c7ded --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7239-php8.php @@ -0,0 +1,36 @@ + 0) { + assertType('mixed', max($arr)); + assertType('mixed', min($arr)); + } else { + assertType('*ERROR*', max($arr)); + assertType('*ERROR*', min($arr)); + } + + assertType('array', max([], $arr)); + assertType('array', min([], $arr)); + + if (count($strings) > 0) { + assertType('string', max($strings)); + assertType('string', min($strings)); + } else { + assertType('*ERROR*', max($strings)); + assertType('*ERROR*', min($strings)); + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-7239.php b/tests/PHPStan/Analyser/data/bug-7239.php new file mode 100644 index 0000000000..b31a69e785 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7239.php @@ -0,0 +1,36 @@ + 0) { + assertType('mixed', max($arr)); + assertType('mixed', min($arr)); + } else { + assertType('false', max($arr)); + assertType('false', min($arr)); + } + + assertType('array', max([], $arr)); + assertType('array', min([], $arr)); + + if (count($strings) > 0) { + assertType('string', max($strings)); + assertType('string', min($strings)); + } else { + assertType('false', max($strings)); + assertType('false', min($strings)); + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-7291.php b/tests/PHPStan/Analyser/data/bug-7291.php new file mode 100644 index 0000000000..cae3e945b3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7291.php @@ -0,0 +1,25 @@ +foo; + + assertType('stdClass|null', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-7301.php b/tests/PHPStan/Analyser/data/bug-7301.php new file mode 100644 index 0000000000..334c6d989d --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7301.php @@ -0,0 +1,29 @@ + + */ + $arg = function () { + return ['key' => 'value']; + }; + + $result = templated($arg); + + assertType('array', $result); +}; diff --git a/tests/PHPStan/Analyser/data/bug-7519.php b/tests/PHPStan/Analyser/data/bug-7519.php new file mode 100644 index 0000000000..1fd556f0e3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7519.php @@ -0,0 +1,56 @@ +> + */ +class FooFilterIterator extends FilterIterator +{ + /** + * @param Iterator $iterator + */ + public function __construct(Iterator $iterator) + { + parent::__construct($iterator); + } + + public function accept(): bool + { + return true; + } +} + +function doFoo() { + $generator = static function (): Generator { + yield true => true; + yield false => false; + yield new stdClass => new StdClass; + yield [] => []; + }; + + $iterator = new FooFilterIterator($generator()); + + assertType('array{}|bool|stdClass', $iterator->key()); + assertType('array{}|bool|stdClass', $iterator->current()); + + $generator = static function (): Generator { + yield true => true; + yield false => false; + }; + + $iterator = new FooFilterIterator($generator()); + + assertType('bool', $iterator->key()); + assertType('bool', $iterator->current()); +} diff --git a/tests/PHPStan/Analyser/data/bug-7547.php b/tests/PHPStan/Analyser/data/bug-7547.php new file mode 100644 index 0000000000..c2a7a3ad80 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7547.php @@ -0,0 +1,17 @@ +', mb_str_split($v, 1)); +assertType('list', mb_str_split($v, 1)); diff --git a/tests/PHPStan/Analyser/data/bug-7581.php b/tests/PHPStan/Analyser/data/bug-7581.php index 73423fed8e..53b18b1252 100644 --- a/tests/PHPStan/Analyser/data/bug-7581.php +++ b/tests/PHPStan/Analyser/data/bug-7581.php @@ -110,4 +110,6 @@ function parse(array $parsed): void break; endswitch; endforeach; + + echo 'test'; } diff --git a/tests/PHPStan/Analyser/data/bug-7607.php b/tests/PHPStan/Analyser/data/bug-7607.php new file mode 100644 index 0000000000..b88d6e5c02 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7607.php @@ -0,0 +1,59 @@ +blank($url = $url ?? $this->getUrlForCurrentRequest())) { + return false; + } + + assertType('non-empty-string', $url); + $parsed = parse_url($url); + + return is_array($parsed); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-7805.php b/tests/PHPStan/Analyser/data/bug-7805.php new file mode 100644 index 0000000000..cf96fe46d5 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7805.php @@ -0,0 +1,33 @@ +", $params); + $params = $params === [] ? ['list'] : $params; + assertType("array{'list'}", $params); + assertNativeType("non-empty-array", $params); + array_unshift($params, 'help'); + assertType("array{'help', 'list'}", $params); + assertNativeType("non-empty-array", $params); + } + assertType("array{}|array{'help', 'list'}", $params); + assertNativeType('array', $params); + + return $params; +} diff --git a/tests/PHPStan/Analyser/data/bug-7913.php b/tests/PHPStan/Analyser/data/bug-7913.php new file mode 100644 index 0000000000..7afd5511db --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7913.php @@ -0,0 +1,25 @@ + : non-falsy-string + * ) $v + * @return void + */ + function foo( $k, $v ) { + if ( $k === 'a' ) { + assertType('int<0, 1>', $v); + } else { + assertType('non-falsy-string', $v); + } + } +} + +class HelloWorld2 +{ + /** + * @param string|array $name + * @param ($name is array ? null : int) $value + */ + public function setConfig($name, $value): void + { + if (is_array($name)) { + assertType('null', $value); + } else { + assertType('int', $value); + } + } + + /** + * @param string|array $name + * @param int $value + */ + public function setConfigMimicConditionalParamType($name, $value): void + { + if (is_array($name)) { + $value = null; + } + + if (is_array($name)) { + assertType('null', $value); + } else { + assertType('int', $value); + } + } +} + +/** + * @param ($isArray is false ? string : array) $data + * + * @return ($isArray is false ? string : array) + */ +function to_utf8($data, bool $isArray = false) +{ + if ($isArray) { + assertType('array', $data); + if (is_array($data)) { // always true + foreach ($data as $k => $value) { + $data[$k] = to_utf8($value, is_array($value)); + } + } else { + assertType('*NEVER*', $data); + $data = []; // dead code + } + } else { + assertType('string', $data); + $data = @iconv('UTF-8', 'UTF-8//IGNORE', $data); + } + + return $data; +} diff --git a/tests/PHPStan/Analyser/data/bug-7918.php b/tests/PHPStan/Analyser/data/bug-7918.php index 38fe267e39..2c021d53db 100644 --- a/tests/PHPStan/Analyser/data/bug-7918.php +++ b/tests/PHPStan/Analyser/data/bug-7918.php @@ -2,6 +2,9 @@ namespace Bug7918; +use function PHPStan\dumpType; +use function PHPStan\Testing\assertType; + class TestController { /** @return array */ @@ -15,6 +18,14 @@ private function rand(): bool return random_int(0, 1) > 0; } + /** + * @phpstan-impure + */ + private function randImpure(): bool + { + return random_int(0, 1) > 0; + } + /** @return list> */ public function run(): array { @@ -65,8 +76,60 @@ public function run(): array $arr3[] = $result; + assertType('non-empty-list', $arr3); + } + + assertType('list', $arr3); + + return $arr3; + } + + /** @return list> */ + public function runImpure(): array + { + $arr3 = []; + foreach ($this->someFunc() as $id => $arr2) { + // Solution 1 - Specify $result type + // /** @var array $result */ + $result = [ + 'val1' => false, + 'val2' => false, + 'val3' => false, + 'val4' => false, + 'val5' => false, + 'val6' => false, + 'val7' => false, + ]; + + if ($this->randImpure()) { + $result['val1'] = true; + } + if ($this->randImpure()) { + $result['val2'] = true; + } + + if ($this->randImpure()) { + $result['val3'] = true; + } + + if ($this->randImpure()) { + $result['val4'] = true; + } + + if ($this->randImpure()) { + $result['val5'] = true; + } + + if ($this->randImpure()) { + $result['val6'] = true; + } + + $arr3[] = $result; + assertType('non-empty-list', $arr3); } + assertType('list', $arr3); + return $arr3; } } diff --git a/tests/PHPStan/Analyser/data/bug-7944.php b/tests/PHPStan/Analyser/data/bug-7944.php new file mode 100644 index 0000000000..737ab3dcb8 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7944.php @@ -0,0 +1,31 @@ +value = $value; + } +} + +/** + * @param non-empty-string $p + */ +function test($p): void { + $value = new Value($p); + assertType('Bug7944\\Value', $value); +}; + diff --git a/tests/PHPStan/Analyser/data/bug-7980.php b/tests/PHPStan/Analyser/data/bug-7980.php new file mode 100644 index 0000000000..1e2f3c0601 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7980.php @@ -0,0 +1,28 @@ +value) ? $valueObj->value : 0; + +test($value, $valueObj?->ttl); diff --git a/tests/PHPStan/Analyser/data/bug-7996.php b/tests/PHPStan/Analyser/data/bug-7996.php new file mode 100644 index 0000000000..e336dee7f9 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7996.php @@ -0,0 +1,31 @@ + $inputArray + * @return non-empty-array<\stdclass> + */ + public function filter(array $inputArray): array + { + $currentItem = reset($inputArray); + $outputArray = [$currentItem]; // $outputArray is now non-empty-array + assertType('array{stdclass}', $outputArray); + + while ($nextItem = next($inputArray)) { + if (rand(1, 2) === 1) { + assertType('non-empty-list', $outputArray); + // The fact that this is into an if, reverts type of $outputArray to array + $outputArray[] = $nextItem; + } + assertType('non-empty-list', $outputArray); + } + + assertType('non-empty-list', $outputArray); + return $outputArray; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8004.php b/tests/PHPStan/Analyser/data/bug-8004.php index 17bc9c5062..938f6f1111 100644 --- a/tests/PHPStan/Analyser/data/bug-8004.php +++ b/tests/PHPStan/Analyser/data/bug-8004.php @@ -73,7 +73,7 @@ public function getErrorsOnInvalidQuestions(array $importQuiz, int $key): array } } - assertType("array", $errors); + assertType("list&oversized-array", $errors); return $errors; } diff --git a/tests/PHPStan/Analyser/data/bug-8015.php b/tests/PHPStan/Analyser/data/bug-8015.php index 7c08e6091b..99fa39d27b 100644 --- a/tests/PHPStan/Analyser/data/bug-8015.php +++ b/tests/PHPStan/Analyser/data/bug-8015.php @@ -16,15 +16,15 @@ function extractParameters(array $items): array $config['things'] = []; assertType('array{}', $config['things']); foreach ($item as $thing) { - assertType('array', $config['things']); + assertType('list', $config['things']); $config['things'][] = (string) $thing; } - assertType('array', $config['things']); + assertType('list', $config['things']); } else { $config[$itemName] = (string) $item; } } - assertType('array|string', $config['things']); + assertType('list|string', $config['things']); return $config; } diff --git a/tests/PHPStan/Analyser/data/bug-8084.php b/tests/PHPStan/Analyser/data/bug-8084.php new file mode 100644 index 0000000000..fe5869e8e2 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8084.php @@ -0,0 +1,20 @@ + */ +class TypeWithSpecific implements TypeWithGeneric +{ + public function get(): Specific + { + return new Specific(); + } +} + +class HelloWorld +{ + /** @param TypeWithGeneric $type */ + public function test(TypeWithGeneric $type): void + { + match (get_class($type)) { + TypeWithSpecific::class => assertType(TypeWithSpecific::class, $type), + default => false, + }; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8127.php b/tests/PHPStan/Analyser/data/bug-8127.php new file mode 100644 index 0000000000..6e38769e6f --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8127.php @@ -0,0 +1,52 @@ + + */ +final class SinkCollector implements Collector +{ + public function getNodeType(): string + { + return CallLike::class; + } + + public function processNode(\PhpParser\Node $node, Scope $scope) + {} +} + +class TaintType +{ + public const TYPE_INPUT = 'input'; + public const TYPE_SQL = 'sql'; + public const TYPE_HTML = 'html'; + + public const TYPES = [self::TYPE_INPUT, self::TYPE_SQL, self::TYPE_HTML]; +} + +/** + * @implements Rule + */ +final class TaintRule implements Rule +{ + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(\PhpParser\Node $node, Scope $scope): array + { + $sinkCollectorData = $node->get(SinkCollector::class); + assertType("array>", $sinkCollectorData); + + return []; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8146a.php b/tests/PHPStan/Analyser/data/bug-8146a.php new file mode 100644 index 0000000000..3d0e10a65f --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8146a.php @@ -0,0 +1,152 @@ +session = $session; + $this->object = $object; + } + + public function sayHello(): void + { + $changeLog = []; + + $firstname = $this->session->get('firstname'); + if ($firstname !== $this->object->getFirstname()) { + $changelog['firstname_old'] = $this->object->getFirstname(); + $changelog['firstname_new'] = $firstname; + } + + $lastname = $this->session->get('lastname'); + if ($lastname !== $this->object->getLastname()) { + $changelog['lastname_old'] = $this->object->getLastname(); + $changelog['lastname_new'] = $lastname; + } + + $street = $this->session->get('street'); + if ($street !== $this->object->getStreet()) { + $changelog['street_old'] = $this->object->getStreet(); + $changelog['street_new'] = $street; + } + + $zip = $this->session->get('zip'); + if ($zip !== $this->object->getZip()) { + $changelog['zip_old'] = $this->object->getZip(); + $changelog['zip_new'] = $zip; + } + + $city = $this->session->get('city'); + if ($city !== $this->object->getCity()) { + $changelog['city_old'] = $this->object->getCity(); + $changelog['city_new'] = $city; + } + + $phonenumber = $this->session->get('phonenumber'); + if ($phonenumber !== $this->object->getPhonenumber()) { + $changelog['phonenumber_old'] = $this->object->getPhonenumber(); + $changelog['phonenumber_new'] = $phonenumber; + } + + $email = $this->session->get('email'); + if ($email !== $this->object->getEmail()) { + $changelog['email_old'] = $this->object->getEmail(); + $changelog['email_new'] = $email; + } + + $deliveryFirstname = $this->session->get('deliveryFirstname'); + if ($deliveryFirstname !== $this->object->getDeliveryFirstname()) { + $changelog['deliveryFirstname_old'] = $this->object->getDeliveryFirstname(); + $changelog['deliveryFirstname_new'] = $deliveryFirstname; + } + + $deliveryLastname = $this->session->get('deliveryLastname'); + if ($deliveryLastname !== $this->object->getDeliveryLastname()) { + $changelog['deliveryLastname_old'] = $this->object->getDeliveryLastname(); + $changelog['deliveryLastname_new'] = $deliveryLastname; + } + $deliveryStreet = $this->session->get('deliveryStreet'); + if ($deliveryStreet !== $this->object->getDeliveryStreet()) { + $changelog['deliveryStreet_old'] = $this->object->getDeliveryStreet(); + $changelog['deliveryStreet_new'] = $deliveryStreet; + } + $deliveryZip = $this->session->get('deliveryZip'); + if ($deliveryZip !== $this->object->getDeliveryZip()) { + $changelog['deliveryZip_old'] = $this->object->getDeliveryZip(); + $changelog['deliveryZip_new'] = $deliveryZip; + } + $deliveryCity = $this->session->get('deliveryCity'); + if ($deliveryCity !== $this->object->getDeliveryCity()) { + $changelog['deliveryCity_old'] = $this->object->getDeliveryCity(); + $changelog['deliveryCity_new'] = $deliveryCity; + } + + } +} + +interface SessionInterface +{ + /** + * @return mixed + */ + public function get(string $key); +} + +interface DataObject +{ + /** + * @return string|null + */ + public function getFirstname(); + /** + * @return string|null + */ + public function getLastname(); + /** + * @return string|null + */ + public function getStreet(); + /** + * @return string|null + */ + public function getZip(); + /** + * @return string|null + */ + public function getCity(); + /** + * @return string|null + */ + public function getPhonenumber(); + /** + * @return string|null + */ + public function getEmail(); + /** + * @return string|null + */ + public function getDeliveryFirstname(); + /** + * @return string|null + */ + public function getDeliveryLastname(); + /** + * @return string|null + */ + public function getDeliveryStreet(); + /** + * @return string|null + */ + public function getDeliveryZip(); + /** + * @return string|null + */ + public function getDeliveryCity(); +} diff --git a/tests/PHPStan/Analyser/data/bug-8146b.php b/tests/PHPStan/Analyser/data/bug-8146b.php new file mode 100644 index 0000000000..6d3ed34290 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8146b.php @@ -0,0 +1,5544 @@ +, coordinates: array{lat: float, lng: float}}>> */ + public function getData(): array + { + return [ + 'Bács-Kiskun' => [ + 'Ágasegyháza' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8386043, 'lng' => 19.4502899], + ], + 'Akasztó' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6898175, 'lng' => 19.205086], + ], + 'Apostag' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.8812652, 'lng' => 18.9648478], + ], + 'Bácsalmás' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.1250396, 'lng' => 19.3357509], + ], + 'Bácsbokod' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1234737, 'lng' => 19.155708], + ], + 'Bácsborsód' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0989373, 'lng' => 19.1566725], + ], + 'Bácsszentgyörgy' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 45.9746039, 'lng' => 19.0398066], + ], + 'Bácsszőlős' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.1352003, 'lng' => 19.4215997], + ], + 'Baja' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1817951, 'lng' => 18.9543051], + ], + 'Ballószög' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.8619947, 'lng' => 19.5726144], + ], + 'Balotaszállás' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.3512041, 'lng' => 19.5403558], + ], + 'Bátmonostor' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1057304, 'lng' => 18.9238311], + ], + 'Bátya' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.4891741, 'lng' => 18.9579127], + ], + 'Bócsa' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6113504, 'lng' => 19.4826419], + ], + 'Borota' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2657107, 'lng' => 19.2233598], + ], + 'Bugac' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6883076, 'lng' => 19.6833655], + ], + 'Bugacpusztaháza' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7022143, 'lng' => 19.6356538], + ], + 'Császártöltés' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4222869, 'lng' => 19.1815532], + ], + 'Csátalja' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0363238, 'lng' => 18.9469006], + ], + 'Csávoly' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1912599, 'lng' => 19.1451178], + ], + 'Csengőd' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.71532, 'lng' => 19.2660933], + ], + 'Csikéria' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.121679, 'lng' => 19.473777], + ], + 'Csólyospálos' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.4180837, 'lng' => 19.8402638], + ], + 'Dávod' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 45.9976187, 'lng' => 18.9176479], + ], + 'Drágszél' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.4653889, 'lng' => 19.0382659], + ], + 'Dunaegyháza' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.8383215, 'lng' => 18.9605216], + ], + 'Dunafalva' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.081562, 'lng' => 18.7782526], + ], + 'Dunapataj' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6422106, 'lng' => 18.9989393], + ], + 'Dunaszentbenedek' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.593856, 'lng' => 18.8935322], + ], + 'Dunatetétlen' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.7578624, 'lng' => 19.0932563], + ], + 'Dunavecse' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.9133047, 'lng' => 18.9731873], + ], + 'Dusnok' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.3893659, 'lng' => 18.960842], + ], + 'Érsekcsanád' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.2541554, 'lng' => 18.9835293], + ], + 'Érsekhalma' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.3472701, 'lng' => 19.1247379], + ], + 'Fajsz' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.4157936, 'lng' => 18.9191954], + ], + 'Felsőlajos' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0647473, 'lng' => 19.4944348], + ], + 'Felsőszentiván' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1966179, 'lng' => 19.1873616], + ], + 'Foktő' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5268759, 'lng' => 18.9196874], + ], + 'Fülöpháza' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.8914016, 'lng' => 19.4432493], + ], + 'Fülöpjakab' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.742058, 'lng' => 19.7227232], + ], + 'Fülöpszállás' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8195701, 'lng' => 19.2372115], + ], + 'Gara' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0349999, 'lng' => 19.0393411], + ], + 'Gátér' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.680435, 'lng' => 19.9596412], + ], + 'Géderlak' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6072512, 'lng' => 18.9135762], + ], + 'Hajós' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4001409, 'lng' => 19.1193255], + ], + 'Harkakötöny' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4634053, 'lng' => 19.6069951], + ], + 'Harta' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6960997, 'lng' => 19.0328195], + ], + 'Helvécia' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.8360977, 'lng' => 19.620438], + ], + 'Hercegszántó' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 45.9482057, 'lng' => 18.9389127], + ], + 'Homokmégy' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.4892762, 'lng' => 19.0730421], + ], + 'Imrehegy' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4867668, 'lng' => 19.3056372], + ], + 'Izsák' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8020009, 'lng' => 19.3546225], + ], + 'Jakabszállás' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7602785, 'lng' => 19.6055301], + ], + 'Jánoshalma' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2974544, 'lng' => 19.3250656], + ], + 'Jászszentlászló' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.5672659, 'lng' => 19.7590541], + ], + 'Kalocsa' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5281229, 'lng' => 18.9840376], + ], + 'Kaskantyú' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6711891, 'lng' => 19.3895391], + ], + 'Katymár' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0344636, 'lng' => 19.2087609], + ], + 'Kecel' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.5243135, 'lng' => 19.2451963], + ], + 'Kecskemét' => [ + 'constituencies' => ['Bács-Kiskun 2.', 'Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.8963711, 'lng' => 19.6896861], + ], + 'Kelebia' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.1958608, 'lng' => 19.6066291], + ], + 'Kéleshalom' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.3641795, 'lng' => 19.2831241], + ], + 'Kerekegyháza' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.9385747, 'lng' => 19.4770208], + ], + 'Kiskőrös' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6224967, 'lng' => 19.2874568], + ], + 'Kiskunfélegyháza' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7112802, 'lng' => 19.8515196], + ], + 'Kiskunhalas' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4354409, 'lng' => 19.4834284], + ], + 'Kiskunmajsa' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.4904848, 'lng' => 19.7366569], + ], + 'Kisszállás' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2791272, 'lng' => 19.4908079], + ], + 'Kömpöc' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.4640167, 'lng' => 19.8665681], + ], + 'Kunadacs' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.956503, 'lng' => 19.2880496], + ], + 'Kunbaja' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.0848391, 'lng' => 19.4213713], + ], + 'Kunbaracs' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.9891493, 'lng' => 19.3999584], + ], + 'Kunfehértó' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.362671, 'lng' => 19.4141949], + ], + 'Kunpeszér' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0611502, 'lng' => 19.2753764], + ], + 'Kunszállás' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7627801, 'lng' => 19.7532925], + ], + 'Kunszentmiklós' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0244473, 'lng' => 19.1235997], + ], + 'Ladánybene' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0344239, 'lng' => 19.456807], + ], + 'Lajosmizse' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0248225, 'lng' => 19.5559232], + ], + 'Lakitelek' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8710339, 'lng' => 19.9930216], + ], + 'Madaras' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0554833, 'lng' => 19.2633403], + ], + 'Mátételke' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1614675, 'lng' => 19.2802263], + ], + 'Mélykút' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2132295, 'lng' => 19.3814176], + ], + 'Miske' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.4434918, 'lng' => 19.0315752], + ], + 'Móricgát' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6233704, 'lng' => 19.6885382], + ], + 'Nagybaracska' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0444015, 'lng' => 18.9048387], + ], + 'Nemesnádudvar' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.3348444, 'lng' => 19.0542114], + ], + 'Nyárlőrinc' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8611255, 'lng' => 19.8773125], + ], + 'Ordas' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6364524, 'lng' => 18.9504602], + ], + 'Öregcsertő' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.515272, 'lng' => 19.1090595], + ], + 'Orgovány' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7497582, 'lng' => 19.4746024], + ], + 'Páhi' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.7136232, 'lng' => 19.3856937], + ], + 'Pálmonostora' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6265115, 'lng' => 19.9425525], + ], + 'Petőfiszállás' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6243457, 'lng' => 19.8596537], + ], + 'Pirtó' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.5139604, 'lng' => 19.4301958], + ], + 'Rém' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.2470804, 'lng' => 19.1416684], + ], + 'Solt' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.8021967, 'lng' => 19.0108147], + ], + 'Soltszentimre' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.769786, 'lng' => 19.2840433], + ], + 'Soltvadkert' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5789287, 'lng' => 19.3938029], + ], + 'Sükösd' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.2832039, 'lng' => 18.9942907], + ], + 'Szabadszállás' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.8763076, 'lng' => 19.2232539], + ], + 'Szakmár' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5543652, 'lng' => 19.0742847], + ], + 'Szalkszentmárton' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.9754928, 'lng' => 19.0171018], + ], + 'Szank' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.5557842, 'lng' => 19.6668956], + ], + 'Szentkirály' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.9169398, 'lng' => 19.9175371], + ], + 'Szeremle' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1436504, 'lng' => 18.8810207], + ], + 'Tabdi' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6818019, 'lng' => 19.3042672], + ], + 'Tass' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0184485, 'lng' => 19.0281253], + ], + 'Tataháza' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.173167, 'lng' => 19.3024716], + ], + 'Tázlár' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.5509533, 'lng' => 19.5159844], + ], + 'Tiszaalpár' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8140236, 'lng' => 19.9936556], + ], + 'Tiszakécske' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.9358726, 'lng' => 20.0969279], + ], + 'Tiszaug' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8537215, 'lng' => 20.052921], + ], + 'Tompa' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2060507, 'lng' => 19.5389553], + ], + 'Újsolt' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.8706098, 'lng' => 19.1186222], + ], + 'Újtelek' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5911716, 'lng' => 19.0564597], + ], + 'Uszód' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5704972, 'lng' => 18.9038275], + ], + 'Városföld' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.8174844, 'lng' => 19.7597893], + ], + 'Vaskút' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1080968, 'lng' => 18.9861524], + ], + 'Zsana' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.3802847, 'lng' => 19.6600846], + ], + ], + 'Baranya' => [ + 'Abaliget' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1428711, 'lng' => 18.1152298], + ], + 'Adorjás' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8509119, 'lng' => 18.0617924], + ], + 'Ág' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2962836, 'lng' => 18.2023275], + ], + 'Almamellék' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1603198, 'lng' => 17.8765681], + ], + 'Almáskeresztúr' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1199547, 'lng' => 17.8958453], + ], + 'Alsómocsolád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.313518, 'lng' => 18.2481993], + ], + 'Alsószentmárton' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.7912208, 'lng' => 18.3065816], + ], + 'Apátvarasd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1856469, 'lng' => 18.47932], + ], + 'Aranyosgadány' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.007757, 'lng' => 18.1195466], + ], + 'Áta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9367366, 'lng' => 18.2985608], + ], + 'Babarc' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0042229, 'lng' => 18.5527511], + ], + 'Babarcszőlős' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.898699, 'lng' => 18.1360284], + ], + 'Bakóca' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2074891, 'lng' => 18.0002016], + ], + 'Bakonya' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0850942, 'lng' => 18.082286], + ], + 'Baksa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9554293, 'lng' => 18.0909794], + ], + 'Bánfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.994691, 'lng' => 17.8798792], + ], + 'Bár' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0482419, 'lng' => 18.7119502], + ], + 'Baranyahídvég' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8461886, 'lng' => 18.0229597], + ], + 'Baranyajenő' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2734519, 'lng' => 18.0469416], + ], + 'Baranyaszentgyörgy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2461345, 'lng' => 18.0119839], + ], + 'Basal' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0734372, 'lng' => 17.7832659], + ], + 'Belvárdgyula' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9750659, 'lng' => 18.4288438], + ], + 'Beremend' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.7877528, 'lng' => 18.4322322], + ], + 'Berkesd' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0766759, 'lng' => 18.4078442], + ], + 'Besence' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8956421, 'lng' => 17.9654588], + ], + 'Bezedek' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8653948, 'lng' => 18.5854023], + ], + 'Bicsérd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0216488, 'lng' => 18.0779429], + ], + 'Bikal' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3329154, 'lng' => 18.2845332], + ], + 'Birján' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0007461, 'lng' => 18.3739733], + ], + 'Bisse' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9082449, 'lng' => 18.2603363], + ], + 'Boda' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0796449, 'lng' => 18.0477749], + ], + 'Bodolyabér' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.196906, 'lng' => 18.1189705], + ], + 'Bogád' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0858618, 'lng' => 18.3215439], + ], + 'Bogádmindszent' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9069292, 'lng' => 18.0382456], + ], + 'Bogdása' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8756825, 'lng' => 17.7892759], + ], + 'Boldogasszonyfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1826055, 'lng' => 17.8379176], + ], + 'Bóly' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9654045, 'lng' => 18.5166166], + ], + 'Borjád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9356423, 'lng' => 18.4708549], + ], + 'Bosta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9500492, 'lng' => 18.2104193], + ], + 'Botykapeterd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0499466, 'lng' => 17.8662441], + ], + 'Bükkösd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1100188, 'lng' => 17.9925218], + ], + 'Bürüs' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9653278, 'lng' => 17.7591739], + ], + 'Csányoszró' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8810774, 'lng' => 17.9101381], + ], + 'Csarnóta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8949174, 'lng' => 18.2163121], + ], + 'Csebény' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1893582, 'lng' => 17.9275209], + ], + 'Cserdi' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0808529, 'lng' => 17.9911191], + ], + 'Cserkút' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0756664, 'lng' => 18.1340119], + ], + 'Csertő' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.093457, 'lng' => 17.8034587], + ], + 'Csonkamindszent' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0518017, 'lng' => 17.9658056], + ], + 'Cún' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8122974, 'lng' => 18.0678543], + ], + 'Dencsháza' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.993512, 'lng' => 17.8347772], + ], + 'Dinnyeberki' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0972962, 'lng' => 17.9563165], + ], + 'Diósviszló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8774861, 'lng' => 18.1640495], + ], + 'Drávacsehi' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8130167, 'lng' => 18.1666181], + ], + 'Drávacsepely' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8308297, 'lng' => 18.1352308], + ], + 'Drávafok' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8860365, 'lng' => 17.7636317], + ], + 'Drávaiványi' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8470684, 'lng' => 17.8159164], + ], + 'Drávakeresztúr' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8386967, 'lng' => 17.7580104], + ], + 'Drávapalkonya' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8033438, 'lng' => 18.1790753], + ], + 'Drávapiski' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8396577, 'lng' => 18.0989657], + ], + 'Drávaszabolcs' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.803275, 'lng' => 18.2093234], + ], + 'Drávaszerdahely' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8363562, 'lng' => 18.1638527], + ], + 'Drávasztára' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8230964, 'lng' => 17.8220692], + ], + 'Dunaszekcső' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0854783, 'lng' => 18.7542203], + ], + 'Egerág' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9834452, 'lng' => 18.3039561], + ], + 'Egyházasharaszti' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8097356, 'lng' => 18.3314381], + ], + 'Egyházaskozár' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3319023, 'lng' => 18.3178591], + ], + 'Ellend' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0580138, 'lng' => 18.3760682], + ], + 'Endrőc' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9296401, 'lng' => 17.7621758], + ], + 'Erdősmárok' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.055568, 'lng' => 18.5458091], + ], + 'Erdősmecske' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1768439, 'lng' => 18.5109755], + ], + 'Erzsébet' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1004339, 'lng' => 18.4587621], + ], + 'Fazekasboda' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1230108, 'lng' => 18.4850924], + ], + 'Feked' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1626797, 'lng' => 18.5588015], + ], + 'Felsőegerszeg' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2539122, 'lng' => 18.1335751], + ], + 'Felsőszentmárton' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8513101, 'lng' => 17.7034033], + ], + 'Garé' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9180881, 'lng' => 18.1956808], + ], + 'Gerde' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9904428, 'lng' => 18.0255496], + ], + 'Gerényes' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.3070289, 'lng' => 18.1848981], + ], + 'Geresdlak' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1107897, 'lng' => 18.5268599], + ], + 'Gilvánfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9184356, 'lng' => 17.9622098], + ], + 'Gödre' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2899579, 'lng' => 17.9723779], + ], + 'Görcsöny' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9709725, 'lng' => 18.133486], + ], + 'Görcsönydoboka' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0709275, 'lng' => 18.6275109], + ], + 'Gordisa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.7970748, 'lng' => 18.2354868], + ], + 'Gyód' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 45.9979549, 'lng' => 18.1781638], + ], + 'Gyöngyfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9601196, 'lng' => 17.9506649], + ], + 'Gyöngyösmellék' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9868644, 'lng' => 17.7014751], + ], + 'Harkány' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8534053, 'lng' => 18.2348372], + ], + 'Hásságy' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0330172, 'lng' => 18.388848], + ], + 'Hegyhátmaróc' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3109929, 'lng' => 18.3362487], + ], + 'Hegyszentmárton' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9036373, 'lng' => 18.086797], + ], + 'Helesfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0894523, 'lng' => 17.9770167], + ], + 'Hetvehely' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1332155, 'lng' => 18.0432466], + ], + 'Hidas' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2574631, 'lng' => 18.4937015], + ], + 'Himesháza' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0797595, 'lng' => 18.5805933], + ], + 'Hirics' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8247516, 'lng' => 17.9934259], + ], + 'Hobol' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0197823, 'lng' => 17.7724266], + ], + 'Homorúd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.981847, 'lng' => 18.7887766], + ], + 'Horváthertelend' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1751748, 'lng' => 17.9272893], + ], + 'Hosszúhetény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1583167, 'lng' => 18.3520974], + ], + 'Husztót' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1711511, 'lng' => 18.0932139], + ], + 'Ibafa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1552456, 'lng' => 17.9179873], + ], + 'Illocska' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.800591, 'lng' => 18.5233576], + ], + 'Ipacsfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8345382, 'lng' => 18.2055561], + ], + 'Ivánbattyán' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9077809, 'lng' => 18.4176354], + ], + 'Ivándárda' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.831643, 'lng' => 18.5922589], + ], + 'Kacsóta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0390809, 'lng' => 17.9544689], + ], + 'Kákics' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9028359, 'lng' => 17.8568313], + ], + 'Kárász' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2667559, 'lng' => 18.3188548], + ], + 'Kásád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.7793743, 'lng' => 18.3991912], + ], + 'Katádfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9970924, 'lng' => 17.8692171], + ], + 'Kátoly' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0634292, 'lng' => 18.4496796], + ], + 'Kékesd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1007579, 'lng' => 18.4720006], + ], + 'Kémes' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8241919, 'lng' => 18.1031607], + ], + 'Kemse' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8237775, 'lng' => 17.9119613], + ], + 'Keszü' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 46.0160053, 'lng' => 18.1918765], + ], + 'Kétújfalu' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9643465, 'lng' => 17.7128738], + ], + 'Királyegyháza' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9975029, 'lng' => 17.9670799], + ], + 'Kisasszonyfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9467478, 'lng' => 18.0062386], + ], + 'Kisbeszterce' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2054937, 'lng' => 18.033257], + ], + 'Kisbudmér' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9132933, 'lng' => 18.4468642], + ], + 'Kisdér' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9397014, 'lng' => 18.1280256], + ], + 'Kisdobsza' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0279686, 'lng' => 17.654966], + ], + 'Kishajmás' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2000972, 'lng' => 18.0807394], + ], + 'Kisharsány' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8597428, 'lng' => 18.3628602], + ], + 'Kisherend' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9657006, 'lng' => 18.3308199], + ], + 'Kisjakabfalva' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8961294, 'lng' => 18.4347874], + ], + 'Kiskassa' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9532763, 'lng' => 18.3984025], + ], + 'Kislippó' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8309942, 'lng' => 18.5387451], + ], + 'Kisnyárád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0369956, 'lng' => 18.5642298], + ], + 'Kisszentmárton' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8245119, 'lng' => 18.0223384], + ], + 'Kistamási' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0118086, 'lng' => 17.7210893], + ], + 'Kistapolca' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8215113, 'lng' => 18.383003], + ], + 'Kistótfalu' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9080691, 'lng' => 18.3097841], + ], + 'Kisvaszar' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2748571, 'lng' => 18.2126962], + ], + 'Köblény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2948258, 'lng' => 18.303697], + ], + 'Kökény' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 45.9995372, 'lng' => 18.2057648], + ], + 'Kölked' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9489796, 'lng' => 18.7058024], + ], + 'Komló' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.1929788, 'lng' => 18.2512139], + ], + 'Kórós' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8666591, 'lng' => 18.0818986], + ], + 'Kovácshida' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8322528, 'lng' => 18.1852847], + ], + 'Kovácsszénája' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1714525, 'lng' => 18.1099753], + ], + 'Kővágószőlős' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0824433, 'lng' => 18.1242335], + ], + 'Kővágótöttös' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0859181, 'lng' => 18.1005597], + ], + 'Kozármisleny' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0412574, 'lng' => 18.2872228], + ], + 'Lánycsók' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0073964, 'lng' => 18.624077], + ], + 'Lapáncsa' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8187417, 'lng' => 18.4965793], + ], + 'Liget' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2346633, 'lng' => 18.1924669], + ], + 'Lippó' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.863493, 'lng' => 18.5702136], + ], + 'Liptód' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.044203, 'lng' => 18.5153709], + ], + 'Lothárd' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0015129, 'lng' => 18.3534664], + ], + 'Lovászhetény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1573687, 'lng' => 18.4736022], + ], + 'Lúzsok' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8386895, 'lng' => 17.9448893], + ], + 'Mágocs' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3507989, 'lng' => 18.2282954], + ], + 'Magyarbóly' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8424536, 'lng' => 18.4905327], + ], + 'Magyaregregy' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2497645, 'lng' => 18.3080926], + ], + 'Magyarhertelend' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1887919, 'lng' => 18.1496193], + ], + 'Magyarlukafa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1692382, 'lng' => 17.7566367], + ], + 'Magyarmecske' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9444333, 'lng' => 17.963957], + ], + 'Magyarsarlós' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0412482, 'lng' => 18.3527956], + ], + 'Magyarszék' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1966719, 'lng' => 18.1955889], + ], + 'Magyartelek' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9438384, 'lng' => 17.9834231], + ], + 'Majs' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9090894, 'lng' => 18.59764], + ], + 'Mánfa' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.1620219, 'lng' => 18.2424376], + ], + 'Maráza' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0767639, 'lng' => 18.5102704], + ], + 'Márfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8597093, 'lng' => 18.184506], + ], + 'Máriakéménd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0275242, 'lng' => 18.4616888], + ], + 'Markóc' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8633597, 'lng' => 17.7628134], + ], + 'Marócsa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9143499, 'lng' => 17.8155625], + ], + 'Márok' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8776725, 'lng' => 18.5052153], + ], + 'Martonfa' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1162762, 'lng' => 18.373108], + ], + 'Matty' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.7959854, 'lng' => 18.2646823], + ], + 'Máza' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2674701, 'lng' => 18.3987184], + ], + 'Mecseknádasd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.22466, 'lng' => 18.4653855], + ], + 'Mecsekpölöske' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2232838, 'lng' => 18.2117379], + ], + 'Mekényes' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3905907, 'lng' => 18.3338629], + ], + 'Merenye' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.069313, 'lng' => 17.6981454], + ], + 'Meződ' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2898147, 'lng' => 18.1028572], + ], + 'Mindszentgodisa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2270491, 'lng' => 18.070952], + ], + 'Mohács' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0046295, 'lng' => 18.6794304], + ], + 'Molvány' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0294158, 'lng' => 17.7455964], + ], + 'Monyoród' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0115276, 'lng' => 18.4781726], + ], + 'Mozsgó' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1148249, 'lng' => 17.8457585], + ], + 'Nagybudmér' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9378397, 'lng' => 18.4443309], + ], + 'Nagycsány' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.871837, 'lng' => 17.9441308], + ], + 'Nagydobsza' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0290366, 'lng' => 17.6672107], + ], + 'Nagyhajmás' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.372206, 'lng' => 18.2898052], + ], + 'Nagyharsány' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8466947, 'lng' => 18.3947776], + ], + 'Nagykozár' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.067814, 'lng' => 18.316561], + ], + 'Nagynyárád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9447148, 'lng' => 18.578055], + ], + 'Nagypall' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1474016, 'lng' => 18.4539234], + ], + 'Nagypeterd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0459728, 'lng' => 17.8979423], + ], + 'Nagytótfalu' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8638406, 'lng' => 18.3426767], + ], + 'Nagyváty' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0617075, 'lng' => 17.93209], + ], + 'Nemeske' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.020198, 'lng' => 17.7129695], + ], + 'Nyugotszenterzsébet' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0747959, 'lng' => 17.9096635], + ], + 'Óbánya' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2220338, 'lng' => 18.4084838], + ], + 'Ócsárd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9341296, 'lng' => 18.1533436], + ], + 'Ófalu' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2210918, 'lng' => 18.534029], + ], + 'Okorág' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9262423, 'lng' => 17.8761913], + ], + 'Okorvölgy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.15235, 'lng' => 18.0600392], + ], + 'Olasz' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0128298, 'lng' => 18.4122965], + ], + 'Old' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.7893924, 'lng' => 18.3526547], + ], + 'Orfű' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.1504207, 'lng' => 18.1423992], + ], + 'Oroszló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2201904, 'lng' => 18.122659], + ], + 'Ózdfalu' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9288431, 'lng' => 18.0210679], + ], + 'Palé' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2603608, 'lng' => 18.0690432], + ], + 'Palkonya' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8968607, 'lng' => 18.3899099], + ], + 'Palotabozsok' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1275672, 'lng' => 18.6416844], + ], + 'Páprád' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8927275, 'lng' => 18.0103745], + ], + 'Patapoklosi' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0753051, 'lng' => 17.7415323], + ], + 'Pécs' => [ + 'constituencies' => ['Baranya 2.', 'Baranya 1.'], + 'coordinates' => ['lat' => 46.0727345, 'lng' => 18.232266], + ], + 'Pécsbagota' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9906469, 'lng' => 18.0728758], + ], + 'Pécsdevecser' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9585177, 'lng' => 18.3839237], + ], + 'Pécsudvard' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 46.0108323, 'lng' => 18.2750737], + ], + 'Pécsvárad' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1591341, 'lng' => 18.4185199], + ], + 'Pellérd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.034172, 'lng' => 18.1551531], + ], + 'Pereked' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0940085, 'lng' => 18.3768639], + ], + 'Peterd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9726228, 'lng' => 18.3606704], + ], + 'Pettend' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0001576, 'lng' => 17.7011535], + ], + 'Piskó' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8112973, 'lng' => 17.9384454], + ], + 'Pócsa' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9100922, 'lng' => 18.4699792], + ], + 'Pogány' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 45.9827333, 'lng' => 18.2568939], + ], + 'Rádfalva' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8598624, 'lng' => 18.1252323], + ], + 'Regenye' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.969783, 'lng' => 18.1685228], + ], + 'Romonya' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0871177, 'lng' => 18.3391112], + ], + 'Rózsafa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0227215, 'lng' => 17.8889708], + ], + 'Sámod' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8536384, 'lng' => 18.0384521], + ], + 'Sárok' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8414254, 'lng' => 18.6119412], + ], + 'Sásd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2563232, 'lng' => 18.1024778], + ], + 'Sátorhely' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9417452, 'lng' => 18.6330768], + ], + 'Sellye' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.873291, 'lng' => 17.8494986], + ], + 'Siklós' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8555814, 'lng' => 18.2979721], + ], + 'Siklósbodony' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9105251, 'lng' => 18.1202589], + ], + 'Siklósnagyfalu' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.820428, 'lng' => 18.3636246], + ], + 'Somberek' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0812348, 'lng' => 18.6586781], + ], + 'Somogyapáti' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0920041, 'lng' => 17.7506787], + ], + 'Somogyhárságy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1623103, 'lng' => 17.7731873], + ], + 'Somogyhatvan' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1120284, 'lng' => 17.7126553], + ], + 'Somogyviszló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1146313, 'lng' => 17.7636375], + ], + 'Sósvertike' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8340815, 'lng' => 17.8614028], + ], + 'Sumony' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9675435, 'lng' => 17.9146319], + ], + 'Szabadszentkirály' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0059012, 'lng' => 18.0435247], + ], + 'Szágy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2244706, 'lng' => 17.9469817], + ], + 'Szajk' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9921175, 'lng' => 18.5328986], + ], + 'Szalánta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9471908, 'lng' => 18.2376181], + ], + 'Szalatnak' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2903675, 'lng' => 18.2809735], + ], + 'Szaporca' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8135724, 'lng' => 18.1045054], + ], + 'Szárász' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3487743, 'lng' => 18.3727487], + ], + 'Szászvár' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2739639, 'lng' => 18.3774781], + ], + 'Szava' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9024581, 'lng' => 18.1738569], + ], + 'Szebény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1296283, 'lng' => 18.5879918], + ], + 'Szederkény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9986735, 'lng' => 18.4530663], + ], + 'Székelyszabar' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0471326, 'lng' => 18.6012321], + ], + 'Szellő' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0744167, 'lng' => 18.4609549], + ], + 'Szemely' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0083381, 'lng' => 18.3256717], + ], + 'Szentdénes' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0079644, 'lng' => 17.9271651], + ], + 'Szentegát' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9754975, 'lng' => 17.8244079], + ], + 'Szentkatalin' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.174384, 'lng' => 18.0505714], + ], + 'Szentlászló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1540417, 'lng' => 17.8331512], + ], + 'Szentlőrinc' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0403123, 'lng' => 17.9897756], + ], + 'Szigetvár' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0487727, 'lng' => 17.7983466], + ], + 'Szilágy' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.1009525, 'lng' => 18.4065405], + ], + 'Szilvás' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9616358, 'lng' => 18.1981701], + ], + 'Szőke' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9604273, 'lng' => 18.1867423], + ], + 'Szőkéd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9645154, 'lng' => 18.2884592], + ], + 'Szörény' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9683861, 'lng' => 17.6819713], + ], + 'Szulimán' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1264433, 'lng' => 17.805449], + ], + 'Szűr' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.099254, 'lng' => 18.5809615], + ], + 'Tarrós' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2806564, 'lng' => 18.1425225], + ], + 'Tékes' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2866262, 'lng' => 18.1744149], + ], + 'Teklafalu' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9493136, 'lng' => 17.7287585], + ], + 'Tengeri' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9263477, 'lng' => 18.087938], + ], + 'Tésenfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8127763, 'lng' => 18.1178921], + ], + 'Téseny' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9515499, 'lng' => 18.0479966], + ], + 'Tófű' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3094872, 'lng' => 18.3576794], + ], + 'Tormás' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2309543, 'lng' => 17.9937201], + ], + 'Tótszentgyörgy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0521798, 'lng' => 17.7178541], + ], + 'Töttös' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9150433, 'lng' => 18.5407584], + ], + 'Túrony' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9054082, 'lng' => 18.2309533], + ], + 'Udvar' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.900472, 'lng' => 18.6594842], + ], + 'Újpetre' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.934779, 'lng' => 18.3636323], + ], + 'Vajszló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8592442, 'lng' => 17.9868205], + ], + 'Várad' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9743574, 'lng' => 17.7456586], + ], + 'Varga' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2475508, 'lng' => 18.1424694], + ], + 'Vásárosbéc' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1825351, 'lng' => 17.7246441], + ], + 'Vásárosdombó' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.3064752, 'lng' => 18.1334675], + ], + 'Vázsnok' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2653395, 'lng' => 18.1253751], + ], + 'Vejti' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8096089, 'lng' => 17.9682522], + ], + 'Vékény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2695945, 'lng' => 18.3423454], + ], + 'Velény' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9807601, 'lng' => 18.0514344], + ], + 'Véménd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1551161, 'lng' => 18.6190866], + ], + 'Versend' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9953039, 'lng' => 18.5115869], + ], + 'Villány' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8700399, 'lng' => 18.453201], + ], + 'Villánykövesd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8823189, 'lng' => 18.425812], + ], + 'Vokány' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9133714, 'lng' => 18.3364685], + ], + 'Zádor' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9623692, 'lng' => 17.6579278], + ], + 'Zaláta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8111976, 'lng' => 17.8901202], + ], + 'Zengővárkony' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1728638, 'lng' => 18.4320077], + ], + 'Zók' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0104261, 'lng' => 18.0965422], + ], + ], + 'Békés' => [ + 'Almáskamarás' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4617785, 'lng' => 21.092448], + ], + 'Battonya' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.2902462, 'lng' => 21.0199215], + ], + 'Békés' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.6704899, 'lng' => 21.0434996], + ], + 'Békéscsaba' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.6735939, 'lng' => 21.0877309], + ], + 'Békéssámson' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4208677, 'lng' => 20.6176498], + ], + 'Békésszentandrás' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8715996, 'lng' => 20.48336], + ], + 'Bélmegyer' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8726019, 'lng' => 21.1832832], + ], + 'Biharugra' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.9691009, 'lng' => 21.5987651], + ], + 'Bucsa' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 47.2047017, 'lng' => 20.9970391], + ], + 'Csabacsűd' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8244161, 'lng' => 20.6485242], + ], + 'Csabaszabadi' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.574811, 'lng' => 20.951145], + ], + 'Csanádapáca' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5409397, 'lng' => 20.8852553], + ], + 'Csárdaszállás' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8647568, 'lng' => 20.9374853], + ], + 'Csorvás' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.6308376, 'lng' => 20.8340929], + ], + 'Dévaványa' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 47.0313217, 'lng' => 20.9595443], + ], + 'Doboz' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.7343152, 'lng' => 21.2420659], + ], + 'Dombegyház' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3415879, 'lng' => 21.1342664], + ], + 'Dombiratos' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4195218, 'lng' => 21.1178789], + ], + 'Ecsegfalva' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 47.14789, 'lng' => 20.9239261], + ], + 'Elek' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.5291929, 'lng' => 21.2487556], + ], + 'Füzesgyarmat' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 47.1051107, 'lng' => 21.2108329], + ], + 'Gádoros' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.6667476, 'lng' => 20.5961159], + ], + 'Gerendás' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.5969212, 'lng' => 20.8593687], + ], + 'Geszt' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8831763, 'lng' => 21.5794915], + ], + 'Gyomaendrőd' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.9317797, 'lng' => 20.8113125], + ], + 'Gyula' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.6473027, 'lng' => 21.2784255], + ], + 'Hunya' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.812869, 'lng' => 20.8458337], + ], + 'Kamut' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.7619186, 'lng' => 20.9798143], + ], + 'Kardos' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.7941712, 'lng' => 20.715629], + ], + 'Kardoskút' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.498573, 'lng' => 20.7040158], + ], + 'Kaszaper' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4598817, 'lng' => 20.8251944], + ], + 'Kertészsziget' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 47.1542945, 'lng' => 21.0610234], + ], + 'Kétegyháza' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.5417887, 'lng' => 21.1810736], + ], + 'Kétsoprony' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.7208319, 'lng' => 20.8870273], + ], + 'Kevermes' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4167579, 'lng' => 21.1818484], + ], + 'Kisdombegyház' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3693244, 'lng' => 21.0996778], + ], + 'Kondoros' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.7574628, 'lng' => 20.7972363], + ], + 'Körösladány' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.9607513, 'lng' => 21.0767574], + ], + 'Körösnagyharsány' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 47.0080391, 'lng' => 21.6417355], + ], + 'Köröstarcsa' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8780314, 'lng' => 21.02402], + ], + 'Körösújfalu' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.9659419, 'lng' => 21.3988486], + ], + 'Kötegyán' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.738284, 'lng' => 21.481692], + ], + 'Kunágota' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4234015, 'lng' => 21.0467553], + ], + 'Lőkösháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4297019, 'lng' => 21.2318793], + ], + 'Magyarbánhegyes' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4577279, 'lng' => 20.968734], + ], + 'Magyardombegyház' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3794548, 'lng' => 21.0743712], + ], + 'Medgyesbodzás' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5186797, 'lng' => 20.9596371], + ], + 'Medgyesegyháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4967576, 'lng' => 21.0271996], + ], + 'Méhkerék' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.7735176, 'lng' => 21.4435935], + ], + 'Mezőberény' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.825687, 'lng' => 21.0243614], + ], + 'Mezőgyán' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8709809, 'lng' => 21.5257366], + ], + 'Mezőhegyes' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3172449, 'lng' => 20.8173892], + ], + 'Mezőkovácsháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4093003, 'lng' => 20.9112692], + ], + 'Murony' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.760463, 'lng' => 21.0411739], + ], + 'Nagybánhegyes' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.460095, 'lng' => 20.902578], + ], + 'Nagykamarás' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4727168, 'lng' => 21.1213871], + ], + 'Nagyszénás' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.6722161, 'lng' => 20.6734381], + ], + 'Okány' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8982798, 'lng' => 21.3467384], + ], + 'Örménykút' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.830573, 'lng' => 20.7344497], + ], + 'Orosháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5684222, 'lng' => 20.6544927], + ], + 'Pusztaföldvár' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5251751, 'lng' => 20.8024526], + ], + 'Pusztaottlaka' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5386606, 'lng' => 21.0060316], + ], + 'Sarkad' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.7374245, 'lng' => 21.3810771], + ], + 'Sarkadkeresztúr' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8107081, 'lng' => 21.3841932], + ], + 'Szabadkígyós' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.601522, 'lng' => 21.0753003], + ], + 'Szarvas' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8635641, 'lng' => 20.5526535], + ], + 'Szeghalom' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 47.0239347, 'lng' => 21.1666571], + ], + 'Tarhos' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8132012, 'lng' => 21.2109597], + ], + 'Telekgerendás' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.6566167, 'lng' => 20.9496242], + ], + 'Tótkomlós' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4107596, 'lng' => 20.7363644], + ], + 'Újkígyós' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.5899757, 'lng' => 21.0242728], + ], + 'Újszalonta' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8128247, 'lng' => 21.4908762], + ], + 'Végegyháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3882623, 'lng' => 20.8699923], + ], + 'Vésztő' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.9244546, 'lng' => 21.2628502], + ], + 'Zsadány' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.9230248, 'lng' => 21.4873156], + ], + ], + 'Borsod-Abaúj-Zemplén' => [ + 'Abaújalpár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3065157, 'lng' => 21.232147], + ], + 'Abaújkér' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3033478, 'lng' => 21.2013068], + ], + 'Abaújlak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4051818, 'lng' => 20.9548056], + ], + 'Abaújszántó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2792184, 'lng' => 21.1874523], + ], + 'Abaújszolnok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3730791, 'lng' => 20.9749255], + ], + 'Abaújvár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5266538, 'lng' => 21.3150208], + ], + 'Abod' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3928646, 'lng' => 20.7923344], + ], + 'Aggtelek' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4686657, 'lng' => 20.5040699], + ], + 'Alacska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2157484, 'lng' => 20.6502945], + ], + 'Alsóberecki' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3437614, 'lng' => 21.6905164], + ], + 'Alsódobsza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1799523, 'lng' => 21.0026817], + ], + 'Alsógagy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4052855, 'lng' => 21.0255485], + ], + 'Alsóregmec' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4634336, 'lng' => 21.6181953], + ], + 'Alsószuha' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3726027, 'lng' => 20.5044038], + ], + 'Alsótelekes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4105212, 'lng' => 20.6547156], + ], + 'Alsóvadász' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2401438, 'lng' => 20.9043765], + ], + 'Alsózsolca' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.0748263, 'lng' => 20.8850624], + ], + 'Arka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3562385, 'lng' => 21.252529], + ], + 'Arló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1746548, 'lng' => 20.2560308], + ], + 'Arnót' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.1319962, 'lng' => 20.859401], + ], + 'Ároktő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7284812, 'lng' => 20.9423131], + ], + 'Aszaló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2177554, 'lng' => 20.9624804], + ], + 'Baktakék' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3675199, 'lng' => 21.0288911], + ], + 'Balajt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3210349, 'lng' => 20.7866111], + ], + 'Bánhorváti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2260139, 'lng' => 20.504815], + ], + 'Bánréve' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2986902, 'lng' => 20.3560194], + ], + 'Baskó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3326787, 'lng' => 21.336418], + ], + 'Becskeháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5294979, 'lng' => 20.8354743], + ], + 'Bekecs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1534102, 'lng' => 21.1762263], + ], + 'Berente' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2385836, 'lng' => 20.6700776], + ], + 'Beret' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3458722, 'lng' => 21.0235103], + ], + 'Berzék' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0240535, 'lng' => 20.9528886], + ], + 'Bőcs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0442332, 'lng' => 20.9683874], + ], + 'Bodroghalom' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3009977, 'lng' => 21.707044], + ], + 'Bodrogkeresztúr' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1630176, 'lng' => 21.3595899], + ], + 'Bodrogkisfalud' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1789303, 'lng' => 21.3617788], + ], + 'Bodrogolaszi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2867085, 'lng' => 21.5160527], + ], + 'Bódvalenke' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5424028, 'lng' => 20.8041838], + ], + 'Bódvarákó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5111514, 'lng' => 20.7358047], + ], + 'Bódvaszilas' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5377629, 'lng' => 20.7312757], + ], + 'Bogács' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9030764, 'lng' => 20.5312356], + ], + 'Boldogkőújfalu' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3193629, 'lng' => 21.242022], + ], + 'Boldogkőváralja' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3380634, 'lng' => 21.2367554], + ], + 'Boldva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.218091, 'lng' => 20.7886144], + ], + 'Borsodbóta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2121829, 'lng' => 20.3960602], + ], + 'Borsodgeszt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9559428, 'lng' => 20.6944004], + ], + 'Borsodivánka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.701045, 'lng' => 20.6547148], + ], + 'Borsodnádasd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1191717, 'lng' => 20.2529566], + ], + 'Borsodszentgyörgy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1892068, 'lng' => 20.2073894], + ], + 'Borsodszirák' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2610318, 'lng' => 20.7676252], + ], + 'Bózsva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4743356, 'lng' => 21.468268], + ], + 'Bükkábrány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8884157, 'lng' => 20.6810544], + ], + 'Bükkaranyos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9866329, 'lng' => 20.7794609], + ], + 'Bükkmogyorósd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1291531, 'lng' => 20.3563552], + ], + 'Bükkszentkereszt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0668164, 'lng' => 20.6324773], + ], + 'Bükkzsérc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9587559, 'lng' => 20.5025627], + ], + 'Büttös' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4783127, 'lng' => 21.0110122], + ], + 'Cigánd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2558937, 'lng' => 21.8889241], + ], + 'Csenyéte' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4345165, 'lng' => 21.0412334], + ], + 'Cserépfalu' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9413093, 'lng' => 20.5347083], + ], + 'Cserépváralja' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9325883, 'lng' => 20.5598918], + ], + 'Csernely' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1438586, 'lng' => 20.3390005], + ], + 'Csincse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8883234, 'lng' => 20.768705], + ], + 'Csobád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2796877, 'lng' => 21.0269782], + ], + 'Csobaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0485163, 'lng' => 21.3382189], + ], + 'Csokvaomány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1666711, 'lng' => 20.3744746], + ], + 'Damak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3168034, 'lng' => 20.8216124], + ], + 'Dámóc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3748294, 'lng' => 22.0336128], + ], + 'Debréte' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.5000066, 'lng' => 20.8661035], + ], + 'Dédestapolcsány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1804582, 'lng' => 20.4850166], + ], + 'Detek' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3336841, 'lng' => 21.0176305], + ], + 'Domaháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1836193, 'lng' => 20.1055583], + ], + 'Dövény' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3469512, 'lng' => 20.5431344], + ], + 'Dubicsány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2837745, 'lng' => 20.4940325], + ], + 'Edelény' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2934391, 'lng' => 20.7385817], + ], + 'Egerlövő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7203221, 'lng' => 20.6175935], + ], + 'Égerszög' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.442896, 'lng' => 20.5875195], + ], + 'Emőd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9380038, 'lng' => 20.8154444], + ], + 'Encs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3259442, 'lng' => 21.1133006], + ], + 'Erdőbénye' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2662769, 'lng' => 21.3547995], + ], + 'Erdőhorváti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3158739, 'lng' => 21.4272709], + ], + 'Fáj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4219028, 'lng' => 21.0747972], + ], + 'Fancsal' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3552347, 'lng' => 21.064671], + ], + 'Farkaslyuk' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1876627, 'lng' => 20.3086509], + ], + 'Felsőberecki' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3595718, 'lng' => 21.6950761], + ], + 'Felsődobsza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2555859, 'lng' => 21.0764245], + ], + 'Felsőgagy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4289932, 'lng' => 21.0128468], + ], + 'Felsőkelecsény' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3600051, 'lng' => 20.5939689], + ], + 'Felsőnyárád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3299583, 'lng' => 20.5995966], + ], + 'Felsőregmec' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4915243, 'lng' => 21.6056225], + ], + 'Felsőtelekes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4058831, 'lng' => 20.6352386], + ], + 'Felsővadász' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3709811, 'lng' => 20.9195765], + ], + 'Felsőzsolca' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.1041265, 'lng' => 20.8595396], + ], + 'Filkeháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4960919, 'lng' => 21.4888024], + ], + 'Fony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3910341, 'lng' => 21.2865504], + ], + 'Forró' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3233535, 'lng' => 21.0880493], + ], + 'Fulókércs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4308674, 'lng' => 21.1049891], + ], + 'Füzér' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.539654, 'lng' => 21.4547936], + ], + 'Füzérkajata' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5182556, 'lng' => 21.5000318], + ], + 'Füzérkomlós' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5126205, 'lng' => 21.4532344], + ], + 'Füzérradvány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.483741, 'lng' => 21.530474], + ], + 'Gadna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4006289, 'lng' => 20.9296444], + ], + 'Gagyapáti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.409096, 'lng' => 21.0017182], + ], + 'Gagybátor' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.433303, 'lng' => 20.94859], + ], + 'Gagyvendégi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4285166, 'lng' => 20.972405], + ], + 'Galvács' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4190767, 'lng' => 20.7767621], + ], + 'Garadna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4174625, 'lng' => 21.17463], + ], + 'Gelej' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.828655, 'lng' => 20.7755503], + ], + 'Gesztely' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1026673, 'lng' => 20.9654647], + ], + 'Gibárt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3153245, 'lng' => 21.1603909], + ], + 'Girincs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9691368, 'lng' => 20.9846965], + ], + 'Golop' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2374312, 'lng' => 21.1893372], + ], + 'Gömörszőlős' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3730427, 'lng' => 20.4276758], + ], + 'Gönc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4727097, 'lng' => 21.2735417], + ], + 'Göncruszka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4488786, 'lng' => 21.239774], + ], + 'Györgytarló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2053902, 'lng' => 21.6316333], + ], + 'Halmaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2464584, 'lng' => 20.9983349], + ], + 'Hangács' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2896949, 'lng' => 20.8314625], + ], + 'Hangony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2290868, 'lng' => 20.198029], + ], + 'Háromhuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3780662, 'lng' => 21.4283347], + ], + 'Harsány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9679177, 'lng' => 20.7418041], + ], + 'Hegymeg' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3314259, 'lng' => 20.8614048], + ], + 'Hejce' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4234865, 'lng' => 21.2816978], + ], + 'Hejőbába' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9059201, 'lng' => 20.9452436], + ], + 'Hejőkeresztúr' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9610209, 'lng' => 20.8772681], + ], + 'Hejőkürt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8564708, 'lng' => 20.9930661], + ], + 'Hejőpapi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8972354, 'lng' => 20.9054713], + ], + 'Hejőszalonta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9388389, 'lng' => 20.8822344], + ], + 'Hercegkút' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3340476, 'lng' => 21.5301233], + ], + 'Hernádbűd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2966038, 'lng' => 21.137896], + ], + 'Hernádcéce' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3587807, 'lng' => 21.1976117], + ], + 'Hernádkak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0892117, 'lng' => 20.9635617], + ], + 'Hernádkércs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2420151, 'lng' => 21.0501362], + ], + 'Hernádnémeti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0716822, 'lng' => 20.9742345], + ], + 'Hernádpetri' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4815086, 'lng' => 21.1622472], + ], + 'Hernádszentandrás' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2890724, 'lng' => 21.0949074], + ], + 'Hernádszurdok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.48169, 'lng' => 21.2071561], + ], + 'Hernádvécse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4406714, 'lng' => 21.1687099], + ], + 'Hét' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.282992, 'lng' => 20.3875674], + ], + 'Hidasnémeti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5029778, 'lng' => 21.2293013], + ], + 'Hidvégardó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5598883, 'lng' => 20.8395348], + ], + 'Hollóháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5393716, 'lng' => 21.4144474], + ], + 'Homrogd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2834505, 'lng' => 20.9125329], + ], + 'Igrici' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8673926, 'lng' => 20.8831705], + ], + 'Imola' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4201572, 'lng' => 20.5516409], + ], + 'Ináncs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2861362, 'lng' => 21.0681971], + ], + 'Irota' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3964482, 'lng' => 20.8752667], + ], + 'Izsófalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3087892, 'lng' => 20.6536072], + ], + 'Jákfalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3316408, 'lng' => 20.569496], + ], + 'Járdánháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1551033, 'lng' => 20.2477262], + ], + 'Jósvafő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4826254, 'lng' => 20.5504479], + ], + 'Kács' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9574786, 'lng' => 20.6145847], + ], + 'Kánó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4276397, 'lng' => 20.5991681], + ], + 'Kány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.5151651, 'lng' => 21.0143542], + ], + 'Karcsa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3131571, 'lng' => 21.7953512], + ], + 'Karos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3312141, 'lng' => 21.7406654], + ], + 'Kazincbarcika' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2489437, 'lng' => 20.6189771], + ], + 'Kázsmárk' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2728658, 'lng' => 20.9760294], + ], + 'Kéked' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5447244, 'lng' => 21.3500526], + ], + 'Kelemér' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3551802, 'lng' => 20.4296357], + ], + 'Kenézlő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2004193, 'lng' => 21.5311235], + ], + 'Keresztéte' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4989547, 'lng' => 20.950696], + ], + 'Kesznyéten' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9694339, 'lng' => 21.0413905], + ], + 'Királd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2393694, 'lng' => 20.3764361], + ], + 'Kiscsécs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9678112, 'lng' => 21.011133], + ], + 'Kisgyőr' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0096251, 'lng' => 20.6874073], + ], + 'Kishuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4503449, 'lng' => 21.4814089], + ], + 'Kiskinizs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2508135, 'lng' => 21.0345918], + ], + 'Kisrozvágy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3491303, 'lng' => 21.9390758], + ], + 'Kissikátor' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1946631, 'lng' => 20.1302306], + ], + 'Kistokaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0397115, 'lng' => 20.8410079], + ], + 'Komjáti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5452009, 'lng' => 20.7618268], + ], + 'Komlóska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3404486, 'lng' => 21.4622875], + ], + 'Kondó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1880491, 'lng' => 20.6438586], + ], + 'Korlát' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3779667, 'lng' => 21.2457327], + ], + 'Köröm' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9842491, 'lng' => 20.9545886], + ], + 'Kovácsvágás' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.45352, 'lng' => 21.5283164], + ], + 'Krasznokvajda' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4705256, 'lng' => 20.9714153], + ], + 'Kupa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3316226, 'lng' => 20.9145594], + ], + 'Kurityán' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.310505, 'lng' => 20.62573], + ], + 'Lácacséke' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3664002, 'lng' => 21.9934562], + ], + 'Ládbesenyő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3432268, 'lng' => 20.7859308], + ], + 'Lak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3480907, 'lng' => 20.8662135], + ], + 'Legyesbénye' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1564545, 'lng' => 21.1530692], + ], + 'Léh' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2906948, 'lng' => 20.9807054], + ], + 'Lénárddaróc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1486722, 'lng' => 20.3728301], + ], + 'Litka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4544802, 'lng' => 21.0584273], + ], + 'Mád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1922445, 'lng' => 21.2759773], + ], + 'Makkoshotyka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3571928, 'lng' => 21.5164187], + ], + 'Mályi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0175678, 'lng' => 20.8292414], + ], + 'Mályinka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1545567, 'lng' => 20.4958901], + ], + 'Martonyi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4702379, 'lng' => 20.7660532], + ], + 'Megyaszó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1875185, 'lng' => 21.0547033], + ], + 'Méra' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3565901, 'lng' => 21.1469291], + ], + 'Meszes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.438651, 'lng' => 20.7950688], + ], + 'Mezőcsát' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8207081, 'lng' => 20.9051607], + ], + 'Mezőkeresztes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8262301, 'lng' => 20.6884043], + ], + 'Mezőkövesd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8074617, 'lng' => 20.5698525], + ], + 'Mezőnagymihály' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8062776, 'lng' => 20.7308177], + ], + 'Mezőnyárád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8585625, 'lng' => 20.6764688], + ], + 'Mezőzombor' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1501209, 'lng' => 21.2575954], + ], + 'Mikóháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4617944, 'lng' => 21.592572], + ], + 'Miskolc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.', 'Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1034775, 'lng' => 20.7784384], + ], + 'Mogyoróska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3759799, 'lng' => 21.3296401], + ], + 'Monaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3061021, 'lng' => 20.9348205], + ], + 'Monok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2099439, 'lng' => 21.149252], + ], + 'Múcsony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2758139, 'lng' => 20.6716209], + ], + 'Muhi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9778997, 'lng' => 20.9293321], + ], + 'Nagybarca' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2476865, 'lng' => 20.5280319], + ], + 'Nagycsécs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9601505, 'lng' => 20.9482798], + ], + 'Nagyhuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4290026, 'lng' => 21.492424], + ], + 'Nagykinizs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2344766, 'lng' => 21.0335706], + ], + 'Nagyrozvágy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3404683, 'lng' => 21.9228458], + ], + 'Négyes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7013, 'lng' => 20.7040224], + ], + 'Nekézseny' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1689694, 'lng' => 20.4291357], + ], + 'Nemesbikk' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8876867, 'lng' => 20.9661155], + ], + 'Novajidrány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.396674, 'lng' => 21.1688256], + ], + 'Nyékládháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9933002, 'lng' => 20.8429935], + ], + 'Nyésta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3702622, 'lng' => 20.9514276], + ], + 'Nyíri' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4986982, 'lng' => 21.440883], + ], + 'Nyomár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.275559, 'lng' => 20.8198353], + ], + 'Olaszliszka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2419377, 'lng' => 21.4279754], + ], + 'Onga' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1194769, 'lng' => 20.9065655], + ], + 'Ónod' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0024425, 'lng' => 20.9146535], + ], + 'Ormosbánya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3322064, 'lng' => 20.6493181], + ], + 'Oszlár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.8740321, 'lng' => 21.0332202], + ], + 'Ózd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2241439, 'lng' => 20.2888698], + ], + 'Pácin' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3306334, 'lng' => 21.8337743], + ], + 'Pálháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4717353, 'lng' => 21.507078], + ], + 'Pamlény' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.493024, 'lng' => 20.9282949], + ], + 'Pányok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5298401, 'lng' => 21.3478472], + ], + 'Parasznya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1688229, 'lng' => 20.6402064], + ], + 'Pere' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2845544, 'lng' => 21.1211586], + ], + 'Perecse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.5027869, 'lng' => 20.9845634], + ], + 'Perkupa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4712725, 'lng' => 20.6862819], + ], + 'Prügy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0824191, 'lng' => 21.2428751], + ], + 'Pusztafalu' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5439277, 'lng' => 21.4860599], + ], + 'Pusztaradvány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4679248, 'lng' => 21.1338715], + ], + 'Putnok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2939007, 'lng' => 20.4333508], + ], + 'Radostyán' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1787774, 'lng' => 20.6532017], + ], + 'Ragály' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4041753, 'lng' => 20.5211463], + ], + 'Rakaca' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4617206, 'lng' => 20.8848555], + ], + 'Rakacaszend' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4611034, 'lng' => 20.8378744], + ], + 'Rásonysápberencs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.304802, 'lng' => 20.9934828], + ], + 'Rátka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2156932, 'lng' => 21.2267141], + ], + 'Regéc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.392191, 'lng' => 21.3436481], + ], + 'Répáshuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0507939, 'lng' => 20.5254934], + ], + 'Révleányvár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3230427, 'lng' => 22.0416695], + ], + 'Ricse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3251432, 'lng' => 21.9687588], + ], + 'Rudabánya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3747405, 'lng' => 20.6206118], + ], + 'Rudolftelep' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3092868, 'lng' => 20.6711602], + ], + 'Sajóbábony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1742691, 'lng' => 20.734572], + ], + 'Sajóecseg' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.190065, 'lng' => 20.772827], + ], + 'Sajógalgóc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2929878, 'lng' => 20.5323886], + ], + 'Sajóhídvég' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0026817, 'lng' => 20.9495863], + ], + 'Sajóivánka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2654174, 'lng' => 20.5799268], + ], + 'Sajókápolna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1952827, 'lng' => 20.6848853], + ], + 'Sajókaza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2864119, 'lng' => 20.5851277], + ], + 'Sajókeresztúr' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.1694996, 'lng' => 20.7768886], + ], + 'Sajólád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0402765, 'lng' => 20.9024513], + ], + 'Sajólászlófalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1848765, 'lng' => 20.6736002], + ], + 'Sajómercse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2461305, 'lng' => 20.414773], + ], + 'Sajónémeti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.270659, 'lng' => 20.3811845], + ], + 'Sajóörös' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9515653, 'lng' => 21.0219599], + ], + 'Sajópálfala' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.163139, 'lng' => 20.8458093], + ], + 'Sajópetri' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0351497, 'lng' => 20.8878767], + ], + 'Sajópüspöki' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.280186, 'lng' => 20.3400614], + ], + 'Sajósenye' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1960682, 'lng' => 20.8185281], + ], + 'Sajószentpéter' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2188772, 'lng' => 20.7092248], + ], + 'Sajószöged' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9458004, 'lng' => 20.9946112], + ], + 'Sajóvámos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1802021, 'lng' => 20.8298154], + ], + 'Sajóvelezd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2714818, 'lng' => 20.4593985], + ], + 'Sály' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9527979, 'lng' => 20.6597197], + ], + 'Sárazsadány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2684871, 'lng' => 21.497789], + ], + 'Sárospatak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3196929, 'lng' => 21.5687308], + ], + 'Sáta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1876567, 'lng' => 20.3914051], + ], + 'Sátoraljaújhely' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3960601, 'lng' => 21.6551122], + ], + 'Selyeb' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3381582, 'lng' => 20.9541317], + ], + 'Semjén' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3521396, 'lng' => 21.9671011], + ], + 'Serényfalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3071589, 'lng' => 20.3852844], + ], + 'Sima' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2996969, 'lng' => 21.3030527], + ], + 'Sóstófalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.156243, 'lng' => 20.9870638], + ], + 'Szakácsi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3820531, 'lng' => 20.8614571], + ], + 'Szakáld' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9431182, 'lng' => 20.908997], + ], + 'Szalaszend' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3859709, 'lng' => 21.1243501], + ], + 'Szalonna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4500484, 'lng' => 20.7394926], + ], + 'Szászfa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4704359, 'lng' => 20.9418168], + ], + 'Szegi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.1953737, 'lng' => 21.3795562], + ], + 'Szegilong' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2162488, 'lng' => 21.3965639], + ], + 'Szemere' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4661495, 'lng' => 21.099542], + ], + 'Szendrő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4046962, 'lng' => 20.7282046], + ], + 'Szendrőlád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3433366, 'lng' => 20.7419436], + ], + 'Szentistván' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7737632, 'lng' => 20.6579694], + ], + 'Szentistvánbaksa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2227558, 'lng' => 21.0276456], + ], + 'Szerencs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1590429, 'lng' => 21.2048872], + ], + 'Szikszó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1989312, 'lng' => 20.9298039], + ], + 'Szin' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4972791, 'lng' => 20.6601922], + ], + 'Szinpetri' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4847097, 'lng' => 20.625043], + ], + 'Szirmabesenyő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.1509585, 'lng' => 20.7957903], + ], + 'Szögliget' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5215045, 'lng' => 20.6770697], + ], + 'Szőlősardó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.443484, 'lng' => 20.6278686], + ], + 'Szomolya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8919105, 'lng' => 20.4949334], + ], + 'Szuhafő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4082703, 'lng' => 20.4515974], + ], + 'Szuhakálló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2835218, 'lng' => 20.6523991], + ], + 'Szuhogy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3842029, 'lng' => 20.6731282], + ], + 'Taktabáj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0621903, 'lng' => 21.3112131], + ], + 'Taktaharkány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0876121, 'lng' => 21.129918], + ], + 'Taktakenéz' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0508677, 'lng' => 21.2167146], + ], + 'Taktaszada' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1103437, 'lng' => 21.1735733], + ], + 'Tállya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2352295, 'lng' => 21.2260996], + ], + 'Tarcal' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1311328, 'lng' => 21.3418021], + ], + 'Tard' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8784711, 'lng' => 20.598937], + ], + 'Tardona' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1699442, 'lng' => 20.531454], + ], + 'Telkibánya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4854061, 'lng' => 21.3574907], + ], + 'Teresztenye' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4463436, 'lng' => 20.6031689], + ], + 'Tibolddaróc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9206758, 'lng' => 20.6355357], + ], + 'Tiszabábolna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.689752, 'lng' => 20.813906], + ], + 'Tiszacsermely' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2336812, 'lng' => 21.7945686], + ], + 'Tiszadorogma' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.6839826, 'lng' => 20.8661184], + ], + 'Tiszakarád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2061184, 'lng' => 21.7213149], + ], + 'Tiszakeszi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7879554, 'lng' => 20.9904672], + ], + 'Tiszaladány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0621067, 'lng' => 21.4101619], + ], + 'Tiszalúc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0358262, 'lng' => 21.0648204], + ], + 'Tiszapalkonya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.8849204, 'lng' => 21.0557818], + ], + 'Tiszatardos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0406385, 'lng' => 21.379655], + ], + 'Tiszatarján' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8329217, 'lng' => 21.0014346], + ], + 'Tiszaújváros' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9159846, 'lng' => 21.0427447], + ], + 'Tiszavalk' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.6888504, 'lng' => 20.751499], + ], + 'Tokaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1172148, 'lng' => 21.4089015], + ], + 'Tolcsva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2841513, 'lng' => 21.4488452], + ], + 'Tomor' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3258904, 'lng' => 20.8823733], + ], + 'Tornabarakony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4922432, 'lng' => 20.8192157], + ], + 'Tornakápolna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4616855, 'lng' => 20.617706], + ], + 'Tornanádaska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5611186, 'lng' => 20.7846392], + ], + 'Tornaszentandrás' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5226438, 'lng' => 20.7790226], + ], + 'Tornaszentjakab' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.5244312, 'lng' => 20.8729813], + ], + 'Tornyosnémeti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5202757, 'lng' => 21.2506927], + ], + 'Trizs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4251253, 'lng' => 20.4958645], + ], + 'Újcsanálos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1380468, 'lng' => 21.0036907], + ], + 'Uppony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2155013, 'lng' => 20.434654], + ], + 'Vadna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2733247, 'lng' => 20.5552218], + ], + 'Vágáshuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4264605, 'lng' => 21.545222], + ], + 'Vajdácska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3196383, 'lng' => 21.6541401], + ], + 'Vámosújfalu' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2575496, 'lng' => 21.4524394], + ], + 'Varbó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1631678, 'lng' => 20.6217693], + ], + 'Varbóc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4644075, 'lng' => 20.6450152], + ], + 'Vatta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9228447, 'lng' => 20.7389995], + ], + 'Vilmány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4166062, 'lng' => 21.2302229], + ], + 'Vilyvitány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4952223, 'lng' => 21.5589737], + ], + 'Viss' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2176861, 'lng' => 21.5069652], + ], + 'Viszló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4939386, 'lng' => 20.8862569], + ], + 'Vizsoly' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3845496, 'lng' => 21.2158416], + ], + 'Zádorfalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3860789, 'lng' => 20.4852484], + ], + 'Zalkod' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.1857296, 'lng' => 21.4592752], + ], + 'Zemplénagárd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.36024, 'lng' => 22.0709646], + ], + 'Ziliz' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2511796, 'lng' => 20.7922106], + ], + 'Zsujta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4997896, 'lng' => 21.2789138], + ], + 'Zubogy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3792388, 'lng' => 20.5758141], + ], + ], + 'Budapest' => [ + 'Budapest I. ker.' => [ + 'constituencies' => ['Budapest 01.'], + 'coordinates' => ['lat' => 47.4968219, 'lng' => 19.037458], + ], + 'Budapest II. ker.' => [ + 'constituencies' => ['Budapest 03.', 'Budapest 04.'], + 'coordinates' => ['lat' => 47.5393329, 'lng' => 18.986934], + ], + 'Budapest III. ker.' => [ + 'constituencies' => ['Budapest 04.', 'Budapest 10.'], + 'coordinates' => ['lat' => 47.5671768, 'lng' => 19.0368517], + ], + 'Budapest IV. ker.' => [ + 'constituencies' => ['Budapest 11.', 'Budapest 12.'], + 'coordinates' => ['lat' => 47.5648915, 'lng' => 19.0913149], + ], + 'Budapest V. ker.' => [ + 'constituencies' => ['Budapest 01.'], + 'coordinates' => ['lat' => 47.5002319, 'lng' => 19.0520181], + ], + 'Budapest VI. ker.' => [ + 'constituencies' => ['Budapest 05.'], + 'coordinates' => ['lat' => 47.509863, 'lng' => 19.0625813], + ], + 'Budapest VII. ker.' => [ + 'constituencies' => ['Budapest 05.'], + 'coordinates' => ['lat' => 47.5027289, 'lng' => 19.073376], + ], + 'Budapest VIII. ker.' => [ + 'constituencies' => ['Budapest 01.', 'Budapest 06.'], + 'coordinates' => ['lat' => 47.4894184, 'lng' => 19.070668], + ], + 'Budapest IX. ker.' => [ + 'constituencies' => ['Budapest 01.', 'Budapest 06.'], + 'coordinates' => ['lat' => 47.4649279, 'lng' => 19.0916229], + ], + 'Budapest X. ker.' => [ + 'constituencies' => ['Budapest 09.', 'Budapest 14.'], + 'coordinates' => ['lat' => 47.4820909, 'lng' => 19.1575028], + ], + 'Budapest XI. ker.' => [ + 'constituencies' => ['Budapest 02.', 'Budapest 18.'], + 'coordinates' => ['lat' => 47.4593099, 'lng' => 19.0187389], + ], + 'Budapest XII. ker.' => [ + 'constituencies' => ['Budapest 03.'], + 'coordinates' => ['lat' => 47.4991199, 'lng' => 18.990459], + ], + 'Budapest XIII. ker.' => [ + 'constituencies' => ['Budapest 11.', 'Budapest 07.'], + 'coordinates' => ['lat' => 47.5355105, 'lng' => 19.0709266], + ], + 'Budapest XIV. ker.' => [ + 'constituencies' => ['Budapest 08.', 'Budapest 13.'], + 'coordinates' => ['lat' => 47.5224569, 'lng' => 19.114709], + ], + 'Budapest XV. ker.' => [ + 'constituencies' => ['Budapest 12.'], + 'coordinates' => ['lat' => 47.5589, 'lng' => 19.1193], + ], + 'Budapest XVI. ker.' => [ + 'constituencies' => ['Budapest 13.'], + 'coordinates' => ['lat' => 47.5183029, 'lng' => 19.191941], + ], + 'Budapest XVII. ker.' => [ + 'constituencies' => ['Budapest 14.'], + 'coordinates' => ['lat' => 47.4803, 'lng' => 19.2667001], + ], + 'Budapest XVIII. ker.' => [ + 'constituencies' => ['Budapest 15.'], + 'coordinates' => ['lat' => 47.4281229, 'lng' => 19.2098429], + ], + 'Budapest XIX. ker.' => [ + 'constituencies' => ['Budapest 09.', 'Budapest 16.'], + 'coordinates' => ['lat' => 47.4457289, 'lng' => 19.1430149], + ], + 'Budapest XX. ker.' => [ + 'constituencies' => ['Budapest 16.'], + 'coordinates' => ['lat' => 47.4332879, 'lng' => 19.1193169], + ], + 'Budapest XXI. ker.' => [ + 'constituencies' => ['Budapest 17.'], + 'coordinates' => ['lat' => 47.4243579, 'lng' => 19.066142], + ], + 'Budapest XXII. ker.' => [ + 'constituencies' => ['Budapest 18.'], + 'coordinates' => ['lat' => 47.425, 'lng' => 19.031667], + ], + 'Budapest XXIII. ker.' => [ + 'constituencies' => ['Budapest 17.'], + 'coordinates' => ['lat' => 47.3939599, 'lng' => 19.122523], + ], + ], + 'Csongrád-Csanád' => [ + 'Algyő' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3329625, 'lng' => 20.207889], + ], + 'Ambrózfalva' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3501417, 'lng' => 20.7313995], + ], + 'Apátfalva' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.173317, 'lng' => 20.5800472], + ], + 'Árpádhalom' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6158286, 'lng' => 20.547733], + ], + 'Ásotthalom' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.1995983, 'lng' => 19.7833756], + ], + 'Baks' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5518708, 'lng' => 20.1064166], + ], + 'Balástya' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.4261828, 'lng' => 20.004933], + ], + 'Bordány' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3194213, 'lng' => 19.9227063], + ], + 'Csanádalberti' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3267872, 'lng' => 20.7068631], + ], + 'Csanádpalota' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2407708, 'lng' => 20.7228873], + ], + 'Csanytelek' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6014883, 'lng' => 20.1114379], + ], + 'Csengele' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5411505, 'lng' => 19.8644533], + ], + 'Csongrád-Csanád' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.7084264, 'lng' => 20.1436061], + ], + 'Derekegyház' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.580238, 'lng' => 20.3549845], + ], + 'Deszk' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.2179603, 'lng' => 20.2404106], + ], + 'Dóc' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.437292, 'lng' => 20.1363129], + ], + 'Domaszék' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2466283, 'lng' => 19.9990365], + ], + 'Eperjes' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.7076258, 'lng' => 20.5621489], + ], + 'Fábiánsebestyén' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6748615, 'lng' => 20.455037], + ], + 'Felgyő' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6616513, 'lng' => 20.1097394], + ], + 'Ferencszállás' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.2158295, 'lng' => 20.3553359], + ], + 'Földeák' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3184223, 'lng' => 20.4929019], + ], + 'Forráskút' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3655956, 'lng' => 19.9089055], + ], + 'Hódmezővásárhely' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.4181262, 'lng' => 20.3300315], + ], + 'Királyhegyes' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2717114, 'lng' => 20.6126302], + ], + 'Kistelek' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.4694781, 'lng' => 19.9804365], + ], + 'Kiszombor' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.1856953, 'lng' => 20.4265486], + ], + 'Klárafalva' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.220953, 'lng' => 20.3255224], + ], + 'Kövegy' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2246141, 'lng' => 20.6840764], + ], + 'Kübekháza' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.1500892, 'lng' => 20.276983], + ], + 'Magyarcsanád' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.1698824, 'lng' => 20.6132706], + ], + 'Makó' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2219071, 'lng' => 20.4809265], + ], + 'Maroslele' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2698362, 'lng' => 20.3418589], + ], + 'Mártély' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.4682451, 'lng' => 20.2416146], + ], + 'Mindszent' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5227585, 'lng' => 20.1895798], + ], + 'Mórahalom' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2179218, 'lng' => 19.88372], + ], + 'Nagyér' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3703008, 'lng' => 20.729605], + ], + 'Nagylak' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.1737713, 'lng' => 20.7111982], + ], + 'Nagymágocs' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5857132, 'lng' => 20.4833875], + ], + 'Nagytőke' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.7552639, 'lng' => 20.2860999], + ], + 'Óföldeák' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2985957, 'lng' => 20.4369086], + ], + 'Ópusztaszer' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.4957061, 'lng' => 20.0665358], + ], + 'Öttömös' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2808756, 'lng' => 19.6826038], + ], + 'Pitvaros' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3194853, 'lng' => 20.7385996], + ], + 'Pusztamérges' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3280134, 'lng' => 19.6849699], + ], + 'Pusztaszer' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5515959, 'lng' => 19.9870098], + ], + 'Röszke' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.1873773, 'lng' => 20.037455], + ], + 'Ruzsa' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2890678, 'lng' => 19.7481121], + ], + 'Sándorfalva' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.3635951, 'lng' => 20.1032227], + ], + 'Szatymaz' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.3426558, 'lng' => 20.0391941], + ], + 'Szeged' => [ + 'constituencies' => ['Csongrád-Csanád 2.', 'Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.2530102, 'lng' => 20.1414253], + ], + 'Szegvár' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5816447, 'lng' => 20.2266415], + ], + 'Székkutas' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.5063976, 'lng' => 20.537673], + ], + 'Szentes' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.654789, 'lng' => 20.2637492], + ], + 'Tiszasziget' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.1720458, 'lng' => 20.1618289], + ], + 'Tömörkény' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6166243, 'lng' => 20.0436896], + ], + 'Újszentiván' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.1859286, 'lng' => 20.1835123], + ], + 'Üllés' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3355015, 'lng' => 19.8489644], + ], + 'Zákányszék' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2752726, 'lng' => 19.8883111], + ], + 'Zsombó' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3284014, 'lng' => 19.9766186], + ], + ], + 'Fejér' => [ + 'Aba' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0328193, 'lng' => 18.522359], + ], + 'Adony' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.119831, 'lng' => 18.8612469], + ], + 'Alap' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8075763, 'lng' => 18.684028], + ], + 'Alcsútdoboz' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4277067, 'lng' => 18.6030325], + ], + 'Alsószentiván' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.7910573, 'lng' => 18.732161], + ], + 'Bakonycsernye' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.321719, 'lng' => 18.0907379], + ], + 'Bakonykúti' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2458464, 'lng' => 18.195769], + ], + 'Balinka' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3135736, 'lng' => 18.1907168], + ], + 'Baracs' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9049033, 'lng' => 18.8752931], + ], + 'Baracska' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2824737, 'lng' => 18.7598901], + ], + 'Beloiannisz' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.183143, 'lng' => 18.8245727], + ], + 'Besnyő' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.1892568, 'lng' => 18.7936832], + ], + 'Bicske' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4911792, 'lng' => 18.6370142], + ], + 'Bodajk' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3209663, 'lng' => 18.2339242], + ], + 'Bodmér' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4489857, 'lng' => 18.5383832], + ], + 'Cece' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.7698199, 'lng' => 18.6336808], + ], + 'Csabdi' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.5229299, 'lng' => 18.6085371], + ], + 'Csákberény' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3506861, 'lng' => 18.3265064], + ], + 'Csákvár' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3941468, 'lng' => 18.4602445], + ], + 'Csókakő' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3533961, 'lng' => 18.2693867], + ], + 'Csór' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2049913, 'lng' => 18.2557813], + ], + 'Csősz' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0382791, 'lng' => 18.414533], + ], + 'Daruszentmiklós' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.87194, 'lng' => 18.8568642], + ], + 'Dég' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8707664, 'lng' => 18.4445717], + ], + 'Dunaújváros' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 46.9619059, 'lng' => 18.9355227], + ], + 'Előszállás' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8276091, 'lng' => 18.8280627], + ], + 'Enying' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9326943, 'lng' => 18.2414807], + ], + 'Ercsi' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.2482238, 'lng' => 18.8912626], + ], + 'Etyek' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4467098, 'lng' => 18.751179], + ], + 'Fehérvárcsurgó' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2904264, 'lng' => 18.2645262], + ], + 'Felcsút' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4541851, 'lng' => 18.5865775], + ], + 'Füle' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.0535367, 'lng' => 18.2480871], + ], + 'Gánt' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3902121, 'lng' => 18.387061], + ], + 'Gárdony' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.196537, 'lng' => 18.6115195], + ], + 'Gyúró' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3700577, 'lng' => 18.7384824], + ], + 'Hantos' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9943127, 'lng' => 18.6989263], + ], + 'Igar' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.7757642, 'lng' => 18.5137348], + ], + 'Iszkaszentgyörgy' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2399338, 'lng' => 18.2987232], + ], + 'Isztimér' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2787058, 'lng' => 18.1955966], + ], + 'Iváncsa' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.153376, 'lng' => 18.8270434], + ], + 'Jenő' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1047531, 'lng' => 18.2453199], + ], + 'Kajászó' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3234883, 'lng' => 18.7221054], + ], + 'Káloz' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9568415, 'lng' => 18.4853961], + ], + 'Kápolnásnyék' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2398554, 'lng' => 18.6764288], + ], + 'Kincsesbánya' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2632477, 'lng' => 18.2764679], + ], + 'Kisapostag' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8940766, 'lng' => 18.9323135], + ], + 'Kisláng' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9598173, 'lng' => 18.3860884], + ], + 'Kőszárhegy' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.0926048, 'lng' => 18.341234], + ], + 'Kulcs' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.0541246, 'lng' => 18.9197178], + ], + 'Lajoskomárom' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.841585, 'lng' => 18.3355393], + ], + 'Lepsény' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9918514, 'lng' => 18.2469618], + ], + 'Lovasberény' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3109278, 'lng' => 18.5527924], + ], + 'Magyaralmás' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2913027, 'lng' => 18.3245512], + ], + 'Mány' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.5321762, 'lng' => 18.6555811], + ], + 'Martonvásár' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3164516, 'lng' => 18.7877558], + ], + 'Mátyásdomb' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9228626, 'lng' => 18.3470929], + ], + 'Mezőfalva' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9323938, 'lng' => 18.7771045], + ], + 'Mezőkomárom' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8276482, 'lng' => 18.2934472], + ], + 'Mezőszentgyörgy' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9920267, 'lng' => 18.2795568], + ], + 'Mezőszilas' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8166957, 'lng' => 18.4754679], + ], + 'Moha' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2437717, 'lng' => 18.3313907], + ], + 'Mór' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.374928, 'lng' => 18.2036035], + ], + 'Nadap' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2585056, 'lng' => 18.6167437], + ], + 'Nádasdladány' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1341786, 'lng' => 18.2394077], + ], + 'Nagykarácsony' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8706425, 'lng' => 18.7725518], + ], + 'Nagylók' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9764964, 'lng' => 18.64115], + ], + 'Nagyveleg' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.361797, 'lng' => 18.111061], + ], + 'Nagyvenyim' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 46.9571015, 'lng' => 18.8576229], + ], + 'Óbarok' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4922397, 'lng' => 18.5681206], + ], + 'Pákozd' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2172004, 'lng' => 18.5430768], + ], + 'Pátka' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2752462, 'lng' => 18.4950339], + ], + 'Pázmánd' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.283645, 'lng' => 18.654854], + ], + 'Perkáta' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.0482285, 'lng' => 18.784294], + ], + 'Polgárdi' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.0601257, 'lng' => 18.2993645], + ], + 'Pusztaszabolcs' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.1408918, 'lng' => 18.7601638], + ], + 'Pusztavám' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.4297438, 'lng' => 18.2317401], + ], + 'Rácalmás' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.0243223, 'lng' => 18.9350709], + ], + 'Ráckeresztúr' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2729155, 'lng' => 18.8330106], + ], + 'Sárbogárd' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.879104, 'lng' => 18.6213353], + ], + 'Sáregres' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.783236, 'lng' => 18.5935136], + ], + 'Sárkeresztes' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2517488, 'lng' => 18.3541822], + ], + 'Sárkeresztúr' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0025252, 'lng' => 18.5479461], + ], + 'Sárkeszi' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1582764, 'lng' => 18.284968], + ], + 'Sárosd' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0414738, 'lng' => 18.6488144], + ], + 'Sárszentágota' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9706742, 'lng' => 18.5634969], + ], + 'Sárszentmihály' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1537282, 'lng' => 18.3235014], + ], + 'Seregélyes' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.1100586, 'lng' => 18.5788431], + ], + 'Soponya' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0120427, 'lng' => 18.4543505], + ], + 'Söréd' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.322683, 'lng' => 18.280508], + ], + 'Sukoró' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2425436, 'lng' => 18.6022803], + ], + 'Szabadbattyán' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1175572, 'lng' => 18.3681061], + ], + 'Szabadegyháza' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0770131, 'lng' => 18.6912379], + ], + 'Szabadhídvég' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8210159, 'lng' => 18.2798938], + ], + 'Szár' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4791911, 'lng' => 18.5158147], + ], + 'Székesfehérvár' => [ + 'constituencies' => ['Fejér 2.', 'Fejér 1.'], + 'coordinates' => ['lat' => 47.1860262, 'lng' => 18.4221358], + ], + 'Tabajd' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4045316, 'lng' => 18.6302011], + ], + 'Tác' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0794264, 'lng' => 18.403381], + ], + 'Tordas' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3440943, 'lng' => 18.7483302], + ], + 'Újbarok' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4791337, 'lng' => 18.5585574], + ], + 'Úrhida' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1298384, 'lng' => 18.3321437], + ], + 'Vajta' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.7227758, 'lng' => 18.6618091], + ], + 'Vál' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3624339, 'lng' => 18.6766737], + ], + 'Velence' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2300924, 'lng' => 18.6506424], + ], + 'Vereb' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.318485, 'lng' => 18.6197301], + ], + 'Vértesacsa' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3700218, 'lng' => 18.5792793], + ], + 'Vértesboglár' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4291347, 'lng' => 18.5235823], + ], + 'Zámoly' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3168103, 'lng' => 18.408371], + ], + 'Zichyújfalu' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.1291991, 'lng' => 18.6692222], + ], + ], + 'Győr-Moson-Sopron' => [ + 'Abda' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.6962149, 'lng' => 17.5445786], + ], + 'Acsalag' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.676095, 'lng' => 17.1977771], + ], + 'Ágfalva' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.688862, 'lng' => 16.5110233], + ], + 'Agyagosszergény' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.608545, 'lng' => 16.9409912], + ], + 'Árpás' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5134127, 'lng' => 17.3931579], + ], + 'Ásványráró' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8287695, 'lng' => 17.499195], + ], + 'Babót' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5752269, 'lng' => 17.0758604], + ], + 'Bágyogszovát' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5866036, 'lng' => 17.3617273], + ], + 'Bakonygyirót' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4181388, 'lng' => 17.8055502], + ], + 'Bakonypéterd' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4667076, 'lng' => 17.7967619], + ], + 'Bakonyszentlászló' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.3892006, 'lng' => 17.8032754], + ], + 'Barbacs' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6455476, 'lng' => 17.297216], + ], + 'Beled' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4662675, 'lng' => 17.0959263], + ], + 'Bezenye' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9609867, 'lng' => 17.216211], + ], + 'Bezi' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6737572, 'lng' => 17.3921093], + ], + 'Bodonhely' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5655752, 'lng' => 17.4072124], + ], + 'Bogyoszló' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5609657, 'lng' => 17.1850606], + ], + 'Bőny' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6516279, 'lng' => 17.8703841], + ], + 'Börcs' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.6862052, 'lng' => 17.4988893], + ], + 'Bősárkány' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6881947, 'lng' => 17.2507143], + ], + 'Cakóháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.6967121, 'lng' => 17.2863758], + ], + 'Cirák' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4779219, 'lng' => 17.0282338], + ], + 'Csáfordjánosfa' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4151998, 'lng' => 16.9510595], + ], + 'Csapod' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5162077, 'lng' => 16.9234546], + ], + 'Csér' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4169765, 'lng' => 16.9330737], + ], + 'Csikvánd' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4666335, 'lng' => 17.4546305], + ], + 'Csorna' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6103234, 'lng' => 17.2462444], + ], + 'Darnózseli' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8493957, 'lng' => 17.4273958], + ], + 'Dénesfa' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4558445, 'lng' => 17.0335351], + ], + 'Dör' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5979168, 'lng' => 17.2991911], + ], + 'Dunakiliti' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9659588, 'lng' => 17.2882641], + ], + 'Dunaremete' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8761957, 'lng' => 17.4375005], + ], + 'Dunaszeg' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7692554, 'lng' => 17.5407805], + ], + 'Dunaszentpál' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7771623, 'lng' => 17.5043978], + ], + 'Dunasziget' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9359671, 'lng' => 17.3617867], + ], + 'Ebergőc' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5635832, 'lng' => 16.81167], + ], + 'Écs' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5604415, 'lng' => 17.7072193], + ], + 'Edve' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4551126, 'lng' => 17.135508], + ], + 'Egyed' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5192845, 'lng' => 17.3396861], + ], + 'Egyházasfalu' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.46243, 'lng' => 16.7679871], + ], + 'Enese' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6461219, 'lng' => 17.4235267], + ], + 'Farád' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6064483, 'lng' => 17.2003347], + ], + 'Fehértó' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6759514, 'lng' => 17.3453497], + ], + 'Feketeerdő' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9355702, 'lng' => 17.2783691], + ], + 'Felpéc' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5225976, 'lng' => 17.5993517], + ], + 'Fenyőfő' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.3490387, 'lng' => 17.7656259], + ], + 'Fertőboz' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.633426, 'lng' => 16.6998899], + ], + 'Fertőd' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.61818, 'lng' => 16.8741418], + ], + 'Fertőendréd' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6054618, 'lng' => 16.9085891], + ], + 'Fertőhomok' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6196363, 'lng' => 16.7710445], + ], + 'Fertőrákos' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.7209654, 'lng' => 16.6488128], + ], + 'Fertőszentmiklós' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5895578, 'lng' => 16.8730712], + ], + 'Fertőszéplak' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6172442, 'lng' => 16.8405708], + ], + 'Gönyű' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.7334344, 'lng' => 17.8243403], + ], + 'Gyalóka' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4427372, 'lng' => 16.696223], + ], + 'Gyarmat' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4604024, 'lng' => 17.4964917], + ], + 'Gyömöre' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4982876, 'lng' => 17.564804], + ], + 'Győr' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.', 'Győr-Moson-Sopron 1.'], + 'coordinates' => ['lat' => 47.6874569, 'lng' => 17.6503974], + ], + 'Győrasszonyfa' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4950098, 'lng' => 17.8072327], + ], + 'Győrladamér' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7545651, 'lng' => 17.5633004], + ], + 'Gyóró' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4916519, 'lng' => 17.0236667], + ], + 'Győrság' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5751529, 'lng' => 17.7515893], + ], + 'Győrsövényház' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6909394, 'lng' => 17.3734235], + ], + 'Győrszemere' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.551813, 'lng' => 17.5635661], + ], + 'Győrújbarát' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6076284, 'lng' => 17.6389745], + ], + 'Győrújfalu' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.722197, 'lng' => 17.6054524], + ], + 'Győrzámoly' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7434268, 'lng' => 17.5770199], + ], + 'Halászi' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8903231, 'lng' => 17.3256673], + ], + 'Harka' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6339566, 'lng' => 16.5986264], + ], + 'Hédervár' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.831062, 'lng' => 17.4541026], + ], + 'Hegyeshalom' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9117445, 'lng' => 17.156071], + ], + 'Hegykő' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6188466, 'lng' => 16.7940292], + ], + 'Hidegség' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6253847, 'lng' => 16.740935], + ], + 'Himod' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5200248, 'lng' => 17.0064434], + ], + 'Hövej' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5524954, 'lng' => 17.0166402], + ], + 'Ikrény' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6539897, 'lng' => 17.5281764], + ], + 'Iván' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.445549, 'lng' => 16.9096056], + ], + 'Jánossomorja' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7847917, 'lng' => 17.1298642], + ], + 'Jobaháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5799316, 'lng' => 17.1886952], + ], + 'Kajárpéc' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4888221, 'lng' => 17.6350057], + ], + 'Kapuvár' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5912437, 'lng' => 17.0301952], + ], + 'Károlyháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8032696, 'lng' => 17.3446363], + ], + 'Kimle' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8172115, 'lng' => 17.3676625], + ], + 'Kisbabot' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5551791, 'lng' => 17.4149558], + ], + 'Kisbajcs' => [ + 'constituencies' => ['Győr-Moson-Sopron 1.'], + 'coordinates' => ['lat' => 47.7450615, 'lng' => 17.6800942], + ], + 'Kisbodak' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8963234, 'lng' => 17.4196192], + ], + 'Kisfalud' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.2041959, 'lng' => 18.494568], + ], + 'Kóny' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6307264, 'lng' => 17.3596093], + ], + 'Kópháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6385359, 'lng' => 16.6451629], + ], + 'Koroncó' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5999604, 'lng' => 17.5284792], + ], + 'Kunsziget' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7385858, 'lng' => 17.5176565], + ], + 'Lázi' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4661979, 'lng' => 17.8346909], + ], + 'Lébény' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7360651, 'lng' => 17.3905652], + ], + 'Levél' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8949275, 'lng' => 17.2001946], + ], + 'Lipót' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8615868, 'lng' => 17.4603528], + ], + 'Lövő' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5107966, 'lng' => 16.7898395], + ], + 'Maglóca' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6625685, 'lng' => 17.2751221], + ], + 'Magyarkeresztúr' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5200063, 'lng' => 17.1660121], + ], + 'Máriakálnok' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8596905, 'lng' => 17.3237666], + ], + 'Markotabödöge' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6815136, 'lng' => 17.3116772], + ], + 'Mecsér' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.796671, 'lng' => 17.4744842], + ], + 'Mérges' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6012809, 'lng' => 17.4438455], + ], + 'Mezőörs' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.568844, 'lng' => 17.8821253], + ], + 'Mihályi' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5142703, 'lng' => 17.0958265], + ], + 'Mórichida' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5127896, 'lng' => 17.4218174], + ], + 'Mosonmagyaróvár' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8681469, 'lng' => 17.2689169], + ], + 'Mosonszentmiklós' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7294576, 'lng' => 17.4242231], + ], + 'Mosonszolnok' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8511108, 'lng' => 17.1735793], + ], + 'Mosonudvar' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8435379, 'lng' => 17.224348], + ], + 'Nagybajcs' => [ + 'constituencies' => ['Győr-Moson-Sopron 1.'], + 'coordinates' => ['lat' => 47.7639168, 'lng' => 17.686613], + ], + 'Nagycenk' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6081549, 'lng' => 16.6979223], + ], + 'Nagylózs' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5654858, 'lng' => 16.76965], + ], + 'Nagyszentjános' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.7100868, 'lng' => 17.8681808], + ], + 'Nemeskér' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.483855, 'lng' => 16.8050771], + ], + 'Nyalka' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5443407, 'lng' => 17.8091081], + ], + 'Nyúl' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5832389, 'lng' => 17.6862095], + ], + 'Osli' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6385609, 'lng' => 17.0755158], + ], + 'Öttevény' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7255506, 'lng' => 17.4899552], + ], + 'Páli' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4774264, 'lng' => 17.1695082], + ], + 'Pannonhalma' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.549497, 'lng' => 17.7552412], + ], + 'Pásztori' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5553919, 'lng' => 17.2696728], + ], + 'Pázmándfalu' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5710798, 'lng' => 17.7810865], + ], + 'Pér' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6111604, 'lng' => 17.8049747], + ], + 'Pereszteg' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.594289, 'lng' => 16.7354028], + ], + 'Petőháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5965785, 'lng' => 16.8954138], + ], + 'Pinnye' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5855193, 'lng' => 16.7706082], + ], + 'Potyond' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.549377, 'lng' => 17.1821874], + ], + 'Püski' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8846385, 'lng' => 17.4070152], + ], + 'Pusztacsalád' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4853081, 'lng' => 16.9013644], + ], + 'Rábacsanak' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5256113, 'lng' => 17.2902872], + ], + 'Rábacsécsény' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5879598, 'lng' => 17.4227941], + ], + 'Rábakecöl' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4324946, 'lng' => 17.1126349], + ], + 'Rábapatona' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6314656, 'lng' => 17.4797584], + ], + 'Rábapordány' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5574649, 'lng' => 17.3262502], + ], + 'Rábasebes' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4392738, 'lng' => 17.2423807], + ], + 'Rábaszentandrás' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4596327, 'lng' => 17.3272097], + ], + 'Rábaszentmihály' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5775103, 'lng' => 17.4312379], + ], + 'Rábaszentmiklós' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5381909, 'lng' => 17.417513], + ], + 'Rábatamási' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5893387, 'lng' => 17.1699767], + ], + 'Rábcakapi' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7079835, 'lng' => 17.2755839], + ], + 'Rajka' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9977901, 'lng' => 17.1983996], + ], + 'Ravazd' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5162349, 'lng' => 17.7512699], + ], + 'Répceszemere' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4282026, 'lng' => 16.9738943], + ], + 'Répcevis' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4427966, 'lng' => 16.6731972], + ], + 'Rétalap' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6072246, 'lng' => 17.9071507], + ], + 'Röjtökmuzsaj' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5543502, 'lng' => 16.8363467], + ], + 'Románd' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4484049, 'lng' => 17.7909987], + ], + 'Sarród' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6315873, 'lng' => 16.8613408], + ], + 'Sikátor' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4370828, 'lng' => 17.8510581], + ], + 'Sobor' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4768368, 'lng' => 17.3752902], + ], + 'Sokorópátka' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4892381, 'lng' => 17.6953943], + ], + 'Sopron' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6816619, 'lng' => 16.5844795], + ], + 'Sopronhorpács' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4831854, 'lng' => 16.7359058], + ], + 'Sopronkövesd' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5460504, 'lng' => 16.7432859], + ], + 'Sopronnémeti' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5364397, 'lng' => 17.2070182], + ], + 'Szakony' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4262848, 'lng' => 16.7154462], + ], + 'Szany' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4620733, 'lng' => 17.3027671], + ], + 'Szárföld' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5933239, 'lng' => 17.1221243], + ], + 'Szerecseny' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4628425, 'lng' => 17.5536197], + ], + 'Szil' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.501622, 'lng' => 17.233297], + ], + 'Szilsárkány' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5396552, 'lng' => 17.2545808], + ], + 'Táp' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5168299, 'lng' => 17.8292989], + ], + 'Tápszentmiklós' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4930151, 'lng' => 17.8524913], + ], + 'Tarjánpuszta' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5062161, 'lng' => 17.7869857], + ], + 'Tárnokréti' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.7217546, 'lng' => 17.3078226], + ], + 'Tényő' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5407376, 'lng' => 17.6490009], + ], + 'Tét' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5198967, 'lng' => 17.5108553], + ], + 'Töltéstava' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6273335, 'lng' => 17.7343778], + ], + 'Újkér' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4573295, 'lng' => 16.8187647], + ], + 'Újrónafő' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8101728, 'lng' => 17.2015241], + ], + 'Und' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.488856, 'lng' => 16.6961552], + ], + 'Vadosfa' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4986805, 'lng' => 17.1287654], + ], + 'Vág' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4469264, 'lng' => 17.2121765], + ], + 'Vámosszabadi' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7571476, 'lng' => 17.6507532], + ], + 'Várbalog' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8347267, 'lng' => 17.0720923], + ], + 'Vásárosfalu' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4537986, 'lng' => 17.1158473], + ], + 'Vének' => [ + 'constituencies' => ['Győr-Moson-Sopron 1.'], + 'coordinates' => ['lat' => 47.7392272, 'lng' => 17.7556608], + ], + 'Veszkény' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5969056, 'lng' => 17.0891913], + ], + 'Veszprémvarsány' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4290248, 'lng' => 17.8287245], + ], + 'Vitnyéd' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5863882, 'lng' => 16.9832151], + ], + 'Völcsej' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.496503, 'lng' => 16.7604595], + ], + 'Zsebeháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.511293, 'lng' => 17.191017], + ], + 'Zsira' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4580482, 'lng' => 16.6766466], + ], + ], + 'Hajdú-Bihar' => [ + 'Álmosd' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.4167788, 'lng' => 21.9806107], + ], + 'Ártánd' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1241958, 'lng' => 21.7568167], + ], + 'Bagamér' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.4498231, 'lng' => 21.9942012], + ], + 'Bakonszeg' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1900613, 'lng' => 21.4442102], + ], + 'Balmazújváros' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.6145296, 'lng' => 21.3417333], + ], + 'Báránd' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.2936964, 'lng' => 21.2288584], + ], + 'Bedő' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1634194, 'lng' => 21.7502785], + ], + 'Berekböszörmény' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0615952, 'lng' => 21.6782301], + ], + 'Berettyóújfalu' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2196438, 'lng' => 21.5362812], + ], + 'Bihardancsháza' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2291246, 'lng' => 21.3159659], + ], + 'Biharkeresztes' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1301236, 'lng' => 21.7219423], + ], + 'Biharnagybajom' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2108104, 'lng' => 21.2302309], + ], + 'Bihartorda' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.215994, 'lng' => 21.3526252], + ], + 'Bocskaikert' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6435949, 'lng' => 21.659878], + ], + 'Bojt' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1927968, 'lng' => 21.7327485], + ], + 'Csökmő' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0315111, 'lng' => 21.2892817], + ], + 'Darvas' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1017037, 'lng' => 21.3374554], + ], + 'Debrecen' => [ + 'constituencies' => ['Hajdú-Bihar 3.', 'Hajdú-Bihar 1.', 'Hajdú-Bihar 2.'], + 'coordinates' => ['lat' => 47.5316049, 'lng' => 21.6273124], + ], + 'Derecske' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3533886, 'lng' => 21.5658524], + ], + 'Ebes' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.4709086, 'lng' => 21.490457], + ], + 'Egyek' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.6258313, 'lng' => 20.8907463], + ], + 'Esztár' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2837051, 'lng' => 21.7744117], + ], + 'Földes' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.2896801, 'lng' => 21.3633025], + ], + 'Folyás' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.8086696, 'lng' => 21.1371809], + ], + 'Fülöp' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.5981409, 'lng' => 22.0546557], + ], + 'Furta' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1300357, 'lng' => 21.460144], + ], + 'Gáborján' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2360716, 'lng' => 21.6622765], + ], + 'Görbeháza' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.8200025, 'lng' => 21.2359976], + ], + 'Hajdúbagos' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3947066, 'lng' => 21.6643329], + ], + 'Hajdúböszörmény' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.6718908, 'lng' => 21.5126637], + ], + 'Hajdúdorog' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.8166047, 'lng' => 21.4980694], + ], + 'Hajdúhadház' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6802292, 'lng' => 21.6675179], + ], + 'Hajdúnánás' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.843004, 'lng' => 21.4242691], + ], + 'Hajdúsámson' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6049148, 'lng' => 21.7597325], + ], + 'Hajdúszoboszló' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.4435369, 'lng' => 21.3965516], + ], + 'Hajdúszovát' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.3903463, 'lng' => 21.4764161], + ], + 'Hencida' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2507004, 'lng' => 21.6989732], + ], + 'Hortobágy' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.5868751, 'lng' => 21.1560332], + ], + 'Hosszúpályi' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3947673, 'lng' => 21.7346539], + ], + 'Kaba' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.3565391, 'lng' => 21.2726765], + ], + 'Kismarja' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2463277, 'lng' => 21.8214627], + ], + 'Kokad' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.4054409, 'lng' => 21.9336174], + ], + 'Komádi' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0055271, 'lng' => 21.4944772], + ], + 'Konyár' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3213954, 'lng' => 21.6691634], + ], + 'Körösszakál' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0178012, 'lng' => 21.5932398], + ], + 'Körösszegapáti' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0396539, 'lng' => 21.6317831], + ], + 'Létavértes' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.3835171, 'lng' => 21.8798767], + ], + 'Magyarhomorog' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0222187, 'lng' => 21.5480518], + ], + 'Mezőpeterd' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.165025, 'lng' => 21.6200633], + ], + 'Mezősas' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1104156, 'lng' => 21.5671344], + ], + 'Mikepércs' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.4406335, 'lng' => 21.6366773], + ], + 'Monostorpályi' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3984198, 'lng' => 21.7764527], + ], + 'Nádudvar' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.4259381, 'lng' => 21.1616779], + ], + 'Nagyhegyes' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.539228, 'lng' => 21.345552], + ], + 'Nagykereki' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1863168, 'lng' => 21.7922805], + ], + 'Nagyrábé' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2043078, 'lng' => 21.3306582], + ], + 'Nyírábrány' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.541423, 'lng' => 22.0128317], + ], + 'Nyíracsád' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6039774, 'lng' => 21.9715154], + ], + 'Nyíradony' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6899404, 'lng' => 21.9085991], + ], + 'Nyírmártonfalva' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.5862503, 'lng' => 21.8964914], + ], + 'Pocsaj' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2851817, 'lng' => 21.8122198], + ], + 'Polgár' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.8679381, 'lng' => 21.1141038], + ], + 'Püspökladány' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.3216529, 'lng' => 21.1185953], + ], + 'Sáp' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2549739, 'lng' => 21.3555868], + ], + 'Sáránd' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.4062312, 'lng' => 21.6290631], + ], + 'Sárrétudvari' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.2406806, 'lng' => 21.1866058], + ], + 'Szentpéterszeg' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2386719, 'lng' => 21.6178971], + ], + 'Szerep' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.2278774, 'lng' => 21.1407795], + ], + 'Téglás' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.7109686, 'lng' => 21.6727776], + ], + 'Tépe' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.32046, 'lng' => 21.5714076], + ], + 'Tetétlen' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.3148595, 'lng' => 21.3069162], + ], + 'Tiszacsege' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.6997085, 'lng' => 20.9917041], + ], + 'Tiszagyulaháza' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.942524, 'lng' => 21.1428152], + ], + 'Told' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1180165, 'lng' => 21.6413048], + ], + 'Újiráz' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 46.9870862, 'lng' => 21.3556353], + ], + 'Újléta' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.4650261, 'lng' => 21.8733489], + ], + 'Újszentmargita' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.7266767, 'lng' => 21.1047788], + ], + 'Újtikos' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.9176202, 'lng' => 21.171571], + ], + 'Vámospércs' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.525345, 'lng' => 21.8992474], + ], + 'Váncsod' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2011182, 'lng' => 21.6400459], + ], + 'Vekerd' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0959975, 'lng' => 21.4017741], + ], + 'Zsáka' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1340418, 'lng' => 21.4307824], + ], + ], + 'Heves' => [ + 'Abasár' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7989023, 'lng' => 20.0036779], + ], + 'Adács' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6922284, 'lng' => 19.9779484], + ], + 'Aldebrő' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7891428, 'lng' => 20.2302555], + ], + 'Andornaktálya' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8499325, 'lng' => 20.4105243], + ], + 'Apc' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7933298, 'lng' => 19.6955737], + ], + 'Átány' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.6156875, 'lng' => 20.3620368], + ], + 'Atkár' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7209651, 'lng' => 19.8912361], + ], + 'Balaton' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 46.8302679, 'lng' => 17.7340438], + ], + 'Bátor' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.99076, 'lng' => 20.2627351], + ], + 'Bekölce' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0804457, 'lng' => 20.268156], + ], + 'Bélapátfalva' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0578657, 'lng' => 20.3500536], + ], + 'Besenyőtelek' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.6994693, 'lng' => 20.4300342], + ], + 'Boconád' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6414895, 'lng' => 20.1877312], + ], + 'Bodony' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.9420912, 'lng' => 20.0199927], + ], + 'Boldog' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6031287, 'lng' => 19.687521], + ], + 'Bükkszék' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.9915393, 'lng' => 20.1765126], + ], + 'Bükkszenterzsébet' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0532811, 'lng' => 20.1622924], + ], + 'Bükkszentmárton' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0715382, 'lng' => 20.3310312], + ], + 'Csány' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6474142, 'lng' => 19.8259607], + ], + 'Demjén' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8317294, 'lng' => 20.3313872], + ], + 'Detk' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7489442, 'lng' => 20.0983332], + ], + 'Domoszló' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8288666, 'lng' => 20.1172988], + ], + 'Dormánd' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.7203119, 'lng' => 20.4174779], + ], + 'Ecséd' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7307237, 'lng' => 19.7684767], + ], + 'Eger' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.9025348, 'lng' => 20.3772284], + ], + 'Egerbakta' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.9341404, 'lng' => 20.2918134], + ], + 'Egerbocs' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0263467, 'lng' => 20.2598999], + ], + 'Egercsehi' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0545478, 'lng' => 20.261522], + ], + 'Egerfarmos' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.7177802, 'lng' => 20.5358914], + ], + 'Egerszalók' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8702275, 'lng' => 20.3241673], + ], + 'Egerszólát' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8902473, 'lng' => 20.2669774], + ], + 'Erdőkövesd' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0391241, 'lng' => 20.1013656], + ], + 'Erdőtelek' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6852656, 'lng' => 20.3115369], + ], + 'Erk' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6101796, 'lng' => 20.076668], + ], + 'Fedémes' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0320282, 'lng' => 20.1878653], + ], + 'Feldebrő' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8128253, 'lng' => 20.2363322], + ], + 'Felsőtárkány' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.9734513, 'lng' => 20.41906], + ], + 'Füzesabony' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.7495339, 'lng' => 20.4150668], + ], + 'Gyöngyös' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7772651, 'lng' => 19.9294927], + ], + 'Gyöngyöshalász' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7413068, 'lng' => 19.9227242], + ], + 'Gyöngyösoroszi' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8263987, 'lng' => 19.8928817], + ], + 'Gyöngyöspata' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8140904, 'lng' => 19.7923335], + ], + 'Gyöngyössolymos' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8160489, 'lng' => 19.9338831], + ], + 'Gyöngyöstarján' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8132903, 'lng' => 19.8664265], + ], + 'Halmajugra' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7634173, 'lng' => 20.0523104], + ], + 'Hatvan' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6656965, 'lng' => 19.676666], + ], + 'Heréd' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7081485, 'lng' => 19.6327042], + ], + 'Heves' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.5971694, 'lng' => 20.280156], + ], + 'Hevesaranyos' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0109153, 'lng' => 20.2342809], + ], + 'Hevesvezekény' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.5570546, 'lng' => 20.3580453], + ], + 'Hort' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6890439, 'lng' => 19.7842632], + ], + 'Istenmezeje' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0845673, 'lng' => 20.0515347], + ], + 'Ivád' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0203013, 'lng' => 20.0612654], + ], + 'Kál' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7318239, 'lng' => 20.2608866], + ], + 'Kápolna' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7584202, 'lng' => 20.2459749], + ], + 'Karácsond' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7282318, 'lng' => 20.0282488], + ], + 'Kerecsend' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.7947277, 'lng' => 20.3444695], + ], + 'Kerekharaszt' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6623104, 'lng' => 19.6253721], + ], + 'Kisfüzes' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.9881653, 'lng' => 20.1267373], + ], + 'Kisköre' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.4984608, 'lng' => 20.4973609], + ], + 'Kisnána' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8506469, 'lng' => 20.1457821], + ], + 'Kömlő' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 46.1929788, 'lng' => 18.2512139], + ], + 'Kompolt' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7415463, 'lng' => 20.2406377], + ], + 'Lőrinci' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7390261, 'lng' => 19.6756557], + ], + 'Ludas' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7300788, 'lng' => 20.0910629], + ], + 'Maklár' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8054074, 'lng' => 20.410901], + ], + 'Markaz' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8222206, 'lng' => 20.0582311], + ], + 'Mátraballa' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.9843833, 'lng' => 20.0225017], + ], + + ], + ]; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-82.php b/tests/PHPStan/Analyser/data/bug-82.php new file mode 100644 index 0000000000..754d2244d8 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-82.php @@ -0,0 +1,23 @@ + 10001, + 2 => 10002, + 3 => 10003, + 4 => 10004, + 5 => 10005, + 6 => 10006, + 7 => 10007, + 8 => 10008, + 9 => 10009, + 10 => 10010, + 11 => 10011, + 12 => 10012, + 13 => 10013, + 14 => 10014, + 15 => 10015, + 16 => 10016, + 17 => 10017, + 18 => 10018, + 19 => 10019, + 20 => 10020, + 21 => 10021, + 22 => 10022, + 23 => 10023, + 24 => 10024, + 25 => 10025, + 26 => 10026, + 27 => 10027, + 28 => 10028, + 29 => 10029, + 30 => 10030, + 31 => 10031, + 32 => 10032, + 33 => 10033, + 34 => 10034, + 35 => 10035, + 36 => 10036, + 37 => 10037, + 38 => 10038, + 39 => 10039, + 40 => 10040, + 41 => 10041, + 42 => 10042, + 43 => 10043, + 44 => 10044, + 45 => 10045, + 46 => 10046, + 47 => 10047, + 48 => 10048, + 49 => 10049, + 50 => 10050, + 51 => 10051, + 52 => 10052, + 53 => 10053, + 54 => 10054, + 55 => 10055, + 56 => 10056, + 57 => 10057, + 58 => 10058, + 59 => 10059, + 60 => 10060, + 61 => 10061, + 62 => 10062, + 63 => 10063, + 64 => 10064, + 65 => 10065, + 66 => 10066, + 67 => 10067, + 68 => 10068, + 69 => 10069, + 70 => 10070, + 71 => 10071, + 72 => 10072, + 73 => 10073, + 74 => 10074, + 75 => 10075, + 76 => 10076, + 77 => 10077, + 78 => 10078, + 79 => 10079, + 80 => 10080, + 81 => 10081, + 82 => 10082, + 83 => 10083, + 84 => 10084, + 85 => 10085, + 86 => 10086, + 87 => 10087, + 88 => 10088, + 89 => 10089, + 90 => 10090, + 91 => 10091, + 92 => 10092, + 93 => 10093, + 94 => 10094, + 95 => 10095, + 96 => 10096, + 97 => 10097, + 98 => 10098, + 99 => 10099, + 100 => 10100, + 101 => 10101, + 102 => 10102, + 103 => 10103, + 104 => 10104, + 105 => 10105, + 106 => 10106, + 107 => 10107, + 108 => 10108, + 109 => 10109, + 110 => 10110, + 111 => 10111, + 112 => 10112, + 113 => 10113, + 114 => 10114, + 115 => 10115, + 116 => 10116, + 117 => 10117, + 118 => 10118, + 119 => 10119, + 120 => 10120, + 121 => 10121, + 122 => 10122, + 123 => 10123, + 124 => 10124, + 125 => 10125, + 126 => 10126, + 127 => 10127, + 128 => 10128, + 129 => 10129, + 130 => 10130, + 131 => 10131, + 132 => 10132, + 133 => 10133, + 134 => 10134, + 135 => 10135, + 136 => 10136, + 137 => 10137, + 138 => 10138, + 139 => 10139, + 140 => 10140, + 141 => 10141, + 142 => 10142, + 143 => 10143, + 144 => 10144, + 145 => 10145, + 146 => 10146, + 147 => 10147, + 148 => 10148, + 149 => 10149, + 150 => 10150, + 151 => 10151, + 152 => 10152, + 153 => 10153, + 154 => 10154, + 155 => 10155, + 156 => 10156, + 157 => 10157, + 158 => 10158, + 159 => 10159, + 160 => 10160, + 161 => 10161, + 162 => 10162, + 163 => 10163, + 164 => 10164, + 165 => 10165, + 166 => 10166, + 167 => 10167, + 168 => 10168, + 169 => 10169, + 170 => 10170, + 171 => 10171, + 172 => 10172, + 173 => 10173, + 174 => 10174, + 175 => 10175, + 176 => 10176, + 177 => 10177, + 178 => 10178, + 179 => 10179, + 180 => 10180, + 181 => 10181, + 182 => 10182, + 183 => 10183, + 184 => 10184, + 185 => 10185, + 186 => 10186, + 187 => 10187, + 188 => 10188, + 189 => 10189, + 190 => 10190, + 191 => 10191, + 192 => 10192, + 193 => 10193, + 194 => 10194, + 195 => 10195, + 196 => 10196, + 197 => 10197, + 198 => 10198, + 199 => 10199, + 200 => 10200, + 201 => 10201, + 202 => 10202, + 203 => 10203, + 204 => 10204, + 205 => 10205, + 206 => 10206, + 207 => 10207, + 208 => 10208, + 209 => 10209, + 210 => 10210, + 211 => 10211, + 212 => 10212, + 213 => 10213, + 214 => 10214, + 215 => 10215, + 216 => 10216, + 217 => 10217, + 218 => 10218, + 219 => 10219, + 220 => 10220, + 221 => 10221, + 222 => 10222, + 223 => 10223, + 224 => 10224, + 225 => 10225, + 226 => 10226, + 227 => 10227, + 228 => 10228, + 229 => 10229, + 230 => 10230, + 231 => 10231, + 232 => 10232, + 233 => 10233, + 234 => 10234, + 235 => 10235, + 236 => 10236, + 237 => 10237, + 238 => 10238, + 239 => 10239, + 240 => 10240, + 241 => 10241, + 242 => 10242, + 243 => 10243, + 244 => 10244, + 245 => 10245, + 246 => 10246, + 247 => 10247, + 248 => 10248, + 249 => 10249, + 250 => 10250, + 251 => 10251, + 252 => 10252, + 253 => 10253, + 254 => 10254, + 255 => 10255, + 256 => 10256, + 257 => 10257, + 258 => 10258, + 259 => 10259, + 260 => 10260, + 261 => 10261, + 262 => 10262, + 263 => 10263, + 264 => 10264, + 265 => 10265, + 266 => 10266, + 267 => 10267, + 268 => 10268, + 269 => 10269, + 270 => 10270, + 271 => 10271, + 272 => 10272, + 273 => 10273, + 274 => 10274, + 275 => 10275, + 276 => 10276, + 277 => 10277, + 278 => 10278, + 279 => 10279, + 280 => 10280, + 281 => 10281, + 282 => 10282, + 283 => 10283, + 284 => 10284, + 285 => 10285, + 286 => 10286, + 287 => 10287, + 288 => 10288, + 289 => 10289, + 290 => 10290, + 291 => 10291, + 292 => 10292, + 293 => 10293, + 294 => 10294, + 295 => 10295, + 296 => 10296, + 297 => 10297, + 298 => 10298, + 299 => 10299, + 300 => 10300, + 301 => 10301, + 302 => 10302, + 303 => 10303, + 304 => 10304, + 305 => 10305, + 306 => 10306, + 307 => 10307, + 308 => 10308, + 309 => 10309, + 310 => 10310, + 311 => 10311, + 312 => 10312, + 313 => 10313, + 314 => 10314, + 315 => 10315, + 316 => 10316, + 317 => 10317, + 318 => 10318, + 319 => 10319, + 320 => 10320, + 321 => 10321, + 322 => 10322, + 323 => 10323, + 324 => 10324, + 325 => 10325, + 326 => 10326, + 327 => 10327, + 328 => 10328, + 329 => 10329, + 330 => 10330, + 331 => 10331, + 332 => 10332, + 333 => 10333, + 334 => 10334, + 335 => 10335, + 336 => 10336, + 337 => 10337, + 338 => 10338, + 339 => 10339, + 340 => 10340, + 341 => 10341, + 342 => 10342, + 343 => 10343, + 344 => 10344, + 345 => 10345, + 346 => 10346, + 347 => 10347, + 348 => 10348, + 349 => 10349, + 350 => 10350, + 351 => 10351, + 352 => 10352, + 353 => 10353, + 354 => 10354, + 355 => 10355, + 356 => 10356, + 357 => 10357, + 358 => 10358, + 359 => 10359, + 360 => 10360, + 361 => 10361, + 362 => 10362, + 363 => 10363, + 364 => 10364, + 365 => 10365, + 366 => 10366, + 367 => 10367, + 368 => 10368, + 369 => 10369, + 370 => 10370, + 371 => 10371, + 372 => 10372, + 373 => 10373, + 374 => 10374, + 375 => 10375, + 376 => 10376, + 377 => 10377, + 378 => 10378, + 379 => 10379, + 380 => 10380, + 381 => 10381, + 382 => 10382, + 383 => 10383, + 384 => 10384, + 385 => 10385, + 386 => 10386, + 387 => 10387, + 388 => 10388, + 389 => 10389, + 390 => 10390, + 391 => 10391, + 392 => 10392, + 393 => 10393, + 394 => 10394, + 395 => 10395, + 396 => 10396, + 397 => 10397, + 398 => 10398, + 399 => 10399, + 400 => 10400, + 401 => 10401, + 402 => 10402, + 403 => 10403, + 404 => 10404, + 405 => 10405, + 406 => 10406, + 407 => 10407, + 408 => 10408, + 409 => 10409, + 410 => 10410, + 411 => 10411, + 412 => 10412, + 413 => 10413, + 414 => 10414, + 415 => 10415, + 416 => 10416, + 417 => 10417, + 418 => 10418, + 419 => 10419, + 420 => 10420, + 421 => 10421, + 422 => 10422, + 423 => 10423, + 424 => 10424, + 425 => 10425, + 426 => 10426, + 427 => 10427, + 428 => 10428, + 429 => 10429, + 430 => 10430, + 431 => 10431, + 432 => 10432, + 433 => 10433, + 434 => 10434, + 435 => 10435, + 436 => 10436, + 437 => 10437, + 438 => 10438, + 439 => 10439, + 440 => 10440, + 441 => 10441, + 442 => 10442, + 443 => 10443, + 444 => 10444, + 445 => 10445, + 446 => 10446, + 447 => 10447, + 448 => 10448, + 449 => 10449, + 450 => 10450, + 451 => 10451, + 452 => 10452, + 453 => 10453, + 454 => 10454, + 455 => 10455, + 456 => 10456, + 457 => 10457, + 458 => 10458, + 459 => 10459, + 460 => 10460, + 461 => 10461, + 462 => 10462, + 463 => 10463, + 464 => 10464, + 465 => 10465, + 466 => 10466, + 467 => 10467, + 468 => 10468, + 469 => 10469, + 470 => 10470, + 471 => 10471, + 472 => 10472, + 473 => 10473, + 474 => 10474, + 475 => 10475, + 476 => 10476, + 477 => 10477, + 478 => 10478, + 479 => 10479, + 480 => 10480, + 481 => 10481, + 482 => 10482, + 483 => 10483, + 484 => 10484, + 485 => 10485, + 486 => 10486, + 487 => 10487, + 488 => 10488, + 489 => 10489, + 490 => 10490, + 491 => 10491, + 492 => 10492, + 493 => 10493, + 494 => 10494, + 495 => 10495, + 496 => 10496, + 497 => 10497, + 498 => 10498, + 499 => 10499, + 500 => 10500, + 501 => 10501, + 502 => 10502, + 503 => 10503, + 504 => 10504, + 505 => 10505, + 506 => 10506, + 507 => 10507, + 508 => 10508, + 509 => 10509, + 510 => 10510, + 511 => 10511, + 512 => 10512, + 513 => 10513, + 514 => 10514, + 515 => 10515, + 516 => 10516, + 517 => 10517, + 518 => 10518, + 519 => 10519, + 520 => 10520, + 521 => 10521, + 522 => 10522, + 523 => 10523, + 524 => 10524, + 525 => 10525, + 526 => 10526, + 527 => 10527, + 528 => 10528, + 529 => 10529, + 530 => 10530, + 531 => 10531, + 532 => 10532, + 533 => 10533, + 534 => 10534, + 535 => 10535, + 536 => 10536, + 537 => 10537, + 538 => 10538, + 539 => 10539, + 540 => 10540, + 541 => 10541, + 542 => 10542, + 543 => 10543, + 544 => 10544, + 545 => 10545, + 546 => 10546, + 547 => 10547, + 548 => 10548, + 549 => 10549, + 550 => 10550, + 551 => 10551, + 552 => 10552, + 553 => 10553, + 554 => 10554, + 555 => 10555, + 556 => 10556, + 557 => 10557, + 558 => 10558, + 559 => 10559, + 560 => 10560, + 561 => 10561, + 562 => 10562, + 563 => 10563, + 564 => 10564, + 565 => 10565, + 566 => 10566, + 567 => 10567, + 568 => 10568, + 569 => 10569, + 570 => 10570, + 571 => 10571, + 572 => 10572, + 573 => 10573, + 574 => 10574, + 575 => 10575, + 576 => 10576, + 577 => 10577, + 578 => 10578, + 579 => 10579, + 580 => 10580, + 581 => 10581, + 582 => 10582, + 583 => 10583, + 584 => 10584, + 585 => 10585, + 586 => 10586, + 587 => 10587, + 588 => 10588, + 589 => 10589, + 590 => 10590, + 591 => 10591, + 592 => 10592, + 593 => 10593, + 594 => 10594, + 595 => 10595, + 596 => 10596, + 597 => 10597, + 598 => 10598, + 599 => 10599, + 600 => 10600, + 601 => 10601, + 602 => 10602, + 603 => 10603, + 604 => 10604, + 605 => 10605, + 606 => 10606, + 607 => 10607, + 608 => 10608, + 609 => 10609, + 610 => 10610, + 611 => 10611, + 612 => 10612, + 613 => 10613, + 614 => 10614, + 615 => 10615, + 616 => 10616, + 617 => 10617, + 618 => 10618, + 619 => 10619, + 620 => 10620, + 621 => 10621, + 622 => 10622, + 623 => 10623, + 624 => 10624, + 625 => 10625, + 626 => 10626, + 627 => 10627, + 628 => 10628, + 629 => 10629, + 630 => 10630, + 631 => 10631, + 632 => 10632, + 633 => 10633, + 634 => 10634, + 635 => 10635, + 636 => 10636, + 637 => 10637, + 638 => 10638, + 639 => 10639, + 640 => 10640, + 641 => 10641, + 642 => 10642, + 643 => 10643, + 644 => 10644, + 645 => 10645, + 646 => 10646, + 647 => 10647, + 648 => 10648, + 649 => 10649, + 650 => 10650, + 651 => 10651, + 652 => 10652, + 653 => 10653, + 654 => 10654, + 655 => 10655, + 656 => 10656, + 657 => 10657, + 658 => 10658, + 659 => 10659, + 660 => 10660, + 661 => 10661, + 662 => 10662, + 663 => 10663, + 664 => 10664, + 665 => 10665, + 666 => 10666, + 667 => 10667, + 668 => 10668, + 669 => 10669, + 670 => 10670, + 671 => 10671, + 672 => 10672, + 673 => 10673, + 674 => 10674, + 675 => 10675, + 676 => 10676, + 677 => 10677, + 678 => 10678, + 679 => 10679, + 680 => 10680, + 681 => 10681, + 682 => 10682, + 683 => 10683, + 684 => 10684, + 685 => 10685, + 686 => 10686, + 687 => 10687, + 688 => 10688, + 689 => 10689, + 690 => 10690, + 691 => 10691, + 692 => 10692, + 693 => 10693, + 694 => 10694, + 695 => 10695, + 696 => 10696, + 697 => 10697, + 698 => 10698, + 699 => 10699, + 700 => 10700, + 701 => 10701, + 702 => 10702, + 703 => 10703, + 704 => 10704, + 705 => 10705, + 706 => 10706, + 707 => 10707, + 708 => 10708, + 709 => 10709, + 710 => 10710, + 711 => 10711, + 712 => 10712, + 713 => 10713, + 714 => 10714, + 715 => 10715, + 716 => 10716, + 717 => 10717, + 718 => 10718, + 719 => 10719, + 720 => 10720, + 721 => 10721, + 722 => 10722, + 723 => 10723, + 724 => 10724, + 725 => 10725, + 726 => 10726, + 727 => 10727, + 728 => 10728, + 729 => 10729, + 730 => 10730, + 731 => 10731, + 732 => 10732, + 733 => 10733, + 734 => 10734, + 735 => 10735, + 736 => 10736, + 737 => 10737, + 738 => 10738, + 739 => 10739, + 740 => 10740, + 741 => 10741, + 742 => 10742, + 743 => 10743, + 744 => 10744, + 745 => 10745, + 746 => 10746, + 747 => 10747, + 748 => 10748, + 749 => 10749, + 750 => 10750, + 751 => 10751, + 752 => 10752, + 753 => 10753, + 754 => 10754, + 755 => 10755, + 756 => 10756, + 757 => 10757, + 758 => 10758, + 759 => 10759, + 760 => 10760, + 761 => 10761, + 762 => 10762, + 763 => 10763, + 764 => 10764, + 765 => 10765, + 766 => 10766, + 767 => 10767, + 768 => 10768, + 769 => 10769, + 770 => 10770, + 771 => 10771, + 772 => 10772, + 773 => 10773, + 774 => 10774, + 775 => 10775, + 776 => 10776, + 777 => 10777, + 778 => 10778, + 779 => 10779, + 780 => 10780, + 781 => 10781, + 782 => 10782, + 783 => 10783, + 784 => 10784, + 785 => 10785, + 786 => 10786, + 787 => 10787, + 788 => 10788, + 789 => 10789, + 790 => 10790, + 791 => 10791, + 792 => 10792, + 793 => 10793, + 794 => 10794, + 795 => 10795, + 796 => 10796, + 797 => 10797, + 798 => 10798, + 799 => 10799, + 800 => 10800, + 801 => 10801, + 802 => 10802, + 803 => 10803, + 804 => 10804, + 805 => 10805, + 806 => 10806, + 807 => 10807, + 808 => 10808, + 809 => 10809, + 810 => 10810, + 811 => 10811, + 812 => 10812, + 813 => 10813, + 814 => 10814, + 815 => 10815, + 816 => 10816, + 817 => 10817, + 818 => 10818, + 819 => 10819, + 820 => 10820, + 821 => 10821, + 822 => 10822, + 823 => 10823, + 824 => 10824, + 825 => 10825, + 826 => 10826, + 827 => 10827, + 828 => 10828, + 829 => 10829, + 830 => 10830, + 831 => 10831, + 832 => 10832, + 833 => 10833, + 834 => 10834, + 835 => 10835, + 836 => 10836, + 837 => 10837, + 838 => 10838, + 839 => 10839, + 840 => 10840, + 841 => 10841, + 842 => 10842, + 843 => 10843, + 844 => 10844, + 845 => 10845, + 846 => 10846, + 847 => 10847, + 848 => 10848, + 849 => 10849, + 850 => 10850, + 851 => 10851, + 852 => 10852, + 853 => 10853, + 854 => 10854, + 855 => 10855, + 856 => 10856, + 857 => 10857, + 858 => 10858, + 859 => 10859, + 860 => 10860, + 861 => 10861, + 862 => 10862, + 863 => 10863, + 864 => 10864, + 865 => 10865, + 866 => 10866, + 867 => 10867, + 868 => 10868, + 869 => 10869, + 870 => 10870, + 871 => 10871, + 872 => 10872, + 873 => 10873, + 874 => 10874, + 875 => 10875, + 876 => 10876, + 877 => 10877, + 878 => 10878, + 879 => 10879, + 880 => 10880, + 881 => 10881, + 882 => 10882, + 883 => 10883, + 884 => 10884, + 885 => 10885, + 886 => 10886, + 887 => 10887, + 888 => 10888, + 889 => 10889, + 890 => 10890, + 891 => 10891, + 892 => 10892, + 893 => 10893, + 894 => 10894, + 895 => 10895, + 896 => 10896, + 897 => 10897, + 898 => 10898, + 899 => 10899, + 900 => 10900, + 901 => 10901, + 902 => 10902, + 903 => 10903, + 904 => 10904, + 905 => 10905, + 906 => 10906, + 907 => 10907, + 908 => 10908, + 909 => 10909, + 910 => 10910, + 911 => 10911, + 912 => 10912, + 913 => 10913, + 914 => 10914, + 915 => 10915, + 916 => 10916, + 917 => 10917, + 918 => 10918, + 919 => 10919, + 920 => 10920, + 921 => 10921, + 922 => 10922, + 923 => 10923, + 924 => 10924, + 925 => 10925, + 926 => 10926, + 927 => 10927, + 928 => 10928, + 929 => 10929, + 930 => 10930, + 931 => 10931, + 932 => 10932, + 933 => 10933, + 934 => 10934, + 935 => 10935, + 936 => 10936, + 937 => 10937, + 938 => 10938, + 939 => 10939, + 940 => 10940, + 941 => 10941, + 942 => 10942, + 943 => 10943, + 944 => 10944, + 945 => 10945, + 946 => 10946, + 947 => 10947, + 948 => 10948, + 949 => 10949, + 950 => 10950, + 951 => 10951, + 952 => 10952, + 953 => 10953, + 954 => 10954, + 955 => 10955, + 956 => 10956, + 957 => 10957, + 958 => 10958, + 959 => 10959, + 960 => 10960, + 961 => 10961, + 962 => 10962, + 963 => 10963, + 964 => 10964, + 965 => 10965, + 966 => 10966, + 967 => 10967, + 968 => 10968, + 969 => 10969, + 970 => 10970, + 971 => 10971, + 972 => 10972, + 973 => 10973, + 974 => 10974, + 975 => 10975, + 976 => 10976, + 977 => 10977, + 978 => 10978, + 979 => 10979, + 980 => 10980, + 981 => 10981, + 982 => 10982, + 983 => 10983, + 984 => 10984, + 985 => 10985, + 986 => 10986, + 987 => 10987, + 988 => 10988, + 989 => 10989, + 990 => 10990, + 991 => 10991, + 992 => 10992, + 993 => 10993, + 994 => 10994, + 995 => 10995, + 996 => 10996, + 997 => 10997, + 998 => 10998, + 999 => 10999, + 1000 => 11000, + 1001 => 11001, + 1002 => 11002, + 1003 => 11003, + 1004 => 11004, + 1005 => 11005, + 1006 => 11006, + 1007 => 11007, + 1008 => 11008, + 1009 => 11009, + 1010 => 11010, + 1011 => 11011, + 1012 => 11012, + 1013 => 11013, + 1014 => 11014, + 1015 => 11015, + 1016 => 11016, + 1017 => 11017, + 1018 => 11018, + 1019 => 11019, + 1020 => 11020, + 1021 => 11021, + 1022 => 11022, + 1023 => 11023, + 1024 => 11024, + 1025 => 11025, + 1026 => 11026, + 1027 => 11027, + 1028 => 11028, + 1029 => 11029, + 1030 => 11030, + 1031 => 11031, + 1032 => 11032, + 1033 => 11033, + 1034 => 11034, + 1035 => 11035, + 1036 => 11036, + 1037 => 11037, + 1038 => 11038, + 1039 => 11039, + 1040 => 11040, + 1041 => 11041, + 1042 => 11042, + 1043 => 11043, + 1044 => 11044, + 1045 => 11045, + 1046 => 11046, + 1047 => 11047, + 1048 => 11048, + 1049 => 11049, + 1050 => 11050, + 1051 => 11051, + 1052 => 11052, + 1053 => 11053, + 1054 => 11054, + 1055 => 11055, + 1056 => 11056, + 1057 => 11057, + 1058 => 11058, + 1059 => 11059, + 1060 => 11060, + 1061 => 11061, + 1062 => 11062, + 1063 => 11063, + 1064 => 11064, + 1065 => 11065, + 1066 => 11066, + 1067 => 11067, + 1068 => 11068, + 1069 => 11069, + 1070 => 11070, + 1071 => 11071, + 1072 => 11072, + 1073 => 11073, + 1074 => 11074, + 1075 => 11075, + 1076 => 11076, + 1077 => 11077, + 1078 => 11078, + 1079 => 11079, + 1080 => 11080, + 1081 => 11081, + 1082 => 11082, + 1083 => 11083, + 1084 => 11084, + 1085 => 11085, + 1086 => 11086, + 1087 => 11087, + 1088 => 11088, + 1089 => 11089, + 1090 => 11090, + 1091 => 11091, + 1092 => 11092, + 1093 => 11093, + 1094 => 11094, + 1095 => 11095, + 1096 => 11096, + 1097 => 11097, + 1098 => 11098, + 1099 => 11099, + 1100 => 11100, + 1101 => 11101, + 1102 => 11102, + 1103 => 11103, + 1104 => 11104, + 1105 => 11105, + 1106 => 11106, + 1107 => 11107, + 1108 => 11108, + 1109 => 11109, + 1110 => 11110, + 1111 => 11111, + 1112 => 11112, + 1113 => 11113, + 1114 => 11114, + 1115 => 11115, + 1116 => 11116, + 1117 => 11117, + 1118 => 11118, + 1119 => 11119, + 1120 => 11120, + 1121 => 11121, + 1122 => 11122, + 1123 => 11123, + 1124 => 11124, + 1125 => 11125, + 1126 => 11126, + 1127 => 11127, + 1128 => 11128, + 1129 => 11129, + 1130 => 11130, + 1131 => 11131, + 1132 => 11132, + 1133 => 11133, + 1134 => 11134, + 1135 => 11135, + 1136 => 11136, + 1137 => 11137, + 1138 => 11138, + 1139 => 11139, + 1140 => 11140, + 1141 => 11141, + 1142 => 11142, + 1143 => 11143, + 1144 => 11144, + 1145 => 11145, + 1146 => 11146, + 1147 => 11147, + 1148 => 11148, + 1149 => 11149, + 1150 => 11150, + 1151 => 11151, + 1152 => 11152, + 1153 => 11153, + 1154 => 11154, + 1155 => 11155, + 1156 => 11156, + 1157 => 11157, + 1158 => 11158, + 1159 => 11159, + 1160 => 11160, + 1161 => 11161, + 1162 => 11162, + 1163 => 11163, + 1164 => 11164, + 1165 => 11165, + 1166 => 11166, + 1167 => 11167, + 1168 => 11168, + 1169 => 11169, + 1170 => 11170, + 1171 => 11171, + 1172 => 11172, + 1173 => 11173, + 1174 => 11174, + 1175 => 11175, + 1176 => 11176, + 1177 => 11177, + 1178 => 11178, + 1179 => 11179, + 1180 => 11180, + 1181 => 11181, + 1182 => 11182, + 1183 => 11183, + 1184 => 11184, + 1185 => 11185, + 1186 => 11186, + 1187 => 11187, + 1188 => 11188, + 1189 => 11189, + 1190 => 11190, + 1191 => 11191, + 1192 => 11192, + 1193 => 11193, + 1194 => 11194, + 1195 => 11195, + 1196 => 11196, + 1197 => 11197, + 1198 => 11198, + 1199 => 11199, + 1200 => 11200, + 1201 => 11201, + 1202 => 11202, + 1203 => 11203, + 1204 => 11204, + 1205 => 11205, + 1206 => 11206, + 1207 => 11207, + 1208 => 11208, + 1209 => 11209, + 1210 => 11210, + 1211 => 11211, + 1212 => 11212, + 1213 => 11213, + 1214 => 11214, + 1215 => 11215, + 1216 => 11216, + 1217 => 11217, + 1218 => 11218, + 1219 => 11219, + 1220 => 11220, + 1221 => 11221, + 1222 => 11222, + 1223 => 11223, + 1224 => 11224, + 1225 => 11225, + 1226 => 11226, + 1227 => 11227, + 1228 => 11228, + 1229 => 11229, + 1230 => 11230, + 1231 => 11231, + 1232 => 11232, + 1233 => 11233, + 1234 => 11234, + 1235 => 11235, + 1236 => 11236, + 1237 => 11237, + 1238 => 11238, + 1239 => 11239, + 1240 => 11240, + 1241 => 11241, + 1242 => 11242, + 1243 => 11243, + 1244 => 11244, + 1245 => 11245, + 1246 => 11246, + 1247 => 11247, + 1248 => 11248, + 1249 => 11249, + 1250 => 11250, + 1251 => 11251, + 1252 => 11252, + 1253 => 11253, + 1254 => 11254, + 1255 => 11255, + 1256 => 11256, + 1257 => 11257, + 1258 => 11258, + 1259 => 11259, + 1260 => 11260, + 1261 => 11261, + 1262 => 11262, + 1263 => 11263, + 1264 => 11264, + 1265 => 11265, + 1266 => 11266, + 1267 => 11267, + 1268 => 11268, + 1269 => 11269, + 1270 => 11270, + 1271 => 11271, + 1272 => 11272, + 1273 => 11273, + 1274 => 11274, + 1275 => 11275, + 1276 => 11276, + 1277 => 11277, + 1278 => 11278, + 1279 => 11279, + 1280 => 11280, + 1281 => 11281, + 1282 => 11282, + 1283 => 11283, + 1284 => 11284, + 1285 => 11285, + 1286 => 11286, + 1287 => 11287, + 1288 => 11288, + 1289 => 11289, + 1290 => 11290, + 1291 => 11291, + 1292 => 11292, + 1293 => 11293, + 1294 => 11294, + 1295 => 11295, + 1296 => 11296, + 1297 => 11297, + 1298 => 11298, + 1299 => 11299, + 1300 => 11300, + 1301 => 11301, + 1302 => 11302, + 1303 => 11303, + 1304 => 11304, + 1305 => 11305, + 1306 => 11306, + 1307 => 11307, + 1308 => 11308, + 1309 => 11309, + 1310 => 11310, + 1311 => 11311, + 1312 => 11312, + 1313 => 11313, + 1314 => 11314, + 1315 => 11315, + 1316 => 11316, + 1317 => 11317, + 1318 => 11318, + 1319 => 11319, + 1320 => 11320, + 1321 => 11321, + 1322 => 11322, + 1323 => 11323, + 1324 => 11324, + 1325 => 11325, + 1326 => 11326, + 1327 => 11327, + 1328 => 11328, + 1329 => 11329, + 1330 => 11330, + 1331 => 11331, + 1332 => 11332, + 1333 => 11333, + 1334 => 11334, + 1335 => 11335, + 1336 => 11336, + 1337 => 11337, + 1338 => 11338, + 1339 => 11339, + 1340 => 11340, + 1341 => 11341, + 1342 => 11342, + 1343 => 11343, + 1344 => 11344, + 1345 => 11345, + 1346 => 11346, + 1347 => 11347, + 1348 => 11348, + 1349 => 11349, + 1350 => 11350, + 1351 => 11351, + 1352 => 11352, + 1353 => 11353, + 1354 => 11354, + 1355 => 11355, + 1356 => 11356, + 1357 => 11357, + 1358 => 11358, + 1359 => 11359, + 1360 => 11360, + 1361 => 11361, + 1362 => 11362, + 1363 => 11363, + 1364 => 11364, + 1365 => 11365, + 1366 => 11366, + 1367 => 11367, + 1368 => 11368, + 1369 => 11369, + 1370 => 11370, + 1371 => 11371, + 1372 => 11372, + 1373 => 11373, + 1374 => 11374, + 1375 => 11375, + 1376 => 11376, + 1377 => 11377, + 1378 => 11378, + 1379 => 11379, + 1380 => 11380, + 1381 => 11381, + 1382 => 11382, + 1383 => 11383, + 1384 => 11384, + 1385 => 11385, + 1386 => 11386, + 1387 => 11387, + 1388 => 11388, + 1389 => 11389, + 1390 => 11390, + 1391 => 11391, + 1392 => 11392, + 1393 => 11393, + 1394 => 11394, + 1395 => 11395, + 1396 => 11396, + 1397 => 11397, + 1398 => 11398, + 1399 => 11399, + 1400 => 11400, + 1401 => 11401, + 1402 => 11402, + 1403 => 11403, + 1404 => 11404, + 1405 => 11405, + 1406 => 11406, + 1407 => 11407, + 1408 => 11408, + 1409 => 11409, + 1410 => 11410, + 1411 => 11411, + 1412 => 11412, + 1413 => 11413, + 1414 => 11414, + 1415 => 11415, + 1416 => 11416, + 1417 => 11417, + 1418 => 11418, + 1419 => 11419, + 1420 => 11420, + 1421 => 11421, + 1422 => 11422, + 1423 => 11423, + 1424 => 11424, + 1425 => 11425, + 1426 => 11426, + 1427 => 11427, + 1428 => 11428, + 1429 => 11429, + 1430 => 11430, + 1431 => 11431, + 1432 => 11432, + 1433 => 11433, + 1434 => 11434, + 1435 => 11435, + 1436 => 11436, + 1437 => 11437, + 1438 => 11438, + 1439 => 11439, + 1440 => 11440, + 1441 => 11441, + 1442 => 11442, + 1443 => 11443, + 1444 => 11444, + 1445 => 11445, + 1446 => 11446, + 1447 => 11447, + 1448 => 11448, + 1449 => 11449, + 1450 => 11450, + 1451 => 11451, + 1452 => 11452, + 1453 => 11453, + 1454 => 11454, + 1455 => 11455, + 1456 => 11456, + 1457 => 11457, + 1458 => 11458, + 1459 => 11459, + 1460 => 11460, + 1461 => 11461, + 1462 => 11462, + 1463 => 11463, + 1464 => 11464, + 1465 => 11465, + 1466 => 11466, + 1467 => 11467, + 1468 => 11468, + 1469 => 11469, + 1470 => 11470, + 1471 => 11471, + 1472 => 11472, + 1473 => 11473, + 1474 => 11474, + 1475 => 11475, + 1476 => 11476, + 1477 => 11477, + 1478 => 11478, + 1479 => 11479, + 1480 => 11480, + 1481 => 11481, + 1482 => 11482, + 1483 => 11483, + 1484 => 11484, + 1485 => 11485, + 1486 => 11486, + 1487 => 11487, + 1488 => 11488, + 1489 => 11489, + 1490 => 11490, + 1491 => 11491, + 1492 => 11492, + 1493 => 11493, + 1494 => 11494, + 1495 => 11495, + 1496 => 11496, + 1497 => 11497, + 1498 => 11498, + 1499 => 11499, + 1500 => 11500, + 1501 => 11501, + 1502 => 11502, + 1503 => 11503, + 1504 => 11504, + 1505 => 11505, + 1506 => 11506, + 1507 => 11507, + 1508 => 11508, + 1509 => 11509, + 1510 => 11510, + 1511 => 11511, + 1512 => 11512, + 1513 => 11513, + 1514 => 11514, + 1515 => 11515, + 1516 => 11516, + 1517 => 11517, + 1518 => 11518, + 1519 => 11519, + 1520 => 11520, + 1521 => 11521, + 1522 => 11522, + 1523 => 11523, + 1524 => 11524, + 1525 => 11525, + 1526 => 11526, + 1527 => 11527, + 1528 => 11528, + 1529 => 11529, + 1530 => 11530, + 1531 => 11531, + 1532 => 11532, + 1533 => 11533, + 1534 => 11534, + 1535 => 11535, + 1536 => 11536, + 1537 => 11537, + 1538 => 11538, + 1539 => 11539, + 1540 => 11540, + 1541 => 11541, + 1542 => 11542, + 1543 => 11543, + 1544 => 11544, + 1545 => 11545, + 1546 => 11546, + 1547 => 11547, + 1548 => 11548, + 1549 => 11549, + 1550 => 11550, + 1551 => 11551, + 1552 => 11552, + 1553 => 11553, + 1554 => 11554, + 1555 => 11555, + 1556 => 11556, + 1557 => 11557, + 1558 => 11558, + 1559 => 11559, + 1560 => 11560, + 1561 => 11561, + 1562 => 11562, + 1563 => 11563, + 1564 => 11564, + 1565 => 11565, + 1566 => 11566, + 1567 => 11567, + 1568 => 11568, + 1569 => 11569, + 1570 => 11570, + 1571 => 11571, + 1572 => 11572, + 1573 => 11573, + 1574 => 11574, + 1575 => 11575, + 1576 => 11576, + 1577 => 11577, + 1578 => 11578, + 1579 => 11579, + 1580 => 11580, + 1581 => 11581, + 1582 => 11582, + 1583 => 11583, + 1584 => 11584, + 1585 => 11585, + 1586 => 11586, + 1587 => 11587, + 1588 => 11588, + 1589 => 11589, + 1590 => 11590, + 1591 => 11591, + 1592 => 11592, + 1593 => 11593, + 1594 => 11594, + 1595 => 11595, + 1596 => 11596, + 1597 => 11597, + 1598 => 11598, + 1599 => 11599, + 1600 => 11600, + 1601 => 11601, + 1602 => 11602, + 1603 => 11603, + 1604 => 11604, + 1605 => 11605, + 1606 => 11606, + 1607 => 11607, + 1608 => 11608, + 1609 => 11609, + 1610 => 11610, + 1611 => 11611, + 1612 => 11612, + 1613 => 11613, + 1614 => 11614, + 1615 => 11615, + 1616 => 11616, + 1617 => 11617, + 1618 => 11618, + 1619 => 11619, + 1620 => 11620, + 1621 => 11621, + 1622 => 11622, + 1623 => 11623, + 1624 => 11624, + 1625 => 11625, + 1626 => 11626, + 1627 => 11627, + 1628 => 11628, + 1629 => 11629, + 1630 => 11630, + 1631 => 11631, + 1632 => 11632, + 1633 => 11633, + 1634 => 11634, + 1635 => 11635, + 1636 => 11636, + 1637 => 11637, + 1638 => 11638, + 1639 => 11639, + 1640 => 11640, + 1641 => 11641, + 1642 => 11642, + 1643 => 11643, + 1644 => 11644, + 1645 => 11645, + 1646 => 11646, + 1647 => 11647, + 1648 => 11648, + 1649 => 11649, + 1650 => 11650, + 1651 => 11651, + 1652 => 11652, + 1653 => 11653, + 1654 => 11654, + 1655 => 11655, + 1656 => 11656, + 1657 => 11657, + 1658 => 11658, + 1659 => 11659, + 1660 => 11660, + 1661 => 11661, + 1662 => 11662, + 1663 => 11663, + 1664 => 11664, + 1665 => 11665, + 1666 => 11666, + 1667 => 11667, + 1668 => 11668, + 1669 => 11669, + 1670 => 11670, + 1671 => 11671, + 1672 => 11672, + 1673 => 11673, + 1674 => 11674, + 1675 => 11675, + 1676 => 11676, + 1677 => 11677, + 1678 => 11678, + 1679 => 11679, + 1680 => 11680, + 1681 => 11681, + 1682 => 11682, + 1683 => 11683, + 1684 => 11684, + 1685 => 11685, + 1686 => 11686, + 1687 => 11687, + 1688 => 11688, + 1689 => 11689, + 1690 => 11690, + 1691 => 11691, + 1692 => 11692, + 1693 => 11693, + 1694 => 11694, + 1695 => 11695, + 1696 => 11696, + 1697 => 11697, + 1698 => 11698, + 1699 => 11699, + 1700 => 11700, + 1701 => 11701, + 1702 => 11702, + 1703 => 11703, + 1704 => 11704, + 1705 => 11705, + 1706 => 11706, + 1707 => 11707, + 1708 => 11708, + 1709 => 11709, + 1710 => 11710, + 1711 => 11711, + 1712 => 11712, + 1713 => 11713, + 1714 => 11714, + 1715 => 11715, + 1716 => 11716, + 1717 => 11717, + 1718 => 11718, + 1719 => 11719, + 1720 => 11720, + 1721 => 11721, + 1722 => 11722, + 1723 => 11723, + 1724 => 11724, + 1725 => 11725, + 1726 => 11726, + 1727 => 11727, + 1728 => 11728, + 1729 => 11729, + 1730 => 11730, + 1731 => 11731, + 1732 => 11732, + 1733 => 11733, + 1734 => 11734, + 1735 => 11735, + 1736 => 11736, + 1737 => 11737, + 1738 => 11738, + 1739 => 11739, + 1740 => 11740, + 1741 => 11741, + 1742 => 11742, + 1743 => 11743, + 1744 => 11744, + 1745 => 11745, + 1746 => 11746, + 1747 => 11747, + 1748 => 11748, + 1749 => 11749, + 1750 => 11750, + 1751 => 11751, + 1752 => 11752, + 1753 => 11753, + 1754 => 11754, + 1755 => 11755, + 1756 => 11756, + 1757 => 11757, + 1758 => 11758, + 1759 => 11759, + 1760 => 11760, + 1761 => 11761, + 1762 => 11762, + 1763 => 11763, + 1764 => 11764, + 1765 => 11765, + 1766 => 11766, + 1767 => 11767, + 1768 => 11768, + 1769 => 11769, + 1770 => 11770, + 1771 => 11771, + 1772 => 11772, + 1773 => 11773, + 1774 => 11774, + 1775 => 11775, + 1776 => 11776, + 1777 => 11777, + 1778 => 11778, + 1779 => 11779, + 1780 => 11780, + 1781 => 11781, + 1782 => 11782, + 1783 => 11783, + 1784 => 11784, + 1785 => 11785, + 1786 => 11786, + 1787 => 11787, + 1788 => 11788, + 1789 => 11789, + 1790 => 11790, + 1791 => 11791, + 1792 => 11792, + 1793 => 11793, + 1794 => 11794, + 1795 => 11795, + 1796 => 11796, + 1797 => 11797, + 1798 => 11798, + 1799 => 11799, + 1800 => 11800, + 1801 => 11801, + 1802 => 11802, + 1803 => 11803, + 1804 => 11804, + 1805 => 11805, + 1806 => 11806, + 1807 => 11807, + 1808 => 11808, + 1809 => 11809, + 1810 => 11810, + 1811 => 11811, + 1812 => 11812, + 1813 => 11813, + 1814 => 11814, + 1815 => 11815, + 1816 => 11816, + 1817 => 11817, + 1818 => 11818, + 1819 => 11819, + 1820 => 11820, + 1821 => 11821, + 1822 => 11822, + 1823 => 11823, + 1824 => 11824, + 1825 => 11825, + 1826 => 11826, + 1827 => 11827, + 1828 => 11828, + 1829 => 11829, + 1830 => 11830, + 1831 => 11831, + 1832 => 11832, + 1833 => 11833, + 1834 => 11834, + 1835 => 11835, + 1836 => 11836, + 1837 => 11837, + 1838 => 11838, + 1839 => 11839, + 1840 => 11840, + 1841 => 11841, + 1842 => 11842, + 1843 => 11843, + 1844 => 11844, + 1845 => 11845, + 1846 => 11846, + 1847 => 11847, + 1848 => 11848, + 1849 => 11849, + 1850 => 11850, + 1851 => 11851, + 1852 => 11852, + 1853 => 11853, + 1854 => 11854, + 1855 => 11855, + 1856 => 11856, + 1857 => 11857, + 1858 => 11858, + 1859 => 11859, + 1860 => 11860, + 1861 => 11861, + 1862 => 11862, + 1863 => 11863, + 1864 => 11864, + 1865 => 11865, + 1866 => 11866, + 1867 => 11867, + 1868 => 11868, + 1869 => 11869, + 1870 => 11870, + 1871 => 11871, + 1872 => 11872, + 1873 => 11873, + 1874 => 11874, + 1875 => 11875, + 1876 => 11876, + 1877 => 11877, + 1878 => 11878, + 1879 => 11879, + 1880 => 11880, + 1881 => 11881, + 1882 => 11882, + 1883 => 11883, + 1884 => 11884, + 1885 => 11885, + 1886 => 11886, + 1887 => 11887, + 1888 => 11888, + 1889 => 11889, + 1890 => 11890, + 1891 => 11891, + 1892 => 11892, + 1893 => 11893, + 1894 => 11894, + 1895 => 11895, + 1896 => 11896, + 1897 => 11897, + 1898 => 11898, + 1899 => 11899, + 1900 => 11900, + 1901 => 11901, + 1902 => 11902, + 1903 => 11903, + 1904 => 11904, + 1905 => 11905, + 1906 => 11906, + 1907 => 11907, + 1908 => 11908, + 1909 => 11909, + 1910 => 11910, + 1911 => 11911, + 1912 => 11912, + 1913 => 11913, + 1914 => 11914, + 1915 => 11915, + 1916 => 11916, + 1917 => 11917, + 1918 => 11918, + 1919 => 11919, + 1920 => 11920, + 1921 => 11921, + 1922 => 11922, + 1923 => 11923, + 1924 => 11924, + 1925 => 11925, + 1926 => 11926, + 1927 => 11927, + 1928 => 11928, + 1929 => 11929, + 1930 => 11930, + 1931 => 11931, + 1932 => 11932, + 1933 => 11933, + 1934 => 11934, + 1935 => 11935, + 1936 => 11936, + 1937 => 11937, + 1938 => 11938, + 1939 => 11939, + 1940 => 11940, + 1941 => 11941, + 1942 => 11942, + 1943 => 11943, + 1944 => 11944, + 1945 => 11945, + 1946 => 11946, + 1947 => 11947, + 1948 => 11948, + 1949 => 11949, + 1950 => 11950, + 1951 => 11951, + 1952 => 11952, + 1953 => 11953, + 1954 => 11954, + 1955 => 11955, + 1956 => 11956, + 1957 => 11957, + 1958 => 11958, + 1959 => 11959, + 1960 => 11960, + 1961 => 11961, + 1962 => 11962, + 1963 => 11963, + 1964 => 11964, + 1965 => 11965, + 1966 => 11966, + 1967 => 11967, + 1968 => 11968, + 1969 => 11969, + 1970 => 11970, + 1971 => 11971, + 1972 => 11972, + 1973 => 11973, + 1974 => 11974, + 1975 => 11975, + 1976 => 11976, + 1977 => 11977, + 1978 => 11978, + 1979 => 11979, + 1980 => 11980, + 1981 => 11981, + 1982 => 11982, + 1983 => 11983, + 1984 => 11984, + 1985 => 11985, + 1986 => 11986, + 1987 => 11987, + 1988 => 11988, + 1989 => 11989, + 1990 => 11990, + 1991 => 11991, + 1992 => 11992, + 1993 => 11993, + 1994 => 11994, + 1995 => 11995, + 1996 => 11996, + 1997 => 11997, + 1998 => 11998, + 1999 => 11999, + 2000 => 12000, + 2001 => 12001, + 2002 => 12002, + 2003 => 12003, + 2004 => 12004, + 2005 => 12005, + 2006 => 12006, + 2007 => 12007, + 2008 => 12008, + 2009 => 12009, + 2010 => 12010, + 2011 => 12011, + 2012 => 12012, + 2013 => 12013, + 2014 => 12014, + 2015 => 12015, + 2016 => 12016, + 2017 => 12017, + 2018 => 12018, + 2019 => 12019, + 2020 => 12020, + 2021 => 12021, + 2022 => 12022, + 2023 => 12023, + 2024 => 12024, + 2025 => 12025, + 2026 => 12026, + 2027 => 12027, + 2028 => 12028, + 2029 => 12029, + 2030 => 12030, + 2031 => 12031, + 2032 => 12032, + 2033 => 12033, + 2034 => 12034, + 2035 => 12035, + 2036 => 12036, + 2037 => 12037, + 2038 => 12038, + 2039 => 12039, + 2040 => 12040, + 2041 => 12041, + 2042 => 12042, + 2043 => 12043, + 2044 => 12044, + 2045 => 12045, + 2046 => 12046, + 2047 => 12047, + 2048 => 12048, + 2049 => 12049, + 2050 => 12050, + 2051 => 12051, + 2052 => 12052, + 2053 => 12053, + 2054 => 12054, + 2055 => 12055, + 2056 => 12056, + 2057 => 12057, + 2058 => 12058, + 2059 => 12059, + 2060 => 12060, + 2061 => 12061, + 2062 => 12062, + 2063 => 12063, + 2064 => 12064, + 2065 => 12065, + 2066 => 12066, + 2067 => 12067, + 2068 => 12068, + 2069 => 12069, + 2070 => 12070, + 2071 => 12071, + 2072 => 12072, + 2073 => 12073, + 2074 => 12074, + 2075 => 12075, + 2076 => 12076, + 2077 => 12077, + 2078 => 12078, + 2079 => 12079, + 2080 => 12080, + 2081 => 12081, + 2082 => 12082, + 2083 => 12083, + 2084 => 12084, + 2085 => 12085, + 2086 => 12086, + 2087 => 12087, + 2088 => 12088, + 2089 => 12089, + 2090 => 12090, + 2091 => 12091, + 2092 => 12092, + 2093 => 12093, + 2094 => 12094, + 2095 => 12095, + 2096 => 12096, + 2097 => 12097, + 2098 => 12098, + 2099 => 12099, + 2100 => 12100, + 2101 => 12101, + 2102 => 12102, + 2103 => 12103, + 2104 => 12104, + 2105 => 12105, + 2106 => 12106, + 2107 => 12107, + 2108 => 12108, + 2109 => 12109, + 2110 => 12110, + 2111 => 12111, + 2112 => 12112, + 2113 => 12113, + 2114 => 12114, + 2115 => 12115, + 2116 => 12116, + 2117 => 12117, + 2118 => 12118, + 2119 => 12119, + 2120 => 12120, + 2121 => 12121, + 2122 => 12122, + 2123 => 12123, + 2124 => 12124, + 2125 => 12125, + 2126 => 12126, + 2127 => 12127, + 2128 => 12128, + 2129 => 12129, + 2130 => 12130, + 2131 => 12131, + 2132 => 12132, + 2133 => 12133, + 2134 => 12134, + 2135 => 12135, + 2136 => 12136, + 2137 => 12137, + 2138 => 12138, + 2139 => 12139, + 2140 => 12140, + 2141 => 12141, + 2142 => 12142, + 2143 => 12143, + 2144 => 12144, + 2145 => 12145, + 2146 => 12146, + 2147 => 12147, + 2148 => 12148, + 2149 => 12149, + 2150 => 12150, + 2151 => 12151, + 2152 => 12152, + 2153 => 12153, + 2154 => 12154, + 2155 => 12155, + 2156 => 12156, + 2157 => 12157, + 2158 => 12158, + 2159 => 12159, + 2160 => 12160, + 2161 => 12161, + 2162 => 12162, + 2163 => 12163, + 2164 => 12164, + 2165 => 12165, + 2166 => 12166, + 2167 => 12167, + 2168 => 12168, + 2169 => 12169, + 2170 => 12170, + 2171 => 12171, + 2172 => 12172, + 2173 => 12173, + 2174 => 12174, + 2175 => 12175, + 2176 => 12176, + 2177 => 12177, + 2178 => 12178, + 2179 => 12179, + 2180 => 12180, + 2181 => 12181, + 2182 => 12182, + 2183 => 12183, + 2184 => 12184, + 2185 => 12185, + 2186 => 12186, + 2187 => 12187, + 2188 => 12188, + 2189 => 12189, + 2190 => 12190, + 2191 => 12191, + 2192 => 12192, + 2193 => 12193, + 2194 => 12194, + 2195 => 12195, + 2196 => 12196, + 2197 => 12197, + 2198 => 12198, + 2199 => 12199, + 2200 => 12200, + 2201 => 12201, + 2202 => 12202, + 2203 => 12203, + 2204 => 12204, + 2205 => 12205, + 2206 => 12206, + 2207 => 12207, + 2208 => 12208, + 2209 => 12209, + 2210 => 12210, + 2211 => 12211, + 2212 => 12212, + 2213 => 12213, + 2214 => 12214, + 2215 => 12215, + 2216 => 12216, + 2217 => 12217, + 2218 => 12218, + 2219 => 12219, + 2220 => 12220, + 2221 => 12221, + 2222 => 12222, + 2223 => 12223, + 2224 => 12224, + 2225 => 12225, + 2226 => 12226, + 2227 => 12227, + 2228 => 12228, + 2229 => 12229, + 2230 => 12230, + 2231 => 12231, + 2232 => 12232, + 2233 => 12233, + 2234 => 12234, + 2235 => 12235, + 2236 => 12236, + 2237 => 12237, + 2238 => 12238, + 2239 => 12239, + 2240 => 12240, + 2241 => 12241, + 2242 => 12242, + 2243 => 12243, + 2244 => 12244, + 2245 => 12245, + 2246 => 12246, + 2247 => 12247, + 2248 => 12248, + 2249 => 12249, + 2250 => 12250, + 2251 => 12251, + 2252 => 12252, + 2253 => 12253, + 2254 => 12254, + 2255 => 12255, + 2256 => 12256, + 2257 => 12257, + 2258 => 12258, + 2259 => 12259, + 2260 => 12260, + 2261 => 12261, + 2262 => 12262, + 2263 => 12263, + 2264 => 12264, + 2265 => 12265, + 2266 => 12266, + 2267 => 12267, + 2268 => 12268, + 2269 => 12269, + 2270 => 12270, + 2271 => 12271, + 2272 => 12272, + 2273 => 12273, + 2274 => 12274, + 2275 => 12275, + 2276 => 12276, + 2277 => 12277, + 2278 => 12278, + 2279 => 12279, + 2280 => 12280, + 2281 => 12281, + 2282 => 12282, + 2283 => 12283, + 2284 => 12284, + 2285 => 12285, + 2286 => 12286, + 2287 => 12287, + 2288 => 12288, + 2289 => 12289, + 2290 => 12290, + 2291 => 12291, + 2292 => 12292, + 2293 => 12293, + 2294 => 12294, + 2295 => 12295, + 2296 => 12296, + 2297 => 12297, + 2298 => 12298, + 2299 => 12299, + 2300 => 12300, + 2301 => 12301, + 2302 => 12302, + 2303 => 12303, + 2304 => 12304, + 2305 => 12305, + 2306 => 12306, + 2307 => 12307, + 2308 => 12308, + 2309 => 12309, + 2310 => 12310, + 2311 => 12311, + 2312 => 12312, + 2313 => 12313, + 2314 => 12314, + 2315 => 12315, + 2316 => 12316, + 2317 => 12317, + 2318 => 12318, + 2319 => 12319, + 2320 => 12320, + 2321 => 12321, + 2322 => 12322, + 2323 => 12323, + 2324 => 12324, + 2325 => 12325, + 2326 => 12326, + 2327 => 12327, + 2328 => 12328, + 2329 => 12329, + 2330 => 12330, + 2331 => 12331, + 2332 => 12332, + 2333 => 12333, + 2334 => 12334, + 2335 => 12335, + 2336 => 12336, + 2337 => 12337, + 2338 => 12338, + 2339 => 12339, + 2340 => 12340, + 2341 => 12341, + 2342 => 12342, + 2343 => 12343, + 2344 => 12344, + 2345 => 12345, + 2346 => 12346, + 2347 => 12347, + 2348 => 12348, + 2349 => 12349, + 2350 => 12350, + 2351 => 12351, + 2352 => 12352, + 2353 => 12353, + 2354 => 12354, + 2355 => 12355, + 2356 => 12356, + 2357 => 12357, + 2358 => 12358, + 2359 => 12359, + 2360 => 12360, + 2361 => 12361, + 2362 => 12362, + 2363 => 12363, + 2364 => 12364, + 2365 => 12365, + 2366 => 12366, + 2367 => 12367, + 2368 => 12368, + 2369 => 12369, + 2370 => 12370, + 2371 => 12371, + 2372 => 12372, + 2373 => 12373, + 2374 => 12374, + 2375 => 12375, + 2376 => 12376, + 2377 => 12377, + 2378 => 12378, + 2379 => 12379, + 2380 => 12380, + 2381 => 12381, + 2382 => 12382, + 2383 => 12383, + 2384 => 12384, + 2385 => 12385, + 2386 => 12386, + 2387 => 12387, + 2388 => 12388, + 2389 => 12389, + 2390 => 12390, + 2391 => 12391, + 2392 => 12392, + 2393 => 12393, + 2394 => 12394, + 2395 => 12395, + 2396 => 12396, + 2397 => 12397, + 2398 => 12398, + 2399 => 12399, + 2400 => 12400, + 2401 => 12401, + 2402 => 12402, + 2403 => 12403, + 2404 => 12404, + 2405 => 12405, + 2406 => 12406, + 2407 => 12407, + 2408 => 12408, + 2409 => 12409, + 2410 => 12410, + 2411 => 12411, + 2412 => 12412, + 2413 => 12413, + 2414 => 12414, + 2415 => 12415, + 2416 => 12416, + 2417 => 12417, + 2418 => 12418, + 2419 => 12419, + 2420 => 12420, + 2421 => 12421, + 2422 => 12422, + 2423 => 12423, + 2424 => 12424, + 2425 => 12425, + 2426 => 12426, + 2427 => 12427, + 2428 => 12428, + 2429 => 12429, + 2430 => 12430, + 2431 => 12431, + 2432 => 12432, + 2433 => 12433, + 2434 => 12434, + 2435 => 12435, + 2436 => 12436, + 2437 => 12437, + 2438 => 12438, + 2439 => 12439, + 2440 => 12440, + 2441 => 12441, + 2442 => 12442, + 2443 => 12443, + 2444 => 12444, + 2445 => 12445, + 2446 => 12446, + 2447 => 12447, + 2448 => 12448, + 2449 => 12449, + 2450 => 12450, + 2451 => 12451, + 2452 => 12452, + 2453 => 12453, + 2454 => 12454, + 2455 => 12455, + 2456 => 12456, + 2457 => 12457, + 2458 => 12458, + 2459 => 12459, + 2460 => 12460, + 2461 => 12461, + 2462 => 12462, + 2463 => 12463, + 2464 => 12464, + 2465 => 12465, + 2466 => 12466, + 2467 => 12467, + 2468 => 12468, + 2469 => 12469, + 2470 => 12470, + 2471 => 12471, + 2472 => 12472, + 2473 => 12473, + 2474 => 12474, + 2475 => 12475, + 2476 => 12476, + 2477 => 12477, + 2478 => 12478, + 2479 => 12479, + 2480 => 12480, + 2481 => 12481, + 2482 => 12482, + 2483 => 12483, + 2484 => 12484, + 2485 => 12485, + 2486 => 12486, + 2487 => 12487, + 2488 => 12488, + 2489 => 12489, + 2490 => 12490, + 2491 => 12491, + 2492 => 12492, + 2493 => 12493, + 2494 => 12494, + 2495 => 12495, + 2496 => 12496, + 2497 => 12497, + 2498 => 12498, + 2499 => 12499, + 2500 => 12500, + 2501 => 12501, + 2502 => 12502, + 2503 => 12503, + 2504 => 12504, + 2505 => 12505, + 2506 => 12506, + 2507 => 12507, + 2508 => 12508, + 2509 => 12509, + 2510 => 12510, + 2511 => 12511, + 2512 => 12512, + 2513 => 12513, + 2514 => 12514, + 2515 => 12515, + 2516 => 12516, + 2517 => 12517, + 2518 => 12518, + 2519 => 12519, + 2520 => 12520, + 2521 => 12521, + 2522 => 12522, + 2523 => 12523, + 2524 => 12524, + 2525 => 12525, + 2526 => 12526, + 2527 => 12527, + 2528 => 12528, + 2529 => 12529, + 2530 => 12530, + 2531 => 12531, + 2532 => 12532, + 2533 => 12533, + 2534 => 12534, + 2535 => 12535, + 2536 => 12536, + 2537 => 12537, + 2538 => 12538, + 2539 => 12539, + 2540 => 12540, + 2541 => 12541, + 2542 => 12542, + 2543 => 12543, + 2544 => 12544, + 2545 => 12545, + 2546 => 12546, + 2547 => 12547, + 2548 => 12548, + 2549 => 12549, + 2550 => 12550, + 2551 => 12551, + 2552 => 12552, + 2553 => 12553, + 2554 => 12554, + 2555 => 12555, + 2556 => 12556, + 2557 => 12557, + 2558 => 12558, + 2559 => 12559, + 2560 => 12560, + 2561 => 12561, + 2562 => 12562, + 2563 => 12563, + 2564 => 12564, + 2565 => 12565, + 2566 => 12566, + 2567 => 12567, + 2568 => 12568, + 2569 => 12569, + 2570 => 12570, + 2571 => 12571, + 2572 => 12572, + 2573 => 12573, + 2574 => 12574, + 2575 => 12575, + 2576 => 12576, + 2577 => 12577, + 2578 => 12578, + 2579 => 12579, + 2580 => 12580, + 2581 => 12581, + 2582 => 12582, + 2583 => 12583, + 2584 => 12584, + 2585 => 12585, + 2586 => 12586, + 2587 => 12587, + 2588 => 12588, + 2589 => 12589, + 2590 => 12590, + 2591 => 12591, + 2592 => 12592, + 2593 => 12593, + 2594 => 12594, + 2595 => 12595, + 2596 => 12596, + 2597 => 12597, + 2598 => 12598, + 2599 => 12599, + 2600 => 12600, + 2601 => 12601, + 2602 => 12602, + 2603 => 12603, + 2604 => 12604, + 2605 => 12605, + 2606 => 12606, + 2607 => 12607, + 2608 => 12608, + 2609 => 12609, + 2610 => 12610, + 2611 => 12611, + 2612 => 12612, + 2613 => 12613, + 2614 => 12614, + 2615 => 12615, + 2616 => 12616, + 2617 => 12617, + 2618 => 12618, + 2619 => 12619, + 2620 => 12620, + 2621 => 12621, + 2622 => 12622, + 2623 => 12623, + 2624 => 12624, + 2625 => 12625, + 2626 => 12626, + 2627 => 12627, + 2628 => 12628, + 2629 => 12629, + 2630 => 12630, + 2631 => 12631, + 2632 => 12632, + 2633 => 12633, + 2634 => 12634, + 2635 => 12635, + 2636 => 12636, + 2637 => 12637, + 2638 => 12638, + 2639 => 12639, + 2640 => 12640, + 2641 => 12641, + 2642 => 12642, + 2643 => 12643, + 2644 => 12644, + 2645 => 12645, + 2646 => 12646, + 2647 => 12647, + 2648 => 12648, + 2649 => 12649, + 2650 => 12650, + 2651 => 12651, + 2652 => 12652, + 2653 => 12653, + 2654 => 12654, + 2655 => 12655, + 2656 => 12656, + 2657 => 12657, + 2658 => 12658, + 2659 => 12659, + 2660 => 12660, + 2661 => 12661, + 2662 => 12662, + 2663 => 12663, + 2664 => 12664, + 2665 => 12665, + 2666 => 12666, + 2667 => 12667, + 2668 => 12668, + 2669 => 12669, + 2670 => 12670, + 2671 => 12671, + 2672 => 12672, + 2673 => 12673, + 2674 => 12674, + 2675 => 12675, + 2676 => 12676, + 2677 => 12677, + 2678 => 12678, + 2679 => 12679, + 2680 => 12680, + 2681 => 12681, + 2682 => 12682, + 2683 => 12683, + 2684 => 12684, + 2685 => 12685, + 2686 => 12686, + 2687 => 12687, + 2688 => 12688, + 2689 => 12689, + 2690 => 12690, + 2691 => 12691, + 2692 => 12692, + 2693 => 12693, + 2694 => 12694, + 2695 => 12695, + 2696 => 12696, + 2697 => 12697, + 2698 => 12698, + 2699 => 12699, + 2700 => 12700, + 2701 => 12701, + 2702 => 12702, + 2703 => 12703, + 2704 => 12704, + 2705 => 12705, + 2706 => 12706, + 2707 => 12707, + 2708 => 12708, + 2709 => 12709, + 2710 => 12710, + 2711 => 12711, + 2712 => 12712, + 2713 => 12713, + 2714 => 12714, + 2715 => 12715, + 2716 => 12716, + 2717 => 12717, + 2718 => 12718, + 2719 => 12719, + 2720 => 12720, + 2721 => 12721, + 2722 => 12722, + 2723 => 12723, + 2724 => 12724, + 2725 => 12725, + 2726 => 12726, + 2727 => 12727, + 2728 => 12728, + 2729 => 12729, + 2730 => 12730, + 2731 => 12731, + 2732 => 12732, + 2733 => 12733, + 2734 => 12734, + 2735 => 12735, + 2736 => 12736, + 2737 => 12737, + 2738 => 12738, + 2739 => 12739, + 2740 => 12740, + 2741 => 12741, + 2742 => 12742, + 2743 => 12743, + 2744 => 12744, + 2745 => 12745, + 2746 => 12746, + 2747 => 12747, + 2748 => 12748, + 2749 => 12749, + 2750 => 12750, + 2751 => 12751, + 2752 => 12752, + 2753 => 12753, + 2754 => 12754, + 2755 => 12755, + 2756 => 12756, + 2757 => 12757, + 2758 => 12758, + 2759 => 12759, + 2760 => 12760, + 2761 => 12761, + 2762 => 12762, + 2763 => 12763, + 2764 => 12764, + 2765 => 12765, + 2766 => 12766, + 2767 => 12767, + 2768 => 12768, + 2769 => 12769, + 2770 => 12770, + 2771 => 12771, + 2772 => 12772, + 2773 => 12773, + 2774 => 12774, + 2775 => 12775, + 2776 => 12776, + 2777 => 12777, + 2778 => 12778, + 2779 => 12779, + 2780 => 12780, + 2781 => 12781, + 2782 => 12782, + 2783 => 12783, + 2784 => 12784, + 2785 => 12785, + 2786 => 12786, + 2787 => 12787, + 2788 => 12788, + 2789 => 12789, + 2790 => 12790, + 2791 => 12791, + 2792 => 12792, + 2793 => 12793, + 2794 => 12794, + 2795 => 12795, + 2796 => 12796, + 2797 => 12797, + 2798 => 12798, + 2799 => 12799, + 2800 => 12800, + 2801 => 12801, + 2802 => 12802, + 2803 => 12803, + 2804 => 12804, + 2805 => 12805, + 2806 => 12806, + 2807 => 12807, + 2808 => 12808, + 2809 => 12809, + 2810 => 12810, + 2811 => 12811, + 2812 => 12812, + 2813 => 12813, + 2814 => 12814, + 2815 => 12815, + 2816 => 12816, + 2817 => 12817, + 2818 => 12818, + 2819 => 12819, + 2820 => 12820, + 2821 => 12821, + 2822 => 12822, + 2823 => 12823, + 2824 => 12824, + 2825 => 12825, + 2826 => 12826, + 2827 => 12827, + 2828 => 12828, + 2829 => 12829, + 2830 => 12830, + 2831 => 12831, + 2832 => 12832, + 2833 => 12833, + 2834 => 12834, + 2835 => 12835, + 2836 => 12836, + 2837 => 12837, + 2838 => 12838, + 2839 => 12839, + 2840 => 12840, + 2841 => 12841, + 2842 => 12842, + 2843 => 12843, + 2844 => 12844, + 2845 => 12845, + 2846 => 12846, + 2847 => 12847, + 2848 => 12848, + 2849 => 12849, + 2850 => 12850, + 2851 => 12851, + 2852 => 12852, + 2853 => 12853, + 2854 => 12854, + 2855 => 12855, + 2856 => 12856, + 2857 => 12857, + 2858 => 12858, + 2859 => 12859, + 2860 => 12860, + 2861 => 12861, + 2862 => 12862, + 2863 => 12863, + 2864 => 12864, + 2865 => 12865, + 2866 => 12866, + 2867 => 12867, + 2868 => 12868, + 2869 => 12869, + 2870 => 12870, + 2871 => 12871, + 2872 => 12872, + 2873 => 12873, + 2874 => 12874, + 2875 => 12875, + 2876 => 12876, + 2877 => 12877, + 2878 => 12878, + 2879 => 12879, + 2880 => 12880, + 2881 => 12881, + 2882 => 12882, + 2883 => 12883, + 2884 => 12884, + 2885 => 12885, + 2886 => 12886, + 2887 => 12887, + 2888 => 12888, + 2889 => 12889, + 2890 => 12890, + 2891 => 12891, + 2892 => 12892, + 2893 => 12893, + 2894 => 12894, + 2895 => 12895, + 2896 => 12896, + 2897 => 12897, + 2898 => 12898, + 2899 => 12899, + 2900 => 12900, + 2901 => 12901, + 2902 => 12902, + 2903 => 12903, + 2904 => 12904, + 2905 => 12905, + 2906 => 12906, + 2907 => 12907, + 2908 => 12908, + 2909 => 12909, + 2910 => 12910, + 2911 => 12911, + 2912 => 12912, + 2913 => 12913, + 2914 => 12914, + 2915 => 12915, + 2916 => 12916, + 2917 => 12917, + 2918 => 12918, + 2919 => 12919, + 2920 => 12920, + 2921 => 12921, + 2922 => 12922, + 2923 => 12923, + 2924 => 12924, + 2925 => 12925, + 2926 => 12926, + 2927 => 12927, + 2928 => 12928, + 2929 => 12929, + 2930 => 12930, + 2931 => 12931, + 2932 => 12932, + 2933 => 12933, + 2934 => 12934, + 2935 => 12935, + 2936 => 12936, + 2937 => 12937, + 2938 => 12938, + 2939 => 12939, + 2940 => 12940, + 2941 => 12941, + 2942 => 12942, + 2943 => 12943, + 2944 => 12944, + 2945 => 12945, + 2946 => 12946, + 2947 => 12947, + 2948 => 12948, + 2949 => 12949, + 2950 => 12950, + 2951 => 12951, + 2952 => 12952, + 2953 => 12953, + 2954 => 12954, + 2955 => 12955, + 2956 => 12956, + 2957 => 12957, + 2958 => 12958, + 2959 => 12959, + 2960 => 12960, + 2961 => 12961, + 2962 => 12962, + 2963 => 12963, + 2964 => 12964, + 2965 => 12965, + 2966 => 12966, + 2967 => 12967, + 2968 => 12968, + 2969 => 12969, + 2970 => 12970, + 2971 => 12971, + 2972 => 12972, + 2973 => 12973, + 2974 => 12974, + 2975 => 12975, + 2976 => 12976, + 2977 => 12977, + 2978 => 12978, + 2979 => 12979, + 2980 => 12980, + 2981 => 12981, + 2982 => 12982, + 2983 => 12983, + 2984 => 12984, + 2985 => 12985, + 2986 => 12986, + 2987 => 12987, + 2988 => 12988, + 2989 => 12989, + 2990 => 12990, + 2991 => 12991, + 2992 => 12992, + 2993 => 12993, + 2994 => 12994, + 2995 => 12995, + 2996 => 12996, + 2997 => 12997, + 2998 => 12998, + 2999 => 12999, + 3000 => 13000, + 3001 => 13001, + 3002 => 13002, + 3003 => 13003, + 3004 => 13004, + 3005 => 13005, + 3006 => 13006, + 3007 => 13007, + 3008 => 13008, + 3009 => 13009, + 3010 => 13010, + 3011 => 13011, + 3012 => 13012, + 3013 => 13013, + 3014 => 13014, + 3015 => 13015, + 3016 => 13016, + 3017 => 13017, + 3018 => 13018, + 3019 => 13019, + 3020 => 13020, + 3021 => 13021, + 3022 => 13022, + 3023 => 13023, + 3024 => 13024, + 3025 => 13025, + 3026 => 13026, + 3027 => 13027, + 3028 => 13028, + 3029 => 13029, + 3030 => 13030, + 3031 => 13031, + 3032 => 13032, + 3033 => 13033, + 3034 => 13034, + 3035 => 13035, + 3036 => 13036, + 3037 => 13037, + 3038 => 13038, + 3039 => 13039, + 3040 => 13040, + 3041 => 13041, + 3042 => 13042, + 3043 => 13043, + 3044 => 13044, + 3045 => 13045, + 3046 => 13046, + 3047 => 13047, + 3048 => 13048, + 3049 => 13049, + 3050 => 13050, + 3051 => 13051, + 3052 => 13052, + 3053 => 13053, + 3054 => 13054, + 3055 => 13055, + 3056 => 13056, + 3057 => 13057, + 3058 => 13058, + 3059 => 13059, + 3060 => 13060, + 3061 => 13061, + 3062 => 13062, + 3063 => 13063, + 3064 => 13064, + 3065 => 13065, + 3066 => 13066, + 3067 => 13067, + 3068 => 13068, + 3069 => 13069, + 3070 => 13070, + 3071 => 13071, + 3072 => 13072, + 3073 => 13073, + 3074 => 13074, + 3075 => 13075, + 3076 => 13076, + 3077 => 13077, + 3078 => 13078, + 3079 => 13079, + 3080 => 13080, + 3081 => 13081, + 3082 => 13082, + 3083 => 13083, + 3084 => 13084, + 3085 => 13085, + 3086 => 13086, + 3087 => 13087, + 3088 => 13088, + 3089 => 13089, + 3090 => 13090, + 3091 => 13091, + 3092 => 13092, + 3093 => 13093, + 3094 => 13094, + 3095 => 13095, + 3096 => 13096, + 3097 => 13097, + 3098 => 13098, + 3099 => 13099, + 3100 => 13100, + 3101 => 13101, + 3102 => 13102, + 3103 => 13103, + 3104 => 13104, + 3105 => 13105, + 3106 => 13106, + 3107 => 13107, + 3108 => 13108, + 3109 => 13109, + 3110 => 13110, + 3111 => 13111, + 3112 => 13112, + 3113 => 13113, + 3114 => 13114, + 3115 => 13115, + 3116 => 13116, + 3117 => 13117, + 3118 => 13118, + 3119 => 13119, + 3120 => 13120, + 3121 => 13121, + 3122 => 13122, + 3123 => 13123, + 3124 => 13124, + 3125 => 13125, + 3126 => 13126, + 3127 => 13127, + 3128 => 13128, + 3129 => 13129, + 3130 => 13130, + 3131 => 13131, + 3132 => 13132, + 3133 => 13133, + 3134 => 13134, + 3135 => 13135, + 3136 => 13136, + 3137 => 13137, + 3138 => 13138, + 3139 => 13139, + 3140 => 13140, + 3141 => 13141, + 3142 => 13142, + 3143 => 13143, + 3144 => 13144, + 3145 => 13145, + 3146 => 13146, + 3147 => 13147, + 3148 => 13148, + 3149 => 13149, + 3150 => 13150, + 3151 => 13151, + 3152 => 13152, + 3153 => 13153, + 3154 => 13154, + 3155 => 13155, + 3156 => 13156, + 3157 => 13157, + 3158 => 13158, + 3159 => 13159, + 3160 => 13160, + 3161 => 13161, + 3162 => 13162, + 3163 => 13163, + 3164 => 13164, + 3165 => 13165, + 3166 => 13166, + 3167 => 13167, + 3168 => 13168, + 3169 => 13169, + 3170 => 13170, + 3171 => 13171, + 3172 => 13172, + 3173 => 13173, + 3174 => 13174, + 3175 => 13175, + 3176 => 13176, + 3177 => 13177, + 3178 => 13178, + 3179 => 13179, + 3180 => 13180, + 3181 => 13181, + 3182 => 13182, + 3183 => 13183, + 3184 => 13184, + 3185 => 13185, + 3186 => 13186, + 3187 => 13187, + 3188 => 13188, + 3189 => 13189, + 3190 => 13190, + 3191 => 13191, + 3192 => 13192, + 3193 => 13193, + 3194 => 13194, + 3195 => 13195, + 3196 => 13196, + 3197 => 13197, + 3198 => 13198, + 3199 => 13199, + 3200 => 13200, + 3201 => 13201, + 3202 => 13202, + 3203 => 13203, + 3204 => 13204, + 3205 => 13205, + 3206 => 13206, + 3207 => 13207, + 3208 => 13208, + 3209 => 13209, + 3210 => 13210, + 3211 => 13211, + 3212 => 13212, + 3213 => 13213, + 3214 => 13214, + 3215 => 13215, + 3216 => 13216, + 3217 => 13217, + 3218 => 13218, + 3219 => 13219, + 3220 => 13220, + 3221 => 13221, + 3222 => 13222, + 3223 => 13223, + 3224 => 13224, + 3225 => 13225, + 3226 => 13226, + 3227 => 13227, + 3228 => 13228, + 3229 => 13229, + 3230 => 13230, + 3231 => 13231, + 3232 => 13232, + 3233 => 13233, + 3234 => 13234, + 3235 => 13235, + 3236 => 13236, + 3237 => 13237, + 3238 => 13238, + 3239 => 13239, + 3240 => 13240, + 3241 => 13241, + 3242 => 13242, + 3243 => 13243, + 3244 => 13244, + 3245 => 13245, + 3246 => 13246, + 3247 => 13247, + 3248 => 13248, + 3249 => 13249, + 3250 => 13250, + 3251 => 13251, + 3252 => 13252, + 3253 => 13253, + 3254 => 13254, + 3255 => 13255, + 3256 => 13256, + 3257 => 13257, + 3258 => 13258, + 3259 => 13259, + 3260 => 13260, + 3261 => 13261, + 3262 => 13262, + 3263 => 13263, + 3264 => 13264, + 3265 => 13265, + 3266 => 13266, + 3267 => 13267, + 3268 => 13268, + 3269 => 13269, + 3270 => 13270, + 3271 => 13271, + 3272 => 13272, + 3273 => 13273, + 3274 => 13274, + 3275 => 13275, + 3276 => 13276, + 3277 => 13277, + 3278 => 13278, + 3279 => 13279, + 3280 => 13280, + 3281 => 13281, + 3282 => 13282, + 3283 => 13283, + 3284 => 13284, + 3285 => 13285, + 3286 => 13286, + 3287 => 13287, + 3288 => 13288, + 3289 => 13289, + 3290 => 13290, + 3291 => 13291, + 3292 => 13292, + 3293 => 13293, + 3294 => 13294, + 3295 => 13295, + 3296 => 13296, + 3297 => 13297, + 3298 => 13298, + 3299 => 13299, + 3300 => 13300, + 3301 => 13301, + 3302 => 13302, + 3303 => 13303, + 3304 => 13304, + 3305 => 13305, + 3306 => 13306, + 3307 => 13307, + 3308 => 13308, + 3309 => 13309, + 3310 => 13310, + 3311 => 13311, + 3312 => 13312, + 3313 => 13313, + 3314 => 13314, + 3315 => 13315, + 3316 => 13316, + 3317 => 13317, + 3318 => 13318, + 3319 => 13319, + 3320 => 13320, + 3321 => 13321, + 3322 => 13322, + 3323 => 13323, + 3324 => 13324, + 3325 => 13325, + 3326 => 13326, + 3327 => 13327, + 3328 => 13328, + 3329 => 13329, + 3330 => 13330, + 3331 => 13331, + 3332 => 13332, + 3333 => 13333, + 3334 => 13334, + 3335 => 13335, + 3336 => 13336, + 3337 => 13337, + 3338 => 13338, + 3339 => 13339, + 3340 => 13340, + 3341 => 13341, + 3342 => 13342, + 3343 => 13343, + 3344 => 13344, + 3345 => 13345, + 3346 => 13346, + 3347 => 13347, + 3348 => 13348, + 3349 => 13349, + 3350 => 13350, + 3351 => 13351, + 3352 => 13352, + 3353 => 13353, + 3354 => 13354, + 3355 => 13355, + 3356 => 13356, + 3357 => 13357, + 3358 => 13358, + 3359 => 13359, + 3360 => 13360, + 3361 => 13361, + 3362 => 13362, + 3363 => 13363, + 3364 => 13364, + 3365 => 13365, + 3366 => 13366, + 3367 => 13367, + 3368 => 13368, + 3369 => 13369, + 3370 => 13370, + 3371 => 13371, + 3372 => 13372, + 3373 => 13373, + 3374 => 13374, + 3375 => 13375, + 3376 => 13376, + 3377 => 13377, + 3378 => 13378, + 3379 => 13379, + 3380 => 13380, + 3381 => 13381, + 3382 => 13382, + 3383 => 13383, + 3384 => 13384, + 3385 => 13385, + 3386 => 13386, + 3387 => 13387, + 3388 => 13388, + 3389 => 13389, + 3390 => 13390, + 3391 => 13391, + 3392 => 13392, + 3393 => 13393, + 3394 => 13394, + 3395 => 13395, + 3396 => 13396, + 3397 => 13397, + 3398 => 13398, + 3399 => 13399, + 3400 => 13400, + 3401 => 13401, + 3402 => 13402, + 3403 => 13403, + 3404 => 13404, + 3405 => 13405, + 3406 => 13406, + 3407 => 13407, + 3408 => 13408, + 3409 => 13409, + 3410 => 13410, + 3411 => 13411, + 3412 => 13412, + 3413 => 13413, + 3414 => 13414, + 3415 => 13415, + 3416 => 13416, + 3417 => 13417, + 3418 => 13418, + 3419 => 13419, + 3420 => 13420, + 3421 => 13421, + 3422 => 13422, + 3423 => 13423, + 3424 => 13424, + 3425 => 13425, + 3426 => 13426, + 3427 => 13427, + 3428 => 13428, + 3429 => 13429, + 3430 => 13430, + 3431 => 13431, + 3432 => 13432, + 3433 => 13433, + 3434 => 13434, + 3435 => 13435, + 3436 => 13436, + 3437 => 13437, + 3438 => 13438, + 3439 => 13439, + 3440 => 13440, + 3441 => 13441, + 3442 => 13442, + 3443 => 13443, + 3444 => 13444, + 3445 => 13445, + 3446 => 13446, + 3447 => 13447, + 3448 => 13448, + 3449 => 13449, + 3450 => 13450, + 3451 => 13451, + 3452 => 13452, + 3453 => 13453, + 3454 => 13454, + 3455 => 13455, + 3456 => 13456, + 3457 => 13457, + 3458 => 13458, + 3459 => 13459, + 3460 => 13460, + 3461 => 13461, + 3462 => 13462, + 3463 => 13463, + 3464 => 13464, + 3465 => 13465, + 3466 => 13466, + 3467 => 13467, + 3468 => 13468, + 3469 => 13469, + 3470 => 13470, + 3471 => 13471, + 3472 => 13472, + 3473 => 13473, + 3474 => 13474, + 3475 => 13475, + 3476 => 13476, + 3477 => 13477, + 3478 => 13478, + 3479 => 13479, + 3480 => 13480, + 3481 => 13481, + 3482 => 13482, + 3483 => 13483, + 3484 => 13484, + 3485 => 13485, + 3486 => 13486, + 3487 => 13487, + 3488 => 13488, + 3489 => 13489, + 3490 => 13490, + 3491 => 13491, + 3492 => 13492, + 3493 => 13493, + 3494 => 13494, + 3495 => 13495, + 3496 => 13496, + 3497 => 13497, + 3498 => 13498, + 3499 => 13499, + 3500 => 13500, + 3501 => 13501, + 3502 => 13502, + 3503 => 13503, + 3504 => 13504, + 3505 => 13505, + 3506 => 13506, + 3507 => 13507, + 3508 => 13508, + 3509 => 13509, + 3510 => 13510, + 3511 => 13511, + 3512 => 13512, + 3513 => 13513, + 3514 => 13514, + 3515 => 13515, + 3516 => 13516, + 3517 => 13517, + 3518 => 13518, + 3519 => 13519, + 3520 => 13520, + 3521 => 13521, + 3522 => 13522, + 3523 => 13523, + 3524 => 13524, + 3525 => 13525, + 3526 => 13526, + 3527 => 13527, + 3528 => 13528, + 3529 => 13529, + 3530 => 13530, + 3531 => 13531, + 3532 => 13532, + 3533 => 13533, + 3534 => 13534, + 3535 => 13535, + 3536 => 13536, + 3537 => 13537, + 3538 => 13538, + 3539 => 13539, + 3540 => 13540, + 3541 => 13541, + 3542 => 13542, + 3543 => 13543, + 3544 => 13544, + 3545 => 13545, + 3546 => 13546, + 3547 => 13547, + 3548 => 13548, + 3549 => 13549, + 3550 => 13550, + 3551 => 13551, + 3552 => 13552, + 3553 => 13553, + 3554 => 13554, + 3555 => 13555, + 3556 => 13556, + 3557 => 13557, + 3558 => 13558, + 3559 => 13559, + 3560 => 13560, + 3561 => 13561, + 3562 => 13562, + 3563 => 13563, + 3564 => 13564, + 3565 => 13565, + 3566 => 13566, + 3567 => 13567, + 3568 => 13568, + 3569 => 13569, + 3570 => 13570, + 3571 => 13571, + 3572 => 13572, + 3573 => 13573, + 3574 => 13574, + 3575 => 13575, + 3576 => 13576, + 3577 => 13577, + 3578 => 13578, + 3579 => 13579, + 3580 => 13580, + 3581 => 13581, + 3582 => 13582, + 3583 => 13583, + 3584 => 13584, + 3585 => 13585, + 3586 => 13586, + 3587 => 13587, + 3588 => 13588, + 3589 => 13589, + 3590 => 13590, + 3591 => 13591, + 3592 => 13592, + 3593 => 13593, + 3594 => 13594, + 3595 => 13595, + 3596 => 13596, + 3597 => 13597, + 3598 => 13598, + 3599 => 13599, + 3600 => 13600, + 3601 => 13601, + 3602 => 13602, + 3603 => 13603, + 3604 => 13604, + 3605 => 13605, + 3606 => 13606, + 3607 => 13607, + 3608 => 13608, + 3609 => 13609, + 3610 => 13610, + 3611 => 13611, + 3612 => 13612, + 3613 => 13613, + 3614 => 13614, + 3615 => 13615, + 3616 => 13616, + 3617 => 13617, + 3618 => 13618, + 3619 => 13619, + 3620 => 13620, + 3621 => 13621, + 3622 => 13622, + 3623 => 13623, + 3624 => 13624, + 3625 => 13625, + 3626 => 13626, + 3627 => 13627, + 3628 => 13628, + 3629 => 13629, + 3630 => 13630, + 3631 => 13631, + 3632 => 13632, + 3633 => 13633, + 3634 => 13634, + 3635 => 13635, + 3636 => 13636, + 3637 => 13637, + 3638 => 13638, + 3639 => 13639, + 3640 => 13640, + 3641 => 13641, + 3642 => 13642, + 3643 => 13643, + 3644 => 13644, + 3645 => 13645, + 3646 => 13646, + 3647 => 13647, + 3648 => 13648, + 3649 => 13649, + 3650 => 13650, + 3651 => 13651, + 3652 => 13652, + 3653 => 13653, + 3654 => 13654, + 3655 => 13655, + 3656 => 13656, + 3657 => 13657, + 3658 => 13658, + 3659 => 13659, + 3660 => 13660, + 3661 => 13661, + 3662 => 13662, + 3663 => 13663, + 3664 => 13664, + 3665 => 13665, + 3666 => 13666, + 3667 => 13667, + 3668 => 13668, + 3669 => 13669, + 3670 => 13670, + 3671 => 13671, + 3672 => 13672, + 3673 => 13673, + 3674 => 13674, + 3675 => 13675, + 3676 => 13676, + 3677 => 13677, + 3678 => 13678, + 3679 => 13679, + 3680 => 13680, + 3681 => 13681, + 3682 => 13682, + 3683 => 13683, + 3684 => 13684, + 3685 => 13685, + 3686 => 13686, + 3687 => 13687, + 3688 => 13688, + 3689 => 13689, + 3690 => 13690, + 3691 => 13691, + 3692 => 13692, + 3693 => 13693, + 3694 => 13694, + 3695 => 13695, + 3696 => 13696, + 3697 => 13697, + 3698 => 13698, + 3699 => 13699, + 3700 => 13700, + 3701 => 13701, + 3702 => 13702, + 3703 => 13703, + 3704 => 13704, + 3705 => 13705, + 3706 => 13706, + 3707 => 13707, + 3708 => 13708, + 3709 => 13709, + 3710 => 13710, + 3711 => 13711, + 3712 => 13712, + 3713 => 13713, + 3714 => 13714, + 3715 => 13715, + 3716 => 13716, + 3717 => 13717, + 3718 => 13718, + 3719 => 13719, + 3720 => 13720, + 3721 => 13721, + 3722 => 13722, + 3723 => 13723, + 3724 => 13724, + 3725 => 13725, + 3726 => 13726, + 3727 => 13727, + 3728 => 13728, + 3729 => 13729, + 3730 => 13730, + 3731 => 13731, + 3732 => 13732, + 3733 => 13733, + 3734 => 13734, + 3735 => 13735, + 3736 => 13736, + 3737 => 13737, + 3738 => 13738, + 3739 => 13739, + 3740 => 13740, + 3741 => 13741, + 3742 => 13742, + 3743 => 13743, + 3744 => 13744, + 3745 => 13745, + 3746 => 13746, + 3747 => 13747, + 3748 => 13748, + 3749 => 13749, + 3750 => 13750, + 3751 => 13751, + 3752 => 13752, + 3753 => 13753, + 3754 => 13754, + 3755 => 13755, + 3756 => 13756, + 3757 => 13757, + 3758 => 13758, + 3759 => 13759, + 3760 => 13760, + 3761 => 13761, + 3762 => 13762, + 3763 => 13763, + 3764 => 13764, + 3765 => 13765, + 3766 => 13766, + 3767 => 13767, + 3768 => 13768, + 3769 => 13769, + 3770 => 13770, + 3771 => 13771, + 3772 => 13772, + 3773 => 13773, + 3774 => 13774, + 3775 => 13775, + 3776 => 13776, + 3777 => 13777, + 3778 => 13778, + 3779 => 13779, + 3780 => 13780, + 3781 => 13781, + 3782 => 13782, + 3783 => 13783, + 3784 => 13784, + 3785 => 13785, + 3786 => 13786, + 3787 => 13787, + 3788 => 13788, + 3789 => 13789, + 3790 => 13790, + 3791 => 13791, + 3792 => 13792, + 3793 => 13793, + 3794 => 13794, + 3795 => 13795, + 3796 => 13796, + 3797 => 13797, + 3798 => 13798, + 3799 => 13799, + 3800 => 13800, + 3801 => 13801, + 3802 => 13802, + 3803 => 13803, + 3804 => 13804, + 3805 => 13805, + 3806 => 13806, + 3807 => 13807, + 3808 => 13808, + 3809 => 13809, + 3810 => 13810, + 3811 => 13811, + 3812 => 13812, + 3813 => 13813, + 3814 => 13814, + 3815 => 13815, + 3816 => 13816, + 3817 => 13817, + 3818 => 13818, + 3819 => 13819, + 3820 => 13820, + 3821 => 13821, + 3822 => 13822, + 3823 => 13823, + 3824 => 13824, + 3825 => 13825, + 3826 => 13826, + 3827 => 13827, + 3828 => 13828, + 3829 => 13829, + 3830 => 13830, + 3831 => 13831, + 3832 => 13832, + 3833 => 13833, + 3834 => 13834, + 3835 => 13835, + 3836 => 13836, + 3837 => 13837, + 3838 => 13838, + 3839 => 13839, + 3840 => 13840, + 3841 => 13841, + 3842 => 13842, + 3843 => 13843, + 3844 => 13844, + 3845 => 13845, + 3846 => 13846, + 3847 => 13847, + 3848 => 13848, + 3849 => 13849, + 3850 => 13850, + 3851 => 13851, + 3852 => 13852, + 3853 => 13853, + 3854 => 13854, + 3855 => 13855, + 3856 => 13856, + 3857 => 13857, + 3858 => 13858, + 3859 => 13859, + 3860 => 13860, + 3861 => 13861, + 3862 => 13862, + 3863 => 13863, + 3864 => 13864, + 3865 => 13865, + 3866 => 13866, + 3867 => 13867, + 3868 => 13868, + 3869 => 13869, + 3870 => 13870, + 3871 => 13871, + 3872 => 13872, + 3873 => 13873, + 3874 => 13874, + 3875 => 13875, + 3876 => 13876, + 3877 => 13877, + 3878 => 13878, + 3879 => 13879, + 3880 => 13880, + 3881 => 13881, + 3882 => 13882, + 3883 => 13883, + 3884 => 13884, + 3885 => 13885, + 3886 => 13886, + 3887 => 13887, + 3888 => 13888, + 3889 => 13889, + 3890 => 13890, + 3891 => 13891, + 3892 => 13892, + 3893 => 13893, + 3894 => 13894, + 3895 => 13895, + 3896 => 13896, + 3897 => 13897, + 3898 => 13898, + 3899 => 13899, + 3900 => 13900, + 3901 => 13901, + 3902 => 13902, + 3903 => 13903, + 3904 => 13904, + 3905 => 13905, + 3906 => 13906, + 3907 => 13907, + 3908 => 13908, + 3909 => 13909, + 3910 => 13910, + 3911 => 13911, + 3912 => 13912, + 3913 => 13913, + 3914 => 13914, + 3915 => 13915, + 3916 => 13916, + 3917 => 13917, + 3918 => 13918, + 3919 => 13919, + 3920 => 13920, + 3921 => 13921, + 3922 => 13922, + 3923 => 13923, + 3924 => 13924, + 3925 => 13925, + 3926 => 13926, + 3927 => 13927, + 3928 => 13928, + 3929 => 13929, + 3930 => 13930, + 3931 => 13931, + 3932 => 13932, + 3933 => 13933, + 3934 => 13934, + 3935 => 13935, + 3936 => 13936, + 3937 => 13937, + 3938 => 13938, + 3939 => 13939, + 3940 => 13940, + 3941 => 13941, + 3942 => 13942, + 3943 => 13943, + 3944 => 13944, + 3945 => 13945, + 3946 => 13946, + 3947 => 13947, + 3948 => 13948, + 3949 => 13949, + 3950 => 13950, + 3951 => 13951, + 3952 => 13952, + 3953 => 13953, + 3954 => 13954, + 3955 => 13955, + 3956 => 13956, + 3957 => 13957, + 3958 => 13958, + 3959 => 13959, + 3960 => 13960, + 3961 => 13961, + 3962 => 13962, + 3963 => 13963, + 3964 => 13964, + 3965 => 13965, + 3966 => 13966, + 3967 => 13967, + 3968 => 13968, + 3969 => 13969, + 3970 => 13970, + 3971 => 13971, + 3972 => 13972, + 3973 => 13973, + 3974 => 13974, + 3975 => 13975, + 3976 => 13976, + 3977 => 13977, + 3978 => 13978, + 3979 => 13979, + 3980 => 13980, + 3981 => 13981, + 3982 => 13982, + 3983 => 13983, + 3984 => 13984, + 3985 => 13985, + 3986 => 13986, + 3987 => 13987, + 3988 => 13988, + 3989 => 13989, + 3990 => 13990, + 3991 => 13991, + 3992 => 13992, + 3993 => 13993, + 3994 => 13994, + 3995 => 13995, + 3996 => 13996, + 3997 => 13997, + 3998 => 13998, + 3999 => 13999, + 4000 => 14000, + 4001 => 14001, + 4002 => 14002, + 4003 => 14003, + 4004 => 14004, + 4005 => 14005, + 4006 => 14006, + 4007 => 14007, + 4008 => 14008, + 4009 => 14009, + 4010 => 14010, + 4011 => 14011, + 4012 => 14012, + 4013 => 14013, + 4014 => 14014, + 4015 => 14015, + 4016 => 14016, + 4017 => 14017, + 4018 => 14018, + 4019 => 14019, + 4020 => 14020, + 4021 => 14021, + 4022 => 14022, + 4023 => 14023, + 4024 => 14024, + 4025 => 14025, + 4026 => 14026, + 4027 => 14027, + 4028 => 14028, + 4029 => 14029, + 4030 => 14030, + 4031 => 14031, + 4032 => 14032, + 4033 => 14033, + 4034 => 14034, + 4035 => 14035, + 4036 => 14036, + 4037 => 14037, + 4038 => 14038, + 4039 => 14039, + 4040 => 14040, + 4041 => 14041, + 4042 => 14042, + 4043 => 14043, + 4044 => 14044, + 4045 => 14045, + 4046 => 14046, + 4047 => 14047, + 4048 => 14048, + 4049 => 14049, + 4050 => 14050, + 4051 => 14051, + 4052 => 14052, + 4053 => 14053, + 4054 => 14054, + 4055 => 14055, + 4056 => 14056, + 4057 => 14057, + 4058 => 14058, + 4059 => 14059, + 4060 => 14060, + 4061 => 14061, + 4062 => 14062, + 4063 => 14063, + 4064 => 14064, + 4065 => 14065, + 4066 => 14066, + 4067 => 14067, + 4068 => 14068, + 4069 => 14069, + 4070 => 14070, + 4071 => 14071, + 4072 => 14072, + 4073 => 14073, + 4074 => 14074, + 4075 => 14075, + 4076 => 14076, + 4077 => 14077, + 4078 => 14078, + 4079 => 14079, + 4080 => 14080, + 4081 => 14081, + 4082 => 14082, + 4083 => 14083, + 4084 => 14084, + 4085 => 14085, + 4086 => 14086, + 4087 => 14087, + 4088 => 14088, + 4089 => 14089, + 4090 => 14090, + 4091 => 14091, + 4092 => 14092, + 4093 => 14093, + 4094 => 14094, + 4095 => 14095, + 4096 => 14096, + 4097 => 14097, + 4098 => 14098, + 4099 => 14099, + 4100 => 14100, + 4101 => 14101, + 4102 => 14102, + 4103 => 14103, + 4104 => 14104, + 4105 => 14105, + 4106 => 14106, + 4107 => 14107, + 4108 => 14108, + 4109 => 14109, + 4110 => 14110, + 4111 => 14111, + 4112 => 14112, + 4113 => 14113, + 4114 => 14114, + 4115 => 14115, + 4116 => 14116, + 4117 => 14117, + 4118 => 14118, + 4119 => 14119, + 4120 => 14120, + 4121 => 14121, + 4122 => 14122, + 4123 => 14123, + 4124 => 14124, + 4125 => 14125, + 4126 => 14126, + 4127 => 14127, + 4128 => 14128, + 4129 => 14129, + 4130 => 14130, + 4131 => 14131, + 4132 => 14132, + 4133 => 14133, + 4134 => 14134, + 4135 => 14135, + 4136 => 14136, + 4137 => 14137, + 4138 => 14138, + 4139 => 14139, + 4140 => 14140, + 4141 => 14141, + 4142 => 14142, + 4143 => 14143, + 4144 => 14144, + 4145 => 14145, + 4146 => 14146, + 4147 => 14147, + 4148 => 14148, + 4149 => 14149, + 4150 => 14150, + 4151 => 14151, + 4152 => 14152, + 4153 => 14153, + 4154 => 14154, + 4155 => 14155, + 4156 => 14156, + 4157 => 14157, + 4158 => 14158, + 4159 => 14159, + 4160 => 14160, + 4161 => 14161, + 4162 => 14162, + 4163 => 14163, + 4164 => 14164, + 4165 => 14165, + 4166 => 14166, + 4167 => 14167, + 4168 => 14168, + 4169 => 14169, + 4170 => 14170, + 4171 => 14171, + 4172 => 14172, + 4173 => 14173, + 4174 => 14174, + 4175 => 14175, + 4176 => 14176, + 4177 => 14177, + 4178 => 14178, + 4179 => 14179, + 4180 => 14180, + 4181 => 14181, + 4182 => 14182, + 4183 => 14183, + 4184 => 14184, + 4185 => 14185, + 4186 => 14186, + 4187 => 14187, + 4188 => 14188, + 4189 => 14189, + 4190 => 14190, + 4191 => 14191, + 4192 => 14192, + 4193 => 14193, + 4194 => 14194, + 4195 => 14195, + 4196 => 14196, + 4197 => 14197, + 4198 => 14198, + 4199 => 14199, + 4200 => 14200, + 4201 => 14201, + 4202 => 14202, + 4203 => 14203, + 4204 => 14204, + 4205 => 14205, + 4206 => 14206, + 4207 => 14207, + 4208 => 14208, + 4209 => 14209, + 4210 => 14210, + 4211 => 14211, + 4212 => 14212, + 4213 => 14213, + 4214 => 14214, + 4215 => 14215, + 4216 => 14216, + 4217 => 14217, + 4218 => 14218, + 4219 => 14219, + 4220 => 14220, + 4221 => 14221, + 4222 => 14222, + 4223 => 14223, + 4224 => 14224, + 4225 => 14225, + 4226 => 14226, + 4227 => 14227, + 4228 => 14228, + 4229 => 14229, + 4230 => 14230, + 4231 => 14231, + 4232 => 14232, + 4233 => 14233, + 4234 => 14234, + 4235 => 14235, + 4236 => 14236, + 4237 => 14237, + 4238 => 14238, + 4239 => 14239, + 4240 => 14240, + 4241 => 14241, + 4242 => 14242, + 4243 => 14243, + 4244 => 14244, + 4245 => 14245, + 4246 => 14246, + 4247 => 14247, + 4248 => 14248, + 4249 => 14249, + 4250 => 14250, + 4251 => 14251, + 4252 => 14252, + 4253 => 14253, + 4254 => 14254, + 4255 => 14255, + 4256 => 14256, + 4257 => 14257, + 4258 => 14258, + 4259 => 14259, + 4260 => 14260, + 4261 => 14261, + 4262 => 14262, + 4263 => 14263, + 4264 => 14264, + 4265 => 14265, + 4266 => 14266, + 4267 => 14267, + 4268 => 14268, + 4269 => 14269, + 4270 => 14270, + 4271 => 14271, + 4272 => 14272, + 4273 => 14273, + 4274 => 14274, + 4275 => 14275, + 4276 => 14276, + 4277 => 14277, + 4278 => 14278, + 4279 => 14279, + 4280 => 14280, + 4281 => 14281, + 4282 => 14282, + 4283 => 14283, + 4284 => 14284, + 4285 => 14285, + 4286 => 14286, + 4287 => 14287, + 4288 => 14288, + 4289 => 14289, + 4290 => 14290, + 4291 => 14291, + 4292 => 14292, + 4293 => 14293, + 4294 => 14294, + 4295 => 14295, + 4296 => 14296, + 4297 => 14297, + 4298 => 14298, + 4299 => 14299, + 4300 => 14300, + 4301 => 14301, + 4302 => 14302, + 4303 => 14303, + 4304 => 14304, + 4305 => 14305, + 4306 => 14306, + 4307 => 14307, + 4308 => 14308, + 4309 => 14309, + 4310 => 14310, + 4311 => 14311, + 4312 => 14312, + 4313 => 14313, + 4314 => 14314, + 4315 => 14315, + 4316 => 14316, + 4317 => 14317, + 4318 => 14318, + 4319 => 14319, + 4320 => 14320, + 4321 => 14321, + 4322 => 14322, + 4323 => 14323, + 4324 => 14324, + 4325 => 14325, + 4326 => 14326, + 4327 => 14327, + 4328 => 14328, + 4329 => 14329, + 4330 => 14330, + 4331 => 14331, + 4332 => 14332, + 4333 => 14333, + 4334 => 14334, + 4335 => 14335, + 4336 => 14336, + 4337 => 14337, + 4338 => 14338, + 4339 => 14339, + 4340 => 14340, + 4341 => 14341, + 4342 => 14342, + 4343 => 14343, + 4344 => 14344, + 4345 => 14345, + 4346 => 14346, + 4347 => 14347, + 4348 => 14348, + 4349 => 14349, + 4350 => 14350, + 4351 => 14351, + 4352 => 14352, + 4353 => 14353, + 4354 => 14354, + 4355 => 14355, + 4356 => 14356, + 4357 => 14357, + 4358 => 14358, + 4359 => 14359, + 4360 => 14360, + 4361 => 14361, + 4362 => 14362, + 4363 => 14363, + 4364 => 14364, + 4365 => 14365, + 4366 => 14366, + 4367 => 14367, + 4368 => 14368, + 4369 => 14369, + 4370 => 14370, + 4371 => 14371, + 4372 => 14372, + 4373 => 14373, + 4374 => 14374, + 4375 => 14375, + 4376 => 14376, + 4377 => 14377, + 4378 => 14378, + 4379 => 14379, + 4380 => 14380, + 4381 => 14381, + 4382 => 14382, + 4383 => 14383, + 4384 => 14384, + 4385 => 14385, + 4386 => 14386, + 4387 => 14387, + 4388 => 14388, + 4389 => 14389, + 4390 => 14390, + 4391 => 14391, + 4392 => 14392, + 4393 => 14393, + 4394 => 14394, + 4395 => 14395, + 4396 => 14396, + 4397 => 14397, + 4398 => 14398, + 4399 => 14399, + 4400 => 14400, + 4401 => 14401, + 4402 => 14402, + 4403 => 14403, + 4404 => 14404, + 4405 => 14405, + 4406 => 14406, + 4407 => 14407, + 4408 => 14408, + 4409 => 14409, + 4410 => 14410, + 4411 => 14411, + 4412 => 14412, + 4413 => 14413, + 4414 => 14414, + 4415 => 14415, + 4416 => 14416, + 4417 => 14417, + 4418 => 14418, + 4419 => 14419, + 4420 => 14420, + 4421 => 14421, + 4422 => 14422, + 4423 => 14423, + 4424 => 14424, + 4425 => 14425, + 4426 => 14426, + 4427 => 14427, + 4428 => 14428, + 4429 => 14429, + 4430 => 14430, + 4431 => 14431, + 4432 => 14432, + 4433 => 14433, + 4434 => 14434, + 4435 => 14435, + 4436 => 14436, + 4437 => 14437, + 4438 => 14438, + 4439 => 14439, + 4440 => 14440, + 4441 => 14441, + 4442 => 14442, + 4443 => 14443, + 4444 => 14444, + 4445 => 14445, + 4446 => 14446, + 4447 => 14447, + 4448 => 14448, + 4449 => 14449, + 4450 => 14450, + 4451 => 14451, + 4452 => 14452, + 4453 => 14453, + 4454 => 14454, + 4455 => 14455, + 4456 => 14456, + 4457 => 14457, + 4458 => 14458, + 4459 => 14459, + 4460 => 14460, + 4461 => 14461, + 4462 => 14462, + 4463 => 14463, + 4464 => 14464, + 4465 => 14465, + 4466 => 14466, + 4467 => 14467, + 4468 => 14468, + 4469 => 14469, + 4470 => 14470, + 4471 => 14471, + 4472 => 14472, + 4473 => 14473, + 4474 => 14474, + 4475 => 14475, + 4476 => 14476, + 4477 => 14477, + 4478 => 14478, + 4479 => 14479, + 4480 => 14480, + 4481 => 14481, + 4482 => 14482, + 4483 => 14483, + 4484 => 14484, + 4485 => 14485, + 4486 => 14486, + 4487 => 14487, + 4488 => 14488, + 4489 => 14489, + 4490 => 14490, + 4491 => 14491, + 4492 => 14492, + 4493 => 14493, + 4494 => 14494, + 4495 => 14495, + 4496 => 14496, + 4497 => 14497, + 4498 => 14498, + 4499 => 14499, + 4500 => 14500, + 4501 => 14501, + 4502 => 14502, + 4503 => 14503, + 4504 => 14504, + 4505 => 14505, + 4506 => 14506, + 4507 => 14507, + 4508 => 14508, + 4509 => 14509, + 4510 => 14510, + 4511 => 14511, + 4512 => 14512, + 4513 => 14513, + 4514 => 14514, + 4515 => 14515, + 4516 => 14516, + 4517 => 14517, + 4518 => 14518, + 4519 => 14519, + 4520 => 14520, + 4521 => 14521, + 4522 => 14522, + 4523 => 14523, + 4524 => 14524, + 4525 => 14525, + 4526 => 14526, + 4527 => 14527, + 4528 => 14528, + 4529 => 14529, + 4530 => 14530, + 4531 => 14531, + 4532 => 14532, + 4533 => 14533, + 4534 => 14534, + 4535 => 14535, + 4536 => 14536, + 4537 => 14537, + 4538 => 14538, + 4539 => 14539, + 4540 => 14540, + 4541 => 14541, + 4542 => 14542, + 4543 => 14543, + 4544 => 14544, + 4545 => 14545, + 4546 => 14546, + 4547 => 14547, + 4548 => 14548, + 4549 => 14549, + 4550 => 14550, + 4551 => 14551, + 4552 => 14552, + 4553 => 14553, + 4554 => 14554, + 4555 => 14555, + 4556 => 14556, + 4557 => 14557, + 4558 => 14558, + 4559 => 14559, + 4560 => 14560, + 4561 => 14561, + 4562 => 14562, + 4563 => 14563, + 4564 => 14564, + 4565 => 14565, + 4566 => 14566, + 4567 => 14567, + 4568 => 14568, + 4569 => 14569, + 4570 => 14570, + 4571 => 14571, + 4572 => 14572, + 4573 => 14573, + 4574 => 14574, + 4575 => 14575, + 4576 => 14576, + 4577 => 14577, + 4578 => 14578, + 4579 => 14579, + 4580 => 14580, + 4581 => 14581, + 4582 => 14582, + 4583 => 14583, + 4584 => 14584, + 4585 => 14585, + 4586 => 14586, + 4587 => 14587, + 4588 => 14588, + 4589 => 14589, + 4590 => 14590, + 4591 => 14591, + 4592 => 14592, + 4593 => 14593, + 4594 => 14594, + 4595 => 14595, + 4596 => 14596, + 4597 => 14597, + 4598 => 14598, + 4599 => 14599, + 4600 => 14600, + 4601 => 14601, + 4602 => 14602, + 4603 => 14603, + 4604 => 14604, + 4605 => 14605, + 4606 => 14606, + 4607 => 14607, + 4608 => 14608, + 4609 => 14609, + 4610 => 14610, + 4611 => 14611, + 4612 => 14612, + 4613 => 14613, + 4614 => 14614, + 4615 => 14615, + 4616 => 14616, + 4617 => 14617, + 4618 => 14618, + 4619 => 14619, + 4620 => 14620, + 4621 => 14621, + 4622 => 14622, + 4623 => 14623, + 4624 => 14624, + 4625 => 14625, + 4626 => 14626, + 4627 => 14627, + 4628 => 14628, + 4629 => 14629, + 4630 => 14630, + 4631 => 14631, + 4632 => 14632, + 4633 => 14633, + 4634 => 14634, + 4635 => 14635, + 4636 => 14636, + 4637 => 14637, + 4638 => 14638, + 4639 => 14639, + 4640 => 14640, + 4641 => 14641, + 4642 => 14642, + 4643 => 14643, + 4644 => 14644, + 4645 => 14645, + 4646 => 14646, + 4647 => 14647, + 4648 => 14648, + 4649 => 14649, + 4650 => 14650, + 4651 => 14651, + 4652 => 14652, + 4653 => 14653, + 4654 => 14654, + 4655 => 14655, + 4656 => 14656, + 4657 => 14657, + 4658 => 14658, + 4659 => 14659, + 4660 => 14660, + 4661 => 14661, + 4662 => 14662, + 4663 => 14663, + 4664 => 14664, + 4665 => 14665, + 4666 => 14666, + 4667 => 14667, + 4668 => 14668, + 4669 => 14669, + 4670 => 14670, + 4671 => 14671, + 4672 => 14672, + 4673 => 14673, + 4674 => 14674, + 4675 => 14675, + 4676 => 14676, + 4677 => 14677, + 4678 => 14678, + 4679 => 14679, + 4680 => 14680, + 4681 => 14681, + 4682 => 14682, + 4683 => 14683, + 4684 => 14684, + 4685 => 14685, + 4686 => 14686, + 4687 => 14687, + 4688 => 14688, + 4689 => 14689, + 4690 => 14690, + 4691 => 14691, + 4692 => 14692, + 4693 => 14693, + 4694 => 14694, + 4695 => 14695, + 4696 => 14696, + 4697 => 14697, + 4698 => 14698, + 4699 => 14699, + 4700 => 14700, + 4701 => 14701, + 4702 => 14702, + 4703 => 14703, + 4704 => 14704, + 4705 => 14705, + 4706 => 14706, + 4707 => 14707, + 4708 => 14708, + 4709 => 14709, + 4710 => 14710, + 4711 => 14711, + 4712 => 14712, + 4713 => 14713, + 4714 => 14714, + 4715 => 14715, + 4716 => 14716, + 4717 => 14717, + 4718 => 14718, + 4719 => 14719, + 4720 => 14720, + 4721 => 14721, + 4722 => 14722, + 4723 => 14723, + 4724 => 14724, + 4725 => 14725, + 4726 => 14726, + 4727 => 14727, + 4728 => 14728, + 4729 => 14729, + 4730 => 14730, + 4731 => 14731, + 4732 => 14732, + 4733 => 14733, + 4734 => 14734, + 4735 => 14735, + 4736 => 14736, + 4737 => 14737, + 4738 => 14738, + 4739 => 14739, + 4740 => 14740, + 4741 => 14741, + 4742 => 14742, + 4743 => 14743, + 4744 => 14744, + 4745 => 14745, + 4746 => 14746, + 4747 => 14747, + 4748 => 14748, + 4749 => 14749, + 4750 => 14750, + 4751 => 14751, + 4752 => 14752, + 4753 => 14753, + 4754 => 14754, + 4755 => 14755, + 4756 => 14756, + 4757 => 14757, + 4758 => 14758, + 4759 => 14759, + 4760 => 14760, + 4761 => 14761, + 4762 => 14762, + 4763 => 14763, + 4764 => 14764, + 4765 => 14765, + 4766 => 14766, + 4767 => 14767, + 4768 => 14768, + 4769 => 14769, + 4770 => 14770, + 4771 => 14771, + 4772 => 14772, + 4773 => 14773, + 4774 => 14774, + 4775 => 14775, + 4776 => 14776, + 4777 => 14777, + 4778 => 14778, + 4779 => 14779, + 4780 => 14780, + 4781 => 14781, + 4782 => 14782, + 4783 => 14783, + 4784 => 14784, + 4785 => 14785, + 4786 => 14786, + 4787 => 14787, + 4788 => 14788, + 4789 => 14789, + 4790 => 14790, + 4791 => 14791, + 4792 => 14792, + 4793 => 14793, + 4794 => 14794, + 4795 => 14795, + 4796 => 14796, + 4797 => 14797, + 4798 => 14798, + 4799 => 14799, + 4800 => 14800, + 4801 => 14801, + 4802 => 14802, + 4803 => 14803, + 4804 => 14804, + 4805 => 14805, + 4806 => 14806, + 4807 => 14807, + 4808 => 14808, + 4809 => 14809, + 4810 => 14810, + 4811 => 14811, + 4812 => 14812, + 4813 => 14813, + 4814 => 14814, + 4815 => 14815, + 4816 => 14816, + 4817 => 14817, + 4818 => 14818, + 4819 => 14819, + 4820 => 14820, + 4821 => 14821, + 4822 => 14822, + 4823 => 14823, + 4824 => 14824, + 4825 => 14825, + 4826 => 14826, + 4827 => 14827, + 4828 => 14828, + 4829 => 14829, + 4830 => 14830, + 4831 => 14831, + 4832 => 14832, + 4833 => 14833, + 4834 => 14834, + 4835 => 14835, + 4836 => 14836, + 4837 => 14837, + 4838 => 14838, + 4839 => 14839, + 4840 => 14840, + 4841 => 14841, + 4842 => 14842, + 4843 => 14843, + 4844 => 14844, + 4845 => 14845, + 4846 => 14846, + 4847 => 14847, + 4848 => 14848, + 4849 => 14849, + 4850 => 14850, + 4851 => 14851, + 4852 => 14852, + 4853 => 14853, + 4854 => 14854, + 4855 => 14855, + 4856 => 14856, + 4857 => 14857, + 4858 => 14858, + 4859 => 14859, + 4860 => 14860, + 4861 => 14861, + 4862 => 14862, + 4863 => 14863, + 4864 => 14864, + 4865 => 14865, + 4866 => 14866, + 4867 => 14867, + 4868 => 14868, + 4869 => 14869, + 4870 => 14870, + 4871 => 14871, + 4872 => 14872, + 4873 => 14873, + 4874 => 14874, + 4875 => 14875, + 4876 => 14876, + 4877 => 14877, + 4878 => 14878, + 4879 => 14879, + 4880 => 14880, + 4881 => 14881, + 4882 => 14882, + 4883 => 14883, + 4884 => 14884, + 4885 => 14885, + 4886 => 14886, + 4887 => 14887, + 4888 => 14888, + 4889 => 14889, + 4890 => 14890, + 4891 => 14891, + 4892 => 14892, + 4893 => 14893, + 4894 => 14894, + 4895 => 14895, + 4896 => 14896, + 4897 => 14897, + 4898 => 14898, + 4899 => 14899, + 4900 => 14900, + 4901 => 14901, + 4902 => 14902, + 4903 => 14903, + 4904 => 14904, + 4905 => 14905, + 4906 => 14906, + 4907 => 14907, + 4908 => 14908, + 4909 => 14909, + 4910 => 14910, + 4911 => 14911, + 4912 => 14912, + 4913 => 14913, + 4914 => 14914, + 4915 => 14915, + 4916 => 14916, + 4917 => 14917, + 4918 => 14918, + 4919 => 14919, + 4920 => 14920, + 4921 => 14921, + 4922 => 14922, + 4923 => 14923, + 4924 => 14924, + 4925 => 14925, + 4926 => 14926, + 4927 => 14927, + 4928 => 14928, + 4929 => 14929, + 4930 => 14930, + 4931 => 14931, + 4932 => 14932, + 4933 => 14933, + 4934 => 14934, + 4935 => 14935, + 4936 => 14936, + 4937 => 14937, + 4938 => 14938, + 4939 => 14939, + 4940 => 14940, + 4941 => 14941, + 4942 => 14942, + 4943 => 14943, + 4944 => 14944, + 4945 => 14945, + 4946 => 14946, + 4947 => 14947, + 4948 => 14948, + 4949 => 14949, + 4950 => 14950, + 4951 => 14951, + 4952 => 14952, + 4953 => 14953, + 4954 => 14954, + 4955 => 14955, + 4956 => 14956, + 4957 => 14957, + 4958 => 14958, + 4959 => 14959, + 4960 => 14960, + 4961 => 14961, + 4962 => 14962, + 4963 => 14963, + 4964 => 14964, + 4965 => 14965, + 4966 => 14966, + 4967 => 14967, + 4968 => 14968, + 4969 => 14969, + 4970 => 14970, + 4971 => 14971, + 4972 => 14972, + 4973 => 14973, + 4974 => 14974, + 4975 => 14975, + 4976 => 14976, + 4977 => 14977, + 4978 => 14978, + 4979 => 14979, + 4980 => 14980, + 4981 => 14981, + 4982 => 14982, + 4983 => 14983, + 4984 => 14984, + 4985 => 14985, + 4986 => 14986, + 4987 => 14987, + 4988 => 14988, + 4989 => 14989, + 4990 => 14990, + 4991 => 14991, + 4992 => 14992, + 4993 => 14993, + 4994 => 14994, + 4995 => 14995, + 4996 => 14996, + 4997 => 14997, + 4998 => 14998, + 4999 => 14999, + 5000 => 15000, + 5001 => 15001, + 5002 => 15002, + 5003 => 15003, + 5004 => 15004, + 5005 => 15005, + 5006 => 15006, + 5007 => 15007, + 5008 => 15008, + 5009 => 15009, + 5010 => 15010, + 5011 => 15011, + 5012 => 15012, + 5013 => 15013, + 5014 => 15014, + 5015 => 15015, + 5016 => 15016, + 5017 => 15017, + 5018 => 15018, + 5019 => 15019, + 5020 => 15020, + 5021 => 15021, + 5022 => 15022, + 5023 => 15023, + 5024 => 15024, + 5025 => 15025, + 5026 => 15026, + 5027 => 15027, + 5028 => 15028, + 5029 => 15029, + 5030 => 15030, + 5031 => 15031, + 5032 => 15032, + 5033 => 15033, + 5034 => 15034, + 5035 => 15035, + 5036 => 15036, + 5037 => 15037, + 5038 => 15038, + 5039 => 15039, + 5040 => 15040, + 5041 => 15041, + 5042 => 15042, + 5043 => 15043, + 5044 => 15044, + 5045 => 15045, + 5046 => 15046, + 5047 => 15047, + 5048 => 15048, + 5049 => 15049, + 5050 => 15050, + 5051 => 15051, + 5052 => 15052, + 5053 => 15053, + 5054 => 15054, + 5055 => 15055, + 5056 => 15056, + 5057 => 15057, + 5058 => 15058, + 5059 => 15059, + 5060 => 15060, + 5061 => 15061, + 5062 => 15062, + 5063 => 15063, + 5064 => 15064, + 5065 => 15065, + 5066 => 15066, + 5067 => 15067, + 5068 => 15068, + 5069 => 15069, + 5070 => 15070, + 5071 => 15071, + 5072 => 15072, + 5073 => 15073, + 5074 => 15074, + 5075 => 15075, + 5076 => 15076, + 5077 => 15077, + 5078 => 15078, + 5079 => 15079, + 5080 => 15080, + 5081 => 15081, + 5082 => 15082, + 5083 => 15083, + 5084 => 15084, + 5085 => 15085, + 5086 => 15086, + 5087 => 15087, + 5088 => 15088, + 5089 => 15089, + 5090 => 15090, + 5091 => 15091, + 5092 => 15092, + 5093 => 15093, + 5094 => 15094, + 5095 => 15095, + 5096 => 15096, + 5097 => 15097, + 5098 => 15098, + 5099 => 15099, + 5100 => 15100, + 5101 => 15101, + 5102 => 15102, + 5103 => 15103, + 5104 => 15104, + 5105 => 15105, + 5106 => 15106, + 5107 => 15107, + 5108 => 15108, + 5109 => 15109, + 5110 => 15110, + 5111 => 15111, + 5112 => 15112, + 5113 => 15113, + 5114 => 15114, + 5115 => 15115, + 5116 => 15116, + 5117 => 15117, + 5118 => 15118, + 5119 => 15119, + 5120 => 15120, + 5121 => 15121, + 5122 => 15122, + 5123 => 15123, + 5124 => 15124, + 5125 => 15125, + 5126 => 15126, + 5127 => 15127, + 5128 => 15128, + 5129 => 15129, + 5130 => 15130, + 5131 => 15131, + 5132 => 15132, + 5133 => 15133, + 5134 => 15134, + 5135 => 15135, + 5136 => 15136, + 5137 => 15137, + 5138 => 15138, + 5139 => 15139, + 5140 => 15140, + 5141 => 15141, + 5142 => 15142, + 5143 => 15143, + 5144 => 15144, + 5145 => 15145, + 5146 => 15146, + 5147 => 15147, + 5148 => 15148, + 5149 => 15149, + 5150 => 15150, + 5151 => 15151, + 5152 => 15152, + 5153 => 15153, + 5154 => 15154, + 5155 => 15155, + 5156 => 15156, + 5157 => 15157, + 5158 => 15158, + 5159 => 15159, + 5160 => 15160, + 5161 => 15161, + 5162 => 15162, + 5163 => 15163, + 5164 => 15164, + 5165 => 15165, + 5166 => 15166, + 5167 => 15167, + 5168 => 15168, + 5169 => 15169, + 5170 => 15170, + 5171 => 15171, + 5172 => 15172, + 5173 => 15173, + 5174 => 15174, + 5175 => 15175, + 5176 => 15176, + 5177 => 15177, + 5178 => 15178, + 5179 => 15179, + 5180 => 15180, + 5181 => 15181, + 5182 => 15182, + 5183 => 15183, + 5184 => 15184, + 5185 => 15185, + 5186 => 15186, + 5187 => 15187, + 5188 => 15188, + 5189 => 15189, + 5190 => 15190, + 5191 => 15191, + 5192 => 15192, + 5193 => 15193, + 5194 => 15194, + 5195 => 15195, + 5196 => 15196, + 5197 => 15197, + 5198 => 15198, + 5199 => 15199, + 5200 => 15200, + 5201 => 15201, + 5202 => 15202, + 5203 => 15203, + 5204 => 15204, + 5205 => 15205, + 5206 => 15206, + 5207 => 15207, + 5208 => 15208, + 5209 => 15209, + 5210 => 15210, + 5211 => 15211, + 5212 => 15212, + 5213 => 15213, + 5214 => 15214, + 5215 => 15215, + 5216 => 15216, + 5217 => 15217, + 5218 => 15218, + 5219 => 15219, + 5220 => 15220, + 5221 => 15221, + 5222 => 15222, + 5223 => 15223, + 5224 => 15224, + 5225 => 15225, + 5226 => 15226, + 5227 => 15227, + 5228 => 15228, + 5229 => 15229, + 5230 => 15230, + 5231 => 15231, + 5232 => 15232, + 5233 => 15233, + 5234 => 15234, + 5235 => 15235, + 5236 => 15236, + 5237 => 15237, + 5238 => 15238, + 5239 => 15239, + 5240 => 15240, + 5241 => 15241, + 5242 => 15242, + 5243 => 15243, + 5244 => 15244, + 5245 => 15245, + 5246 => 15246, + 5247 => 15247, + 5248 => 15248, + 5249 => 15249, + 5250 => 15250, + 5251 => 15251, + 5252 => 15252, + 5253 => 15253, + 5254 => 15254, + 5255 => 15255, + 5256 => 15256, + 5257 => 15257, + 5258 => 15258, + 5259 => 15259, + 5260 => 15260, + 5261 => 15261, + 5262 => 15262, + 5263 => 15263, + 5264 => 15264, + 5265 => 15265, + 5266 => 15266, + 5267 => 15267, + 5268 => 15268, + 5269 => 15269, + 5270 => 15270, + 5271 => 15271, + 5272 => 15272, + 5273 => 15273, + 5274 => 15274, + 5275 => 15275, + 5276 => 15276, + 5277 => 15277, + 5278 => 15278, + 5279 => 15279, + 5280 => 15280, + 5281 => 15281, + 5282 => 15282, + 5283 => 15283, + 5284 => 15284, + 5285 => 15285, + 5286 => 15286, + 5287 => 15287, + 5288 => 15288, + 5289 => 15289, + 5290 => 15290, + 5291 => 15291, + 5292 => 15292, + 5293 => 15293, + 5294 => 15294, + 5295 => 15295, + 5296 => 15296, + 5297 => 15297, + 5298 => 15298, + 5299 => 15299, + 5300 => 15300, + 5301 => 15301, + 5302 => 15302, + 5303 => 15303, + 5304 => 15304, + 5305 => 15305, + 5306 => 15306, + 5307 => 15307, + 5308 => 15308, + 5309 => 15309, + 5310 => 15310, + 5311 => 15311, + 5312 => 15312, + 5313 => 15313, + 5314 => 15314, + 5315 => 15315, + 5316 => 15316, + 5317 => 15317, + 5318 => 15318, + 5319 => 15319, + 5320 => 15320, + 5321 => 15321, + 5322 => 15322, + 5323 => 15323, + 5324 => 15324, + 5325 => 15325, + 5326 => 15326, + 5327 => 15327, + 5328 => 15328, + 5329 => 15329, + 5330 => 15330, + 5331 => 15331, + 5332 => 15332, + 5333 => 15333, + 5334 => 15334, + 5335 => 15335, + 5336 => 15336, + 5337 => 15337, + 5338 => 15338, + 5339 => 15339, + 5340 => 15340, + 5341 => 15341, + 5342 => 15342, + 5343 => 15343, + 5344 => 15344, + 5345 => 15345, + 5346 => 15346, + 5347 => 15347, + 5348 => 15348, + 5349 => 15349, + 5350 => 15350, + 5351 => 15351, + 5352 => 15352, + 5353 => 15353, + 5354 => 15354, + 5355 => 15355, + 5356 => 15356, + 5357 => 15357, + 5358 => 15358, + 5359 => 15359, + 5360 => 15360, + 5361 => 15361, + 5362 => 15362, + 5363 => 15363, + 5364 => 15364, + 5365 => 15365, + 5366 => 15366, + 5367 => 15367, + 5368 => 15368, + 5369 => 15369, + 5370 => 15370, + 5371 => 15371, + 5372 => 15372, + 5373 => 15373, + 5374 => 15374, + 5375 => 15375, + 5376 => 15376, + 5377 => 15377, + 5378 => 15378, + 5379 => 15379, + 5380 => 15380, + 5381 => 15381, + 5382 => 15382, + 5383 => 15383, + 5384 => 15384, + 5385 => 15385, + 5386 => 15386, + 5387 => 15387, + 5388 => 15388, + 5389 => 15389, + 5390 => 15390, + 5391 => 15391, + 5392 => 15392, + 5393 => 15393, + 5394 => 15394, + 5395 => 15395, + 5396 => 15396, + 5397 => 15397, + 5398 => 15398, + 5399 => 15399, + 5400 => 15400, + 5401 => 15401, + 5402 => 15402, + 5403 => 15403, + 5404 => 15404, + 5405 => 15405, + 5406 => 15406, + 5407 => 15407, + 5408 => 15408, + 5409 => 15409, + 5410 => 15410, + 5411 => 15411, + 5412 => 15412, + 5413 => 15413, + 5414 => 15414, + 5415 => 15415, + 5416 => 15416, + 5417 => 15417, + 5418 => 15418, + 5419 => 15419, + 5420 => 15420, + 5421 => 15421, + 5422 => 15422, + 5423 => 15423, + 5424 => 15424, + 5425 => 15425, + 5426 => 15426, + 5427 => 15427, + 5428 => 15428, + 5429 => 15429, + 5430 => 15430, + 5431 => 15431, + 5432 => 15432, + 5433 => 15433, + 5434 => 15434, + 5435 => 15435, + 5436 => 15436, + 5437 => 15437, + 5438 => 15438, + 5439 => 15439, + 5440 => 15440, + 5441 => 15441, + 5442 => 15442, + 5443 => 15443, + 5444 => 15444, + 5445 => 15445, + 5446 => 15446, + 5447 => 15447, + 5448 => 15448, + 5449 => 15449, + 5450 => 15450, + 5451 => 15451, + 5452 => 15452, + 5453 => 15453, + 5454 => 15454, + 5455 => 15455, + 5456 => 15456, + 5457 => 15457, + 5458 => 15458, + 5459 => 15459, + 5460 => 15460, + 5461 => 15461, + 5462 => 15462, + 5463 => 15463, + 5464 => 15464, + 5465 => 15465, + 5466 => 15466, + 5467 => 15467, + 5468 => 15468, + 5469 => 15469, + 5470 => 15470, + 5471 => 15471, + 5472 => 15472, + 5473 => 15473, + 5474 => 15474, + 5475 => 15475, + 5476 => 15476, + 5477 => 15477, + 5478 => 15478, + 5479 => 15479, + 5480 => 15480, + 5481 => 15481, + 5482 => 15482, + 5483 => 15483, + 5484 => 15484, + 5485 => 15485, + 5486 => 15486, + 5487 => 15487, + 5488 => 15488, + 5489 => 15489, + 5490 => 15490, + 5491 => 15491, + 5492 => 15492, + 5493 => 15493, + 5494 => 15494, + 5495 => 15495, + 5496 => 15496, + 5497 => 15497, + 5498 => 15498, + 5499 => 15499, + 5500 => 15500, + 5501 => 15501, + 5502 => 15502, + 5503 => 15503, + 5504 => 15504, + 5505 => 15505, + 5506 => 15506, + 5507 => 15507, + 5508 => 15508, + 5509 => 15509, + 5510 => 15510, + 5511 => 15511, + 5512 => 15512, + 5513 => 15513, + 5514 => 15514, + 5515 => 15515, + 5516 => 15516, + 5517 => 15517, + 5518 => 15518, + 5519 => 15519, + 5520 => 15520, + 5521 => 15521, + 5522 => 15522, + 5523 => 15523, + 5524 => 15524, + 5525 => 15525, + 5526 => 15526, + 5527 => 15527, + 5528 => 15528, + 5529 => 15529, + 5530 => 15530, + 5531 => 15531, + 5532 => 15532, + 5533 => 15533, + 5534 => 15534, + 5535 => 15535, + 5536 => 15536, + 5537 => 15537, + 5538 => 15538, + 5539 => 15539, + 5540 => 15540, + 5541 => 15541, + 5542 => 15542, + 5543 => 15543, + 5544 => 15544, + 5545 => 15545, + 5546 => 15546, + 5547 => 15547, + 5548 => 15548, + 5549 => 15549, + 5550 => 15550, + 5551 => 15551, + 5552 => 15552, + 5553 => 15553, + 5554 => 15554, + 5555 => 15555, + 5556 => 15556, + 5557 => 15557, + 5558 => 15558, + 5559 => 15559, + 5560 => 15560, + 5561 => 15561, + 5562 => 15562, + 5563 => 15563, + 5564 => 15564, + 5565 => 15565, + 5566 => 15566, + 5567 => 15567, + 5568 => 15568, + 5569 => 15569, + 5570 => 15570, + 5571 => 15571, + 5572 => 15572, + 5573 => 15573, + 5574 => 15574, + 5575 => 15575, + 5576 => 15576, + 5577 => 15577, + 5578 => 15578, + 5579 => 15579, + 5580 => 15580, + 5581 => 15581, + 5582 => 15582, + 5583 => 15583, + 5584 => 15584, + 5585 => 15585, + 5586 => 15586, + 5587 => 15587, + 5588 => 15588, + 5589 => 15589, + 5590 => 15590, + 5591 => 15591, + 5592 => 15592, + 5593 => 15593, + 5594 => 15594, + 5595 => 15595, + 5596 => 15596, + 5597 => 15597, + 5598 => 15598, + 5599 => 15599, + 5600 => 15600, + 5601 => 15601, + 5602 => 15602, + 5603 => 15603, + 5604 => 15604, + 5605 => 15605, + 5606 => 15606, + 5607 => 15607, + 5608 => 15608, + 5609 => 15609, + 5610 => 15610, + 5611 => 15611, + 5612 => 15612, + 5613 => 15613, + 5614 => 15614, + 5615 => 15615, + 5616 => 15616, + 5617 => 15617, + 5618 => 15618, + 5619 => 15619, + 5620 => 15620, + 5621 => 15621, + 5622 => 15622, + 5623 => 15623, + 5624 => 15624, + 5625 => 15625, + 5626 => 15626, + 5627 => 15627, + 5628 => 15628, + 5629 => 15629, + 5630 => 15630, + 5631 => 15631, + 5632 => 15632, + 5633 => 15633, + 5634 => 15634, + 5635 => 15635, + 5636 => 15636, + 5637 => 15637, + 5638 => 15638, + 5639 => 15639, + 5640 => 15640, + 5641 => 15641, + 5642 => 15642, + 5643 => 15643, + 5644 => 15644, + 5645 => 15645, + 5646 => 15646, + 5647 => 15647, + 5648 => 15648, + 5649 => 15649, + 5650 => 15650, + 5651 => 15651, + 5652 => 15652, + 5653 => 15653, + 5654 => 15654, + 5655 => 15655, + 5656 => 15656, + 5657 => 15657, + 5658 => 15658, + 5659 => 15659, + 5660 => 15660, + 5661 => 15661, + 5662 => 15662, + 5663 => 15663, + 5664 => 15664, + 5665 => 15665, + 5666 => 15666, + 5667 => 15667, + 5668 => 15668, + 5669 => 15669, + 5670 => 15670, + 5671 => 15671, + 5672 => 15672, + 5673 => 15673, + 5674 => 15674, + 5675 => 15675, + 5676 => 15676, + 5677 => 15677, + 5678 => 15678, + 5679 => 15679, + 5680 => 15680, + 5681 => 15681, + 5682 => 15682, + 5683 => 15683, + 5684 => 15684, + 5685 => 15685, + 5686 => 15686, + 5687 => 15687, + 5688 => 15688, + 5689 => 15689, + 5690 => 15690, + 5691 => 15691, + 5692 => 15692, + 5693 => 15693, + 5694 => 15694, + 5695 => 15695, + 5696 => 15696, + 5697 => 15697, + 5698 => 15698, + 5699 => 15699, + 5700 => 15700, + 5701 => 15701, + 5702 => 15702, + 5703 => 15703, + 5704 => 15704, + 5705 => 15705, + 5706 => 15706, + 5707 => 15707, + 5708 => 15708, + 5709 => 15709, + 5710 => 15710, + 5711 => 15711, + 5712 => 15712, + 5713 => 15713, + 5714 => 15714, + 5715 => 15715, + 5716 => 15716, + 5717 => 15717, + 5718 => 15718, + 5719 => 15719, + 5720 => 15720, + 5721 => 15721, + 5722 => 15722, + 5723 => 15723, + 5724 => 15724, + 5725 => 15725, + 5726 => 15726, + 5727 => 15727, + 5728 => 15728, + 5729 => 15729, + 5730 => 15730, + 5731 => 15731, + 5732 => 15732, + 5733 => 15733, + 5734 => 15734, + 5735 => 15735, + 5736 => 15736, + 5737 => 15737, + 5738 => 15738, + 5739 => 15739, + 5740 => 15740, + 5741 => 15741, + 5742 => 15742, + 5743 => 15743, + 5744 => 15744, + 5745 => 15745, + 5746 => 15746, + 5747 => 15747, + 5748 => 15748, + 5749 => 15749, + 5750 => 15750, + 5751 => 15751, + 5752 => 15752, + 5753 => 15753, + 5754 => 15754, + 5755 => 15755, + 5756 => 15756, + 5757 => 15757, + 5758 => 15758, + 5759 => 15759, + 5760 => 15760, + 5761 => 15761, + 5762 => 15762, + 5763 => 15763, + 5764 => 15764, + 5765 => 15765, + 5766 => 15766, + 5767 => 15767, + 5768 => 15768, + 5769 => 15769, + 5770 => 15770, + 5771 => 15771, + 5772 => 15772, + 5773 => 15773, + 5774 => 15774, + 5775 => 15775, + 5776 => 15776, + 5777 => 15777, + 5778 => 15778, + 5779 => 15779, + 5780 => 15780, + 5781 => 15781, + 5782 => 15782, + 5783 => 15783, + 5784 => 15784, + 5785 => 15785, + 5786 => 15786, + 5787 => 15787, + 5788 => 15788, + 5789 => 15789, + 5790 => 15790, + 5791 => 15791, + 5792 => 15792, + 5793 => 15793, + 5794 => 15794, + 5795 => 15795, + 5796 => 15796, + 5797 => 15797, + 5798 => 15798, + 5799 => 15799, + 5800 => 15800, + 5801 => 15801, + 5802 => 15802, + 5803 => 15803, + 5804 => 15804, + 5805 => 15805, + 5806 => 15806, + 5807 => 15807, + 5808 => 15808, + 5809 => 15809, + 5810 => 15810, + 5811 => 15811, + 5812 => 15812, + 5813 => 15813, + 5814 => 15814, + 5815 => 15815, + 5816 => 15816, + 5817 => 15817, + 5818 => 15818, + 5819 => 15819, + 5820 => 15820, + 5821 => 15821, + 5822 => 15822, + 5823 => 15823, + 5824 => 15824, + 5825 => 15825, + 5826 => 15826, + 5827 => 15827, + 5828 => 15828, + 5829 => 15829, + 5830 => 15830, + 5831 => 15831, + 5832 => 15832, + 5833 => 15833, + 5834 => 15834, + 5835 => 15835, + 5836 => 15836, + 5837 => 15837, + 5838 => 15838, + 5839 => 15839, + 5840 => 15840, + 5841 => 15841, + 5842 => 15842, + 5843 => 15843, + 5844 => 15844, + 5845 => 15845, + 5846 => 15846, + 5847 => 15847, + 5848 => 15848, + 5849 => 15849, + 5850 => 15850, + 5851 => 15851, + 5852 => 15852, + 5853 => 15853, + 5854 => 15854, + 5855 => 15855, + 5856 => 15856, + 5857 => 15857, + 5858 => 15858, + 5859 => 15859, + 5860 => 15860, + 5861 => 15861, + 5862 => 15862, + 5863 => 15863, + 5864 => 15864, + 5865 => 15865, + 5866 => 15866, + 5867 => 15867, + 5868 => 15868, + 5869 => 15869, + 5870 => 15870, + 5871 => 15871, + 5872 => 15872, + 5873 => 15873, + 5874 => 15874, + 5875 => 15875, + 5876 => 15876, + 5877 => 15877, + 5878 => 15878, + 5879 => 15879, + 5880 => 15880, + 5881 => 15881, + 5882 => 15882, + 5883 => 15883, + 5884 => 15884, + 5885 => 15885, + 5886 => 15886, + 5887 => 15887, + 5888 => 15888, + 5889 => 15889, + 5890 => 15890, + 5891 => 15891, + 5892 => 15892, + 5893 => 15893, + 5894 => 15894, + 5895 => 15895, + 5896 => 15896, + 5897 => 15897, + 5898 => 15898, + 5899 => 15899, + 5900 => 15900, + 5901 => 15901, + 5902 => 15902, + 5903 => 15903, + 5904 => 15904, + 5905 => 15905, + 5906 => 15906, + 5907 => 15907, + 5908 => 15908, + 5909 => 15909, + 5910 => 15910, + 5911 => 15911, + 5912 => 15912, + 5913 => 15913, + 5914 => 15914, + 5915 => 15915, + 5916 => 15916, + 5917 => 15917, + 5918 => 15918, + 5919 => 15919, + 5920 => 15920, + 5921 => 15921, + 5922 => 15922, + 5923 => 15923, + 5924 => 15924, + 5925 => 15925, + 5926 => 15926, + 5927 => 15927, + 5928 => 15928, + 5929 => 15929, + 5930 => 15930, + 5931 => 15931, + 5932 => 15932, + 5933 => 15933, + 5934 => 15934, + 5935 => 15935, + 5936 => 15936, + 5937 => 15937, + 5938 => 15938, + 5939 => 15939, + 5940 => 15940, + 5941 => 15941, + 5942 => 15942, + 5943 => 15943, + 5944 => 15944, + 5945 => 15945, + 5946 => 15946, + 5947 => 15947, + 5948 => 15948, + 5949 => 15949, + 5950 => 15950, + 5951 => 15951, + 5952 => 15952, + 5953 => 15953, + 5954 => 15954, + 5955 => 15955, + 5956 => 15956, + 5957 => 15957, + 5958 => 15958, + 5959 => 15959, + 5960 => 15960, + 5961 => 15961, + 5962 => 15962, + 5963 => 15963, + 5964 => 15964, + 5965 => 15965, + 5966 => 15966, + 5967 => 15967, + 5968 => 15968, + 5969 => 15969, + 5970 => 15970, + 5971 => 15971, + 5972 => 15972, + 5973 => 15973, + 5974 => 15974, + 5975 => 15975, + 5976 => 15976, + 5977 => 15977, + 5978 => 15978, + 5979 => 15979, + 5980 => 15980, + 5981 => 15981, + 5982 => 15982, + 5983 => 15983, + 5984 => 15984, + 5985 => 15985, + 5986 => 15986, + 5987 => 15987, + 5988 => 15988, + 5989 => 15989, + 5990 => 15990, + 5991 => 15991, + 5992 => 15992, + 5993 => 15993, + 5994 => 15994, + 5995 => 15995, + 5996 => 15996, + 5997 => 15997, + 5998 => 15998, + 5999 => 15999, + 6000 => 16000, + 6001 => 16001, + 6002 => 16002, + 6003 => 16003, + 6004 => 16004, + 6005 => 16005, + 6006 => 16006, + 6007 => 16007, + 6008 => 16008, + 6009 => 16009, + 6010 => 16010, + 6011 => 16011, + 6012 => 16012, + 6013 => 16013, + 6014 => 16014, + 6015 => 16015, + 6016 => 16016, + 6017 => 16017, + 6018 => 16018, + 6019 => 16019, + 6020 => 16020, + 6021 => 16021, + 6022 => 16022, + 6023 => 16023, + 6024 => 16024, + 6025 => 16025, + 6026 => 16026, + 6027 => 16027, + 6028 => 16028, + 6029 => 16029, + 6030 => 16030, + 6031 => 16031, + 6032 => 16032, + 6033 => 16033, + 6034 => 16034, + 6035 => 16035, + 6036 => 16036, + 6037 => 16037, + 6038 => 16038, + 6039 => 16039, + 6040 => 16040, + 6041 => 16041, + 6042 => 16042, + 6043 => 16043, + 6044 => 16044, + 6045 => 16045, + 6046 => 16046, + 6047 => 16047, + 6048 => 16048, + 6049 => 16049, + 6050 => 16050, + 6051 => 16051, + 6052 => 16052, + 6053 => 16053, + 6054 => 16054, + 6055 => 16055, + 6056 => 16056, + 6057 => 16057, + 6058 => 16058, + 6059 => 16059, + 6060 => 16060, + 6061 => 16061, + 6062 => 16062, + 6063 => 16063, + 6064 => 16064, + 6065 => 16065, + 6066 => 16066, + 6067 => 16067, + 6068 => 16068, + 6069 => 16069, + 6070 => 16070, + 6071 => 16071, + 6072 => 16072, + 6073 => 16073, + 6074 => 16074, + 6075 => 16075, + 6076 => 16076, + 6077 => 16077, + 6078 => 16078, + 6079 => 16079, + 6080 => 16080, + 6081 => 16081, + 6082 => 16082, + 6083 => 16083, + 6084 => 16084, + 6085 => 16085, + 6086 => 16086, + 6087 => 16087, + 6088 => 16088, + 6089 => 16089, + 6090 => 16090, + 6091 => 16091, + 6092 => 16092, + 6093 => 16093, + 6094 => 16094, + 6095 => 16095, + 6096 => 16096, + 6097 => 16097, + 6098 => 16098, + 6099 => 16099, + 6100 => 16100, + 6101 => 16101, + 6102 => 16102, + 6103 => 16103, + 6104 => 16104, + 6105 => 16105, + 6106 => 16106, + 6107 => 16107, + 6108 => 16108, + 6109 => 16109, + 6110 => 16110, + 6111 => 16111, + 6112 => 16112, + 6113 => 16113, + 6114 => 16114, + 6115 => 16115, + 6116 => 16116, + 6117 => 16117, + 6118 => 16118, + 6119 => 16119, + 6120 => 16120, + 6121 => 16121, + 6122 => 16122, + 6123 => 16123, + 6124 => 16124, + 6125 => 16125, + 6126 => 16126, + 6127 => 16127, + 6128 => 16128, + 6129 => 16129, + 6130 => 16130, + 6131 => 16131, + 6132 => 16132, + 6133 => 16133, + 6134 => 16134, + 6135 => 16135, + 6136 => 16136, + 6137 => 16137, + 6138 => 16138, + 6139 => 16139, + 6140 => 16140, + 6141 => 16141, + 6142 => 16142, + 6143 => 16143, + 6144 => 16144, + 6145 => 16145, + 6146 => 16146, + 6147 => 16147, + 6148 => 16148, + 6149 => 16149, + 6150 => 16150, + 6151 => 16151, + 6152 => 16152, + 6153 => 16153, + 6154 => 16154, + 6155 => 16155, + 6156 => 16156, + 6157 => 16157, + 6158 => 16158, + 6159 => 16159, + 6160 => 16160, + 6161 => 16161, + 6162 => 16162, + 6163 => 16163, + 6164 => 16164, + 6165 => 16165, + 6166 => 16166, + 6167 => 16167, + 6168 => 16168, + 6169 => 16169, + 6170 => 16170, + 6171 => 16171, + 6172 => 16172, + 6173 => 16173, + 6174 => 16174, + 6175 => 16175, + 6176 => 16176, + 6177 => 16177, + 6178 => 16178, + 6179 => 16179, + 6180 => 16180, + 6181 => 16181, + 6182 => 16182, + 6183 => 16183, + 6184 => 16184, + 6185 => 16185, + 6186 => 16186, + 6187 => 16187, + 6188 => 16188, + 6189 => 16189, + 6190 => 16190, + 6191 => 16191, + 6192 => 16192, + 6193 => 16193, + 6194 => 16194, + 6195 => 16195, + 6196 => 16196, + 6197 => 16197, + 6198 => 16198, + 6199 => 16199, + 6200 => 16200, + 6201 => 16201, + 6202 => 16202, + 6203 => 16203, + 6204 => 16204, + 6205 => 16205, + 6206 => 16206, + 6207 => 16207, + 6208 => 16208, + 6209 => 16209, + 6210 => 16210, + 6211 => 16211, + 6212 => 16212, + 6213 => 16213, + 6214 => 16214, + 6215 => 16215, + 6216 => 16216, + 6217 => 16217, + 6218 => 16218, + 6219 => 16219, + 6220 => 16220, + 6221 => 16221, + 6222 => 16222, + 6223 => 16223, + 6224 => 16224, + 6225 => 16225, + 6226 => 16226, + 6227 => 16227, + 6228 => 16228, + 6229 => 16229, + 6230 => 16230, + 6231 => 16231, + 6232 => 16232, + 6233 => 16233, + 6234 => 16234, + 6235 => 16235, + 6236 => 16236, + 6237 => 16237, + 6238 => 16238, + 6239 => 16239, + 6240 => 16240, + 6241 => 16241, + 6242 => 16242, + 6243 => 16243, + 6244 => 16244, + 6245 => 16245, + 6246 => 16246, + 6247 => 16247, + 6248 => 16248, + 6249 => 16249, + 6250 => 16250, + 6251 => 16251, + 6252 => 16252, + 6253 => 16253, + 6254 => 16254, + 6255 => 16255, + 6256 => 16256, + 6257 => 16257, + 6258 => 16258, + 6259 => 16259, + 6260 => 16260, + 6261 => 16261, + 6262 => 16262, + 6263 => 16263, + 6264 => 16264, + 6265 => 16265, + 6266 => 16266, + 6267 => 16267, + 6268 => 16268, + 6269 => 16269, + 6270 => 16270, + 6271 => 16271, + 6272 => 16272, + 6273 => 16273, + 6274 => 16274, + 6275 => 16275, + 6276 => 16276, + 6277 => 16277, + 6278 => 16278, + 6279 => 16279, + 6280 => 16280, + 6281 => 16281, + 6282 => 16282, + 6283 => 16283, + 6284 => 16284, + 6285 => 16285, + 6286 => 16286, + 6287 => 16287, + 6288 => 16288, + 6289 => 16289, + 6290 => 16290, + 6291 => 16291, + 6292 => 16292, + 6293 => 16293, + 6294 => 16294, + 6295 => 16295, + 6296 => 16296, + 6297 => 16297, + 6298 => 16298, + 6299 => 16299, + 6300 => 16300, + 6301 => 16301, + 6302 => 16302, + 6303 => 16303, + 6304 => 16304, + 6305 => 16305, + 6306 => 16306, + 6307 => 16307, + 6308 => 16308, + 6309 => 16309, + 6310 => 16310, + 6311 => 16311, + 6312 => 16312, + 6313 => 16313, + 6314 => 16314, + 6315 => 16315, + 6316 => 16316, + 6317 => 16317, + 6318 => 16318, + 6319 => 16319, + 6320 => 16320, + 6321 => 16321, + 6322 => 16322, + 6323 => 16323, + 6324 => 16324, + 6325 => 16325, + 6326 => 16326, + 6327 => 16327, + 6328 => 16328, + 6329 => 16329, + 6330 => 16330, + 6331 => 16331, + 6332 => 16332, + 6333 => 16333, + 6334 => 16334, + 6335 => 16335, + 6336 => 16336, + 6337 => 16337, + 6338 => 16338, + 6339 => 16339, + 6340 => 16340, + 6341 => 16341, + 6342 => 16342, + 6343 => 16343, + 6344 => 16344, + 6345 => 16345, + 6346 => 16346, + 6347 => 16347, + 6348 => 16348, + 6349 => 16349, + 6350 => 16350, + 6351 => 16351, + 6352 => 16352, + 6353 => 16353, + 6354 => 16354, + 6355 => 16355, + 6356 => 16356, + 6357 => 16357, + 6358 => 16358, + 6359 => 16359, + 6360 => 16360, + 6361 => 16361, + 6362 => 16362, + 6363 => 16363, + 6364 => 16364, + 6365 => 16365, + 6366 => 16366, + 6367 => 16367, + 6368 => 16368, + 6369 => 16369, + 6370 => 16370, + 6371 => 16371, + 6372 => 16372, + 6373 => 16373, + 6374 => 16374, + 6375 => 16375, + 6376 => 16376, + 6377 => 16377, + 6378 => 16378, + 6379 => 16379, + 6380 => 16380, + 6381 => 16381, + 6382 => 16382, + 6383 => 16383, + 6384 => 16384, + 6385 => 16385, + 6386 => 16386, + 6387 => 16387, + 6388 => 16388, + 6389 => 16389, + 6390 => 16390, + 6391 => 16391, + 6392 => 16392, + 6393 => 16393, + 6394 => 16394, + 6395 => 16395, + 6396 => 16396, + 6397 => 16397, + 6398 => 16398, + 6399 => 16399, + 6400 => 16400, + 6401 => 16401, + 6402 => 16402, + 6403 => 16403, + 6404 => 16404, + 6405 => 16405, + 6406 => 16406, + 6407 => 16407, + 6408 => 16408, + 6409 => 16409, + 6410 => 16410, + 6411 => 16411, + 6412 => 16412, + 6413 => 16413, + 6414 => 16414, + 6415 => 16415, + 6416 => 16416, + 6417 => 16417, + 6418 => 16418, + 6419 => 16419, + 6420 => 16420, + 6421 => 16421, + 6422 => 16422, + 6423 => 16423, + 6424 => 16424, + 6425 => 16425, + 6426 => 16426, + 6427 => 16427, + 6428 => 16428, + 6429 => 16429, + 6430 => 16430, + 6431 => 16431, + 6432 => 16432, + 6433 => 16433, + 6434 => 16434, + 6435 => 16435, + 6436 => 16436, + 6437 => 16437, + 6438 => 16438, + 6439 => 16439, + 6440 => 16440, + 6441 => 16441, + 6442 => 16442, + 6443 => 16443, + 6444 => 16444, + 6445 => 16445, + 6446 => 16446, + 6447 => 16447, + 6448 => 16448, + 6449 => 16449, + 6450 => 16450, + 6451 => 16451, + 6452 => 16452, + 6453 => 16453, + 6454 => 16454, + 6455 => 16455, + 6456 => 16456, + 6457 => 16457, + 6458 => 16458, + 6459 => 16459, + 6460 => 16460, + 6461 => 16461, + 6462 => 16462, + 6463 => 16463, + 6464 => 16464, + 6465 => 16465, + 6466 => 16466, + 6467 => 16467, + 6468 => 16468, + 6469 => 16469, + 6470 => 16470, + 6471 => 16471, + 6472 => 16472, + 6473 => 16473, + 6474 => 16474, + 6475 => 16475, + 6476 => 16476, + 6477 => 16477, + 6478 => 16478, + 6479 => 16479, + 6480 => 16480, + 6481 => 16481, + 6482 => 16482, + 6483 => 16483, + 6484 => 16484, + 6485 => 16485, + 6486 => 16486, + 6487 => 16487, + 6488 => 16488, + 6489 => 16489, + 6490 => 16490, + 6491 => 16491, + 6492 => 16492, + 6493 => 16493, + 6494 => 16494, + 6495 => 16495, + 6496 => 16496, + 6497 => 16497, + 6498 => 16498, + 6499 => 16499, + 6500 => 16500, +]; + +const TEST_ARRAY_2 = [ + 10001 => 20001, + 10002 => 20002, + 10003 => 20003, + 10004 => 20004, + 10005 => 20005, + 10006 => 20006, + 10007 => 20007, + 10008 => 20008, + 10009 => 20009, + 10010 => 20010, + 10011 => 20011, + 10012 => 20012, + 10013 => 20013, + 10014 => 20014, + 10015 => 20015, + 10016 => 20016, + 10017 => 20017, + 10018 => 20018, + 10019 => 20019, + 10020 => 20020, + 10021 => 20021, + 10022 => 20022, + 10023 => 20023, + 10024 => 20024, + 10025 => 20025, + 10026 => 20026, + 10027 => 20027, + 10028 => 20028, + 10029 => 20029, + 10030 => 20030, + 10031 => 20031, + 10032 => 20032, + 10033 => 20033, + 10034 => 20034, + 10035 => 20035, + 10036 => 20036, + 10037 => 20037, + 10038 => 20038, + 10039 => 20039, + 10040 => 20040, + 10041 => 20041, + 10042 => 20042, + 10043 => 20043, + 10044 => 20044, + 10045 => 20045, + 10046 => 20046, + 10047 => 20047, + 10048 => 20048, + 10049 => 20049, + 10050 => 20050, + 10051 => 20051, + 10052 => 20052, + 10053 => 20053, + 10054 => 20054, + 10055 => 20055, + 10056 => 20056, + 10057 => 20057, + 10058 => 20058, + 10059 => 20059, + 10060 => 20060, + 10061 => 20061, + 10062 => 20062, + 10063 => 20063, + 10064 => 20064, + 10065 => 20065, + 10066 => 20066, + 10067 => 20067, + 10068 => 20068, + 10069 => 20069, + 10070 => 20070, + 10071 => 20071, + 10072 => 20072, + 10073 => 20073, + 10074 => 20074, + 10075 => 20075, + 10076 => 20076, + 10077 => 20077, + 10078 => 20078, + 10079 => 20079, + 10080 => 20080, + 10081 => 20081, + 10082 => 20082, + 10083 => 20083, + 10084 => 20084, + 10085 => 20085, + 10086 => 20086, + 10087 => 20087, + 10088 => 20088, + 10089 => 20089, + 10090 => 20090, + 10091 => 20091, + 10092 => 20092, + 10093 => 20093, + 10094 => 20094, + 10095 => 20095, + 10096 => 20096, + 10097 => 20097, + 10098 => 20098, + 10099 => 20099, + 10100 => 20100, + 10101 => 20101, + 10102 => 20102, + 10103 => 20103, + 10104 => 20104, + 10105 => 20105, + 10106 => 20106, + 10107 => 20107, + 10108 => 20108, + 10109 => 20109, + 10110 => 20110, + 10111 => 20111, + 10112 => 20112, + 10113 => 20113, + 10114 => 20114, + 10115 => 20115, + 10116 => 20116, + 10117 => 20117, + 10118 => 20118, + 10119 => 20119, + 10120 => 20120, + 10121 => 20121, + 10122 => 20122, + 10123 => 20123, + 10124 => 20124, + 10125 => 20125, + 10126 => 20126, + 10127 => 20127, + 10128 => 20128, + 10129 => 20129, + 10130 => 20130, + 10131 => 20131, + 10132 => 20132, + 10133 => 20133, + 10134 => 20134, + 10135 => 20135, + 10136 => 20136, + 10137 => 20137, + 10138 => 20138, + 10139 => 20139, + 10140 => 20140, + 10141 => 20141, + 10142 => 20142, + 10143 => 20143, + 10144 => 20144, + 10145 => 20145, + 10146 => 20146, + 10147 => 20147, + 10148 => 20148, + 10149 => 20149, + 10150 => 20150, + 10151 => 20151, + 10152 => 20152, + 10153 => 20153, + 10154 => 20154, + 10155 => 20155, + 10156 => 20156, + 10157 => 20157, + 10158 => 20158, + 10159 => 20159, + 10160 => 20160, + 10161 => 20161, + 10162 => 20162, + 10163 => 20163, + 10164 => 20164, + 10165 => 20165, + 10166 => 20166, + 10167 => 20167, + 10168 => 20168, + 10169 => 20169, + 10170 => 20170, + 10171 => 20171, + 10172 => 20172, + 10173 => 20173, + 10174 => 20174, + 10175 => 20175, + 10176 => 20176, + 10177 => 20177, + 10178 => 20178, + 10179 => 20179, + 10180 => 20180, + 10181 => 20181, + 10182 => 20182, + 10183 => 20183, + 10184 => 20184, + 10185 => 20185, + 10186 => 20186, + 10187 => 20187, + 10188 => 20188, + 10189 => 20189, + 10190 => 20190, + 10191 => 20191, + 10192 => 20192, + 10193 => 20193, + 10194 => 20194, + 10195 => 20195, + 10196 => 20196, + 10197 => 20197, + 10198 => 20198, + 10199 => 20199, + 10200 => 20200, + 10201 => 20201, + 10202 => 20202, + 10203 => 20203, + 10204 => 20204, + 10205 => 20205, + 10206 => 20206, + 10207 => 20207, + 10208 => 20208, + 10209 => 20209, + 10210 => 20210, + 10211 => 20211, + 10212 => 20212, + 10213 => 20213, + 10214 => 20214, + 10215 => 20215, + 10216 => 20216, + 10217 => 20217, + 10218 => 20218, + 10219 => 20219, + 10220 => 20220, + 10221 => 20221, + 10222 => 20222, + 10223 => 20223, + 10224 => 20224, + 10225 => 20225, + 10226 => 20226, + 10227 => 20227, + 10228 => 20228, + 10229 => 20229, + 10230 => 20230, + 10231 => 20231, + 10232 => 20232, + 10233 => 20233, + 10234 => 20234, + 10235 => 20235, + 10236 => 20236, + 10237 => 20237, + 10238 => 20238, + 10239 => 20239, + 10240 => 20240, + 10241 => 20241, + 10242 => 20242, + 10243 => 20243, + 10244 => 20244, + 10245 => 20245, + 10246 => 20246, + 10247 => 20247, + 10248 => 20248, + 10249 => 20249, + 10250 => 20250, + 10251 => 20251, + 10252 => 20252, + 10253 => 20253, + 10254 => 20254, + 10255 => 20255, + 10256 => 20256, + 10257 => 20257, + 10258 => 20258, + 10259 => 20259, + 10260 => 20260, + 10261 => 20261, + 10262 => 20262, + 10263 => 20263, + 10264 => 20264, + 10265 => 20265, + 10266 => 20266, + 10267 => 20267, + 10268 => 20268, + 10269 => 20269, + 10270 => 20270, + 10271 => 20271, + 10272 => 20272, + 10273 => 20273, + 10274 => 20274, + 10275 => 20275, + 10276 => 20276, + 10277 => 20277, + 10278 => 20278, + 10279 => 20279, + 10280 => 20280, + 10281 => 20281, + 10282 => 20282, + 10283 => 20283, + 10284 => 20284, + 10285 => 20285, + 10286 => 20286, + 10287 => 20287, + 10288 => 20288, + 10289 => 20289, + 10290 => 20290, + 10291 => 20291, + 10292 => 20292, + 10293 => 20293, + 10294 => 20294, + 10295 => 20295, + 10296 => 20296, + 10297 => 20297, + 10298 => 20298, + 10299 => 20299, + 10300 => 20300, + 10301 => 20301, + 10302 => 20302, + 10303 => 20303, + 10304 => 20304, + 10305 => 20305, + 10306 => 20306, + 10307 => 20307, + 10308 => 20308, + 10309 => 20309, + 10310 => 20310, + 10311 => 20311, + 10312 => 20312, + 10313 => 20313, + 10314 => 20314, + 10315 => 20315, + 10316 => 20316, + 10317 => 20317, + 10318 => 20318, + 10319 => 20319, + 10320 => 20320, + 10321 => 20321, + 10322 => 20322, + 10323 => 20323, + 10324 => 20324, + 10325 => 20325, + 10326 => 20326, + 10327 => 20327, + 10328 => 20328, + 10329 => 20329, + 10330 => 20330, + 10331 => 20331, + 10332 => 20332, + 10333 => 20333, + 10334 => 20334, + 10335 => 20335, + 10336 => 20336, + 10337 => 20337, + 10338 => 20338, + 10339 => 20339, + 10340 => 20340, + 10341 => 20341, + 10342 => 20342, + 10343 => 20343, + 10344 => 20344, + 10345 => 20345, + 10346 => 20346, + 10347 => 20347, + 10348 => 20348, + 10349 => 20349, + 10350 => 20350, + 10351 => 20351, + 10352 => 20352, + 10353 => 20353, + 10354 => 20354, + 10355 => 20355, + 10356 => 20356, + 10357 => 20357, + 10358 => 20358, + 10359 => 20359, + 10360 => 20360, + 10361 => 20361, + 10362 => 20362, + 10363 => 20363, + 10364 => 20364, + 10365 => 20365, + 10366 => 20366, + 10367 => 20367, + 10368 => 20368, + 10369 => 20369, + 10370 => 20370, + 10371 => 20371, + 10372 => 20372, + 10373 => 20373, + 10374 => 20374, + 10375 => 20375, + 10376 => 20376, + 10377 => 20377, + 10378 => 20378, + 10379 => 20379, + 10380 => 20380, + 10381 => 20381, + 10382 => 20382, + 10383 => 20383, + 10384 => 20384, + 10385 => 20385, + 10386 => 20386, + 10387 => 20387, + 10388 => 20388, + 10389 => 20389, + 10390 => 20390, + 10391 => 20391, + 10392 => 20392, + 10393 => 20393, + 10394 => 20394, + 10395 => 20395, + 10396 => 20396, + 10397 => 20397, + 10398 => 20398, + 10399 => 20399, + 10400 => 20400, + 10401 => 20401, + 10402 => 20402, + 10403 => 20403, + 10404 => 20404, + 10405 => 20405, + 10406 => 20406, + 10407 => 20407, + 10408 => 20408, + 10409 => 20409, + 10410 => 20410, + 10411 => 20411, + 10412 => 20412, + 10413 => 20413, + 10414 => 20414, + 10415 => 20415, + 10416 => 20416, + 10417 => 20417, + 10418 => 20418, + 10419 => 20419, + 10420 => 20420, + 10421 => 20421, + 10422 => 20422, + 10423 => 20423, + 10424 => 20424, + 10425 => 20425, + 10426 => 20426, + 10427 => 20427, + 10428 => 20428, + 10429 => 20429, + 10430 => 20430, + 10431 => 20431, + 10432 => 20432, + 10433 => 20433, + 10434 => 20434, + 10435 => 20435, + 10436 => 20436, + 10437 => 20437, + 10438 => 20438, + 10439 => 20439, + 10440 => 20440, + 10441 => 20441, + 10442 => 20442, + 10443 => 20443, + 10444 => 20444, + 10445 => 20445, + 10446 => 20446, + 10447 => 20447, + 10448 => 20448, + 10449 => 20449, + 10450 => 20450, + 10451 => 20451, + 10452 => 20452, + 10453 => 20453, + 10454 => 20454, + 10455 => 20455, + 10456 => 20456, + 10457 => 20457, + 10458 => 20458, + 10459 => 20459, + 10460 => 20460, + 10461 => 20461, + 10462 => 20462, + 10463 => 20463, + 10464 => 20464, + 10465 => 20465, + 10466 => 20466, + 10467 => 20467, + 10468 => 20468, + 10469 => 20469, + 10470 => 20470, + 10471 => 20471, + 10472 => 20472, + 10473 => 20473, + 10474 => 20474, + 10475 => 20475, + 10476 => 20476, + 10477 => 20477, + 10478 => 20478, + 10479 => 20479, + 10480 => 20480, + 10481 => 20481, + 10482 => 20482, + 10483 => 20483, + 10484 => 20484, + 10485 => 20485, + 10486 => 20486, + 10487 => 20487, + 10488 => 20488, + 10489 => 20489, + 10490 => 20490, + 10491 => 20491, + 10492 => 20492, + 10493 => 20493, + 10494 => 20494, + 10495 => 20495, + 10496 => 20496, + 10497 => 20497, + 10498 => 20498, + 10499 => 20499, + 10500 => 20500, + 10501 => 20501, + 10502 => 20502, + 10503 => 20503, + 10504 => 20504, + 10505 => 20505, + 10506 => 20506, + 10507 => 20507, + 10508 => 20508, + 10509 => 20509, + 10510 => 20510, + 10511 => 20511, + 10512 => 20512, + 10513 => 20513, + 10514 => 20514, + 10515 => 20515, + 10516 => 20516, + 10517 => 20517, + 10518 => 20518, + 10519 => 20519, + 10520 => 20520, + 10521 => 20521, + 10522 => 20522, + 10523 => 20523, + 10524 => 20524, + 10525 => 20525, + 10526 => 20526, + 10527 => 20527, + 10528 => 20528, + 10529 => 20529, + 10530 => 20530, + 10531 => 20531, + 10532 => 20532, + 10533 => 20533, + 10534 => 20534, + 10535 => 20535, + 10536 => 20536, + 10537 => 20537, + 10538 => 20538, + 10539 => 20539, + 10540 => 20540, + 10541 => 20541, + 10542 => 20542, + 10543 => 20543, + 10544 => 20544, + 10545 => 20545, + 10546 => 20546, + 10547 => 20547, + 10548 => 20548, + 10549 => 20549, + 10550 => 20550, + 10551 => 20551, + 10552 => 20552, + 10553 => 20553, + 10554 => 20554, + 10555 => 20555, + 10556 => 20556, + 10557 => 20557, + 10558 => 20558, + 10559 => 20559, + 10560 => 20560, + 10561 => 20561, + 10562 => 20562, + 10563 => 20563, + 10564 => 20564, + 10565 => 20565, + 10566 => 20566, + 10567 => 20567, + 10568 => 20568, + 10569 => 20569, + 10570 => 20570, + 10571 => 20571, + 10572 => 20572, + 10573 => 20573, + 10574 => 20574, + 10575 => 20575, + 10576 => 20576, + 10577 => 20577, + 10578 => 20578, + 10579 => 20579, + 10580 => 20580, + 10581 => 20581, + 10582 => 20582, + 10583 => 20583, + 10584 => 20584, + 10585 => 20585, + 10586 => 20586, + 10587 => 20587, + 10588 => 20588, + 10589 => 20589, + 10590 => 20590, + 10591 => 20591, + 10592 => 20592, + 10593 => 20593, + 10594 => 20594, + 10595 => 20595, + 10596 => 20596, + 10597 => 20597, + 10598 => 20598, + 10599 => 20599, + 10600 => 20600, + 10601 => 20601, + 10602 => 20602, + 10603 => 20603, + 10604 => 20604, + 10605 => 20605, + 10606 => 20606, + 10607 => 20607, + 10608 => 20608, + 10609 => 20609, + 10610 => 20610, + 10611 => 20611, + 10612 => 20612, + 10613 => 20613, + 10614 => 20614, + 10615 => 20615, + 10616 => 20616, + 10617 => 20617, + 10618 => 20618, + 10619 => 20619, + 10620 => 20620, + 10621 => 20621, + 10622 => 20622, + 10623 => 20623, + 10624 => 20624, + 10625 => 20625, + 10626 => 20626, + 10627 => 20627, + 10628 => 20628, + 10629 => 20629, + 10630 => 20630, + 10631 => 20631, + 10632 => 20632, + 10633 => 20633, + 10634 => 20634, + 10635 => 20635, + 10636 => 20636, + 10637 => 20637, + 10638 => 20638, + 10639 => 20639, + 10640 => 20640, + 10641 => 20641, + 10642 => 20642, + 10643 => 20643, + 10644 => 20644, + 10645 => 20645, + 10646 => 20646, + 10647 => 20647, + 10648 => 20648, + 10649 => 20649, + 10650 => 20650, + 10651 => 20651, + 10652 => 20652, + 10653 => 20653, + 10654 => 20654, + 10655 => 20655, + 10656 => 20656, + 10657 => 20657, + 10658 => 20658, + 10659 => 20659, + 10660 => 20660, + 10661 => 20661, + 10662 => 20662, + 10663 => 20663, + 10664 => 20664, + 10665 => 20665, + 10666 => 20666, + 10667 => 20667, + 10668 => 20668, + 10669 => 20669, + 10670 => 20670, + 10671 => 20671, + 10672 => 20672, + 10673 => 20673, + 10674 => 20674, + 10675 => 20675, + 10676 => 20676, + 10677 => 20677, + 10678 => 20678, + 10679 => 20679, + 10680 => 20680, + 10681 => 20681, + 10682 => 20682, + 10683 => 20683, + 10684 => 20684, + 10685 => 20685, + 10686 => 20686, + 10687 => 20687, + 10688 => 20688, + 10689 => 20689, + 10690 => 20690, + 10691 => 20691, + 10692 => 20692, + 10693 => 20693, + 10694 => 20694, + 10695 => 20695, + 10696 => 20696, + 10697 => 20697, + 10698 => 20698, + 10699 => 20699, + 10700 => 20700, + 10701 => 20701, + 10702 => 20702, + 10703 => 20703, + 10704 => 20704, + 10705 => 20705, + 10706 => 20706, + 10707 => 20707, + 10708 => 20708, + 10709 => 20709, + 10710 => 20710, + 10711 => 20711, + 10712 => 20712, + 10713 => 20713, + 10714 => 20714, + 10715 => 20715, + 10716 => 20716, + 10717 => 20717, + 10718 => 20718, + 10719 => 20719, + 10720 => 20720, + 10721 => 20721, + 10722 => 20722, + 10723 => 20723, + 10724 => 20724, + 10725 => 20725, + 10726 => 20726, + 10727 => 20727, + 10728 => 20728, + 10729 => 20729, + 10730 => 20730, + 10731 => 20731, + 10732 => 20732, + 10733 => 20733, + 10734 => 20734, + 10735 => 20735, + 10736 => 20736, + 10737 => 20737, + 10738 => 20738, + 10739 => 20739, + 10740 => 20740, + 10741 => 20741, + 10742 => 20742, + 10743 => 20743, + 10744 => 20744, + 10745 => 20745, + 10746 => 20746, + 10747 => 20747, + 10748 => 20748, + 10749 => 20749, + 10750 => 20750, + 10751 => 20751, + 10752 => 20752, + 10753 => 20753, + 10754 => 20754, + 10755 => 20755, + 10756 => 20756, + 10757 => 20757, + 10758 => 20758, + 10759 => 20759, + 10760 => 20760, + 10761 => 20761, + 10762 => 20762, + 10763 => 20763, + 10764 => 20764, + 10765 => 20765, + 10766 => 20766, + 10767 => 20767, + 10768 => 20768, + 10769 => 20769, + 10770 => 20770, + 10771 => 20771, + 10772 => 20772, + 10773 => 20773, + 10774 => 20774, + 10775 => 20775, + 10776 => 20776, + 10777 => 20777, + 10778 => 20778, + 10779 => 20779, + 10780 => 20780, + 10781 => 20781, + 10782 => 20782, + 10783 => 20783, + 10784 => 20784, + 10785 => 20785, + 10786 => 20786, + 10787 => 20787, + 10788 => 20788, + 10789 => 20789, + 10790 => 20790, + 10791 => 20791, + 10792 => 20792, + 10793 => 20793, + 10794 => 20794, + 10795 => 20795, + 10796 => 20796, + 10797 => 20797, + 10798 => 20798, + 10799 => 20799, + 10800 => 20800, + 10801 => 20801, + 10802 => 20802, + 10803 => 20803, + 10804 => 20804, + 10805 => 20805, + 10806 => 20806, + 10807 => 20807, + 10808 => 20808, + 10809 => 20809, + 10810 => 20810, + 10811 => 20811, + 10812 => 20812, + 10813 => 20813, + 10814 => 20814, + 10815 => 20815, + 10816 => 20816, + 10817 => 20817, + 10818 => 20818, + 10819 => 20819, + 10820 => 20820, + 10821 => 20821, + 10822 => 20822, + 10823 => 20823, + 10824 => 20824, + 10825 => 20825, + 10826 => 20826, + 10827 => 20827, + 10828 => 20828, + 10829 => 20829, + 10830 => 20830, + 10831 => 20831, + 10832 => 20832, + 10833 => 20833, + 10834 => 20834, + 10835 => 20835, + 10836 => 20836, + 10837 => 20837, + 10838 => 20838, + 10839 => 20839, + 10840 => 20840, + 10841 => 20841, + 10842 => 20842, + 10843 => 20843, + 10844 => 20844, + 10845 => 20845, + 10846 => 20846, + 10847 => 20847, + 10848 => 20848, + 10849 => 20849, + 10850 => 20850, + 10851 => 20851, + 10852 => 20852, + 10853 => 20853, + 10854 => 20854, + 10855 => 20855, + 10856 => 20856, + 10857 => 20857, + 10858 => 20858, + 10859 => 20859, + 10860 => 20860, + 10861 => 20861, + 10862 => 20862, + 10863 => 20863, + 10864 => 20864, + 10865 => 20865, + 10866 => 20866, + 10867 => 20867, + 10868 => 20868, + 10869 => 20869, + 10870 => 20870, + 10871 => 20871, + 10872 => 20872, + 10873 => 20873, + 10874 => 20874, + 10875 => 20875, + 10876 => 20876, + 10877 => 20877, + 10878 => 20878, + 10879 => 20879, + 10880 => 20880, + 10881 => 20881, + 10882 => 20882, + 10883 => 20883, + 10884 => 20884, + 10885 => 20885, + 10886 => 20886, + 10887 => 20887, + 10888 => 20888, + 10889 => 20889, + 10890 => 20890, + 10891 => 20891, + 10892 => 20892, + 10893 => 20893, + 10894 => 20894, + 10895 => 20895, + 10896 => 20896, + 10897 => 20897, + 10898 => 20898, + 10899 => 20899, + 10900 => 20900, + 10901 => 20901, + 10902 => 20902, + 10903 => 20903, + 10904 => 20904, + 10905 => 20905, + 10906 => 20906, + 10907 => 20907, + 10908 => 20908, + 10909 => 20909, + 10910 => 20910, + 10911 => 20911, + 10912 => 20912, + 10913 => 20913, + 10914 => 20914, + 10915 => 20915, + 10916 => 20916, + 10917 => 20917, + 10918 => 20918, + 10919 => 20919, + 10920 => 20920, + 10921 => 20921, + 10922 => 20922, + 10923 => 20923, + 10924 => 20924, + 10925 => 20925, + 10926 => 20926, + 10927 => 20927, + 10928 => 20928, + 10929 => 20929, + 10930 => 20930, + 10931 => 20931, + 10932 => 20932, + 10933 => 20933, + 10934 => 20934, + 10935 => 20935, + 10936 => 20936, + 10937 => 20937, + 10938 => 20938, + 10939 => 20939, + 10940 => 20940, + 10941 => 20941, + 10942 => 20942, + 10943 => 20943, + 10944 => 20944, + 10945 => 20945, + 10946 => 20946, + 10947 => 20947, + 10948 => 20948, + 10949 => 20949, + 10950 => 20950, + 10951 => 20951, + 10952 => 20952, + 10953 => 20953, + 10954 => 20954, + 10955 => 20955, + 10956 => 20956, + 10957 => 20957, + 10958 => 20958, + 10959 => 20959, + 10960 => 20960, + 10961 => 20961, + 10962 => 20962, + 10963 => 20963, + 10964 => 20964, + 10965 => 20965, + 10966 => 20966, + 10967 => 20967, + 10968 => 20968, + 10969 => 20969, + 10970 => 20970, + 10971 => 20971, + 10972 => 20972, + 10973 => 20973, + 10974 => 20974, + 10975 => 20975, + 10976 => 20976, + 10977 => 20977, + 10978 => 20978, + 10979 => 20979, + 10980 => 20980, + 10981 => 20981, + 10982 => 20982, + 10983 => 20983, + 10984 => 20984, + 10985 => 20985, + 10986 => 20986, + 10987 => 20987, + 10988 => 20988, + 10989 => 20989, + 10990 => 20990, + 10991 => 20991, + 10992 => 20992, + 10993 => 20993, + 10994 => 20994, + 10995 => 20995, + 10996 => 20996, + 10997 => 20997, + 10998 => 20998, + 10999 => 20999, + 11000 => 21000, + 11001 => 21001, + 11002 => 21002, + 11003 => 21003, + 11004 => 21004, + 11005 => 21005, + 11006 => 21006, + 11007 => 21007, + 11008 => 21008, + 11009 => 21009, + 11010 => 21010, + 11011 => 21011, + 11012 => 21012, + 11013 => 21013, + 11014 => 21014, + 11015 => 21015, + 11016 => 21016, + 11017 => 21017, + 11018 => 21018, + 11019 => 21019, + 11020 => 21020, + 11021 => 21021, + 11022 => 21022, + 11023 => 21023, + 11024 => 21024, + 11025 => 21025, + 11026 => 21026, + 11027 => 21027, + 11028 => 21028, + 11029 => 21029, + 11030 => 21030, + 11031 => 21031, + 11032 => 21032, + 11033 => 21033, + 11034 => 21034, + 11035 => 21035, + 11036 => 21036, + 11037 => 21037, + 11038 => 21038, + 11039 => 21039, + 11040 => 21040, + 11041 => 21041, + 11042 => 21042, + 11043 => 21043, + 11044 => 21044, + 11045 => 21045, + 11046 => 21046, + 11047 => 21047, + 11048 => 21048, + 11049 => 21049, + 11050 => 21050, + 11051 => 21051, + 11052 => 21052, + 11053 => 21053, + 11054 => 21054, + 11055 => 21055, + 11056 => 21056, + 11057 => 21057, + 11058 => 21058, + 11059 => 21059, + 11060 => 21060, + 11061 => 21061, + 11062 => 21062, + 11063 => 21063, + 11064 => 21064, + 11065 => 21065, + 11066 => 21066, + 11067 => 21067, + 11068 => 21068, + 11069 => 21069, + 11070 => 21070, + 11071 => 21071, + 11072 => 21072, + 11073 => 21073, + 11074 => 21074, + 11075 => 21075, + 11076 => 21076, + 11077 => 21077, + 11078 => 21078, + 11079 => 21079, + 11080 => 21080, + 11081 => 21081, + 11082 => 21082, + 11083 => 21083, + 11084 => 21084, + 11085 => 21085, + 11086 => 21086, + 11087 => 21087, + 11088 => 21088, + 11089 => 21089, + 11090 => 21090, + 11091 => 21091, + 11092 => 21092, + 11093 => 21093, + 11094 => 21094, + 11095 => 21095, + 11096 => 21096, + 11097 => 21097, + 11098 => 21098, + 11099 => 21099, + 11100 => 21100, + 11101 => 21101, + 11102 => 21102, + 11103 => 21103, + 11104 => 21104, + 11105 => 21105, + 11106 => 21106, + 11107 => 21107, + 11108 => 21108, + 11109 => 21109, + 11110 => 21110, + 11111 => 21111, + 11112 => 21112, + 11113 => 21113, + 11114 => 21114, + 11115 => 21115, + 11116 => 21116, + 11117 => 21117, + 11118 => 21118, + 11119 => 21119, + 11120 => 21120, + 11121 => 21121, + 11122 => 21122, + 11123 => 21123, + 11124 => 21124, + 11125 => 21125, + 11126 => 21126, + 11127 => 21127, + 11128 => 21128, + 11129 => 21129, + 11130 => 21130, + 11131 => 21131, + 11132 => 21132, + 11133 => 21133, + 11134 => 21134, + 11135 => 21135, + 11136 => 21136, + 11137 => 21137, + 11138 => 21138, + 11139 => 21139, + 11140 => 21140, + 11141 => 21141, + 11142 => 21142, + 11143 => 21143, + 11144 => 21144, + 11145 => 21145, + 11146 => 21146, + 11147 => 21147, + 11148 => 21148, + 11149 => 21149, + 11150 => 21150, + 11151 => 21151, + 11152 => 21152, + 11153 => 21153, + 11154 => 21154, + 11155 => 21155, + 11156 => 21156, + 11157 => 21157, + 11158 => 21158, + 11159 => 21159, + 11160 => 21160, + 11161 => 21161, + 11162 => 21162, + 11163 => 21163, + 11164 => 21164, + 11165 => 21165, + 11166 => 21166, + 11167 => 21167, + 11168 => 21168, + 11169 => 21169, + 11170 => 21170, + 11171 => 21171, + 11172 => 21172, + 11173 => 21173, + 11174 => 21174, + 11175 => 21175, + 11176 => 21176, + 11177 => 21177, + 11178 => 21178, + 11179 => 21179, + 11180 => 21180, + 11181 => 21181, + 11182 => 21182, + 11183 => 21183, + 11184 => 21184, + 11185 => 21185, + 11186 => 21186, + 11187 => 21187, + 11188 => 21188, + 11189 => 21189, + 11190 => 21190, + 11191 => 21191, + 11192 => 21192, + 11193 => 21193, + 11194 => 21194, + 11195 => 21195, + 11196 => 21196, + 11197 => 21197, + 11198 => 21198, + 11199 => 21199, + 11200 => 21200, + 11201 => 21201, + 11202 => 21202, + 11203 => 21203, + 11204 => 21204, + 11205 => 21205, + 11206 => 21206, + 11207 => 21207, + 11208 => 21208, + 11209 => 21209, + 11210 => 21210, + 11211 => 21211, + 11212 => 21212, + 11213 => 21213, + 11214 => 21214, + 11215 => 21215, + 11216 => 21216, + 11217 => 21217, + 11218 => 21218, + 11219 => 21219, + 11220 => 21220, + 11221 => 21221, + 11222 => 21222, + 11223 => 21223, + 11224 => 21224, + 11225 => 21225, + 11226 => 21226, + 11227 => 21227, + 11228 => 21228, + 11229 => 21229, + 11230 => 21230, + 11231 => 21231, + 11232 => 21232, + 11233 => 21233, + 11234 => 21234, + 11235 => 21235, + 11236 => 21236, + 11237 => 21237, + 11238 => 21238, + 11239 => 21239, + 11240 => 21240, + 11241 => 21241, + 11242 => 21242, + 11243 => 21243, + 11244 => 21244, + 11245 => 21245, + 11246 => 21246, + 11247 => 21247, + 11248 => 21248, + 11249 => 21249, + 11250 => 21250, + 11251 => 21251, + 11252 => 21252, + 11253 => 21253, + 11254 => 21254, + 11255 => 21255, + 11256 => 21256, + 11257 => 21257, + 11258 => 21258, + 11259 => 21259, + 11260 => 21260, + 11261 => 21261, + 11262 => 21262, + 11263 => 21263, + 11264 => 21264, + 11265 => 21265, + 11266 => 21266, + 11267 => 21267, + 11268 => 21268, + 11269 => 21269, + 11270 => 21270, + 11271 => 21271, + 11272 => 21272, + 11273 => 21273, + 11274 => 21274, + 11275 => 21275, + 11276 => 21276, + 11277 => 21277, + 11278 => 21278, + 11279 => 21279, + 11280 => 21280, + 11281 => 21281, + 11282 => 21282, + 11283 => 21283, + 11284 => 21284, + 11285 => 21285, + 11286 => 21286, + 11287 => 21287, + 11288 => 21288, + 11289 => 21289, + 11290 => 21290, + 11291 => 21291, + 11292 => 21292, + 11293 => 21293, + 11294 => 21294, + 11295 => 21295, + 11296 => 21296, + 11297 => 21297, + 11298 => 21298, + 11299 => 21299, + 11300 => 21300, + 11301 => 21301, + 11302 => 21302, + 11303 => 21303, + 11304 => 21304, + 11305 => 21305, + 11306 => 21306, + 11307 => 21307, + 11308 => 21308, + 11309 => 21309, + 11310 => 21310, + 11311 => 21311, + 11312 => 21312, + 11313 => 21313, + 11314 => 21314, + 11315 => 21315, + 11316 => 21316, + 11317 => 21317, + 11318 => 21318, + 11319 => 21319, + 11320 => 21320, + 11321 => 21321, + 11322 => 21322, + 11323 => 21323, + 11324 => 21324, + 11325 => 21325, + 11326 => 21326, + 11327 => 21327, + 11328 => 21328, + 11329 => 21329, + 11330 => 21330, + 11331 => 21331, + 11332 => 21332, + 11333 => 21333, + 11334 => 21334, + 11335 => 21335, + 11336 => 21336, + 11337 => 21337, + 11338 => 21338, + 11339 => 21339, + 11340 => 21340, + 11341 => 21341, + 11342 => 21342, + 11343 => 21343, + 11344 => 21344, + 11345 => 21345, + 11346 => 21346, + 11347 => 21347, + 11348 => 21348, + 11349 => 21349, + 11350 => 21350, + 11351 => 21351, + 11352 => 21352, + 11353 => 21353, + 11354 => 21354, + 11355 => 21355, + 11356 => 21356, + 11357 => 21357, + 11358 => 21358, + 11359 => 21359, + 11360 => 21360, + 11361 => 21361, + 11362 => 21362, + 11363 => 21363, + 11364 => 21364, + 11365 => 21365, + 11366 => 21366, + 11367 => 21367, + 11368 => 21368, + 11369 => 21369, + 11370 => 21370, + 11371 => 21371, + 11372 => 21372, + 11373 => 21373, + 11374 => 21374, + 11375 => 21375, + 11376 => 21376, + 11377 => 21377, + 11378 => 21378, + 11379 => 21379, + 11380 => 21380, + 11381 => 21381, + 11382 => 21382, + 11383 => 21383, + 11384 => 21384, + 11385 => 21385, + 11386 => 21386, + 11387 => 21387, + 11388 => 21388, + 11389 => 21389, + 11390 => 21390, + 11391 => 21391, + 11392 => 21392, + 11393 => 21393, + 11394 => 21394, + 11395 => 21395, + 11396 => 21396, + 11397 => 21397, + 11398 => 21398, + 11399 => 21399, + 11400 => 21400, + 11401 => 21401, + 11402 => 21402, + 11403 => 21403, + 11404 => 21404, + 11405 => 21405, + 11406 => 21406, + 11407 => 21407, + 11408 => 21408, + 11409 => 21409, + 11410 => 21410, + 11411 => 21411, + 11412 => 21412, + 11413 => 21413, + 11414 => 21414, + 11415 => 21415, + 11416 => 21416, + 11417 => 21417, + 11418 => 21418, + 11419 => 21419, + 11420 => 21420, + 11421 => 21421, + 11422 => 21422, + 11423 => 21423, + 11424 => 21424, + 11425 => 21425, + 11426 => 21426, + 11427 => 21427, + 11428 => 21428, + 11429 => 21429, + 11430 => 21430, + 11431 => 21431, + 11432 => 21432, + 11433 => 21433, + 11434 => 21434, + 11435 => 21435, + 11436 => 21436, + 11437 => 21437, + 11438 => 21438, + 11439 => 21439, + 11440 => 21440, + 11441 => 21441, + 11442 => 21442, + 11443 => 21443, + 11444 => 21444, + 11445 => 21445, + 11446 => 21446, + 11447 => 21447, + 11448 => 21448, + 11449 => 21449, + 11450 => 21450, + 11451 => 21451, + 11452 => 21452, + 11453 => 21453, + 11454 => 21454, + 11455 => 21455, + 11456 => 21456, + 11457 => 21457, + 11458 => 21458, + 11459 => 21459, + 11460 => 21460, + 11461 => 21461, + 11462 => 21462, + 11463 => 21463, + 11464 => 21464, + 11465 => 21465, + 11466 => 21466, + 11467 => 21467, + 11468 => 21468, + 11469 => 21469, + 11470 => 21470, + 11471 => 21471, + 11472 => 21472, + 11473 => 21473, + 11474 => 21474, + 11475 => 21475, + 11476 => 21476, + 11477 => 21477, + 11478 => 21478, + 11479 => 21479, + 11480 => 21480, + 11481 => 21481, + 11482 => 21482, + 11483 => 21483, + 11484 => 21484, + 11485 => 21485, + 11486 => 21486, + 11487 => 21487, + 11488 => 21488, + 11489 => 21489, + 11490 => 21490, + 11491 => 21491, + 11492 => 21492, + 11493 => 21493, + 11494 => 21494, + 11495 => 21495, + 11496 => 21496, + 11497 => 21497, + 11498 => 21498, + 11499 => 21499, + 11500 => 21500, + 11501 => 21501, + 11502 => 21502, + 11503 => 21503, + 11504 => 21504, + 11505 => 21505, + 11506 => 21506, + 11507 => 21507, + 11508 => 21508, + 11509 => 21509, + 11510 => 21510, + 11511 => 21511, + 11512 => 21512, + 11513 => 21513, + 11514 => 21514, + 11515 => 21515, + 11516 => 21516, + 11517 => 21517, + 11518 => 21518, + 11519 => 21519, + 11520 => 21520, + 11521 => 21521, + 11522 => 21522, + 11523 => 21523, + 11524 => 21524, + 11525 => 21525, + 11526 => 21526, + 11527 => 21527, + 11528 => 21528, + 11529 => 21529, + 11530 => 21530, + 11531 => 21531, + 11532 => 21532, + 11533 => 21533, + 11534 => 21534, + 11535 => 21535, + 11536 => 21536, + 11537 => 21537, + 11538 => 21538, + 11539 => 21539, + 11540 => 21540, + 11541 => 21541, + 11542 => 21542, + 11543 => 21543, + 11544 => 21544, + 11545 => 21545, + 11546 => 21546, + 11547 => 21547, + 11548 => 21548, + 11549 => 21549, + 11550 => 21550, + 11551 => 21551, + 11552 => 21552, + 11553 => 21553, + 11554 => 21554, + 11555 => 21555, + 11556 => 21556, + 11557 => 21557, + 11558 => 21558, + 11559 => 21559, + 11560 => 21560, + 11561 => 21561, + 11562 => 21562, + 11563 => 21563, + 11564 => 21564, + 11565 => 21565, + 11566 => 21566, + 11567 => 21567, + 11568 => 21568, + 11569 => 21569, + 11570 => 21570, + 11571 => 21571, + 11572 => 21572, + 11573 => 21573, + 11574 => 21574, + 11575 => 21575, + 11576 => 21576, + 11577 => 21577, + 11578 => 21578, + 11579 => 21579, + 11580 => 21580, + 11581 => 21581, + 11582 => 21582, + 11583 => 21583, + 11584 => 21584, + 11585 => 21585, + 11586 => 21586, + 11587 => 21587, + 11588 => 21588, + 11589 => 21589, + 11590 => 21590, + 11591 => 21591, + 11592 => 21592, + 11593 => 21593, + 11594 => 21594, + 11595 => 21595, + 11596 => 21596, + 11597 => 21597, + 11598 => 21598, + 11599 => 21599, + 11600 => 21600, + 11601 => 21601, + 11602 => 21602, + 11603 => 21603, + 11604 => 21604, + 11605 => 21605, + 11606 => 21606, + 11607 => 21607, + 11608 => 21608, + 11609 => 21609, + 11610 => 21610, + 11611 => 21611, + 11612 => 21612, + 11613 => 21613, + 11614 => 21614, + 11615 => 21615, + 11616 => 21616, + 11617 => 21617, + 11618 => 21618, + 11619 => 21619, + 11620 => 21620, + 11621 => 21621, + 11622 => 21622, + 11623 => 21623, + 11624 => 21624, + 11625 => 21625, + 11626 => 21626, + 11627 => 21627, + 11628 => 21628, + 11629 => 21629, + 11630 => 21630, + 11631 => 21631, + 11632 => 21632, + 11633 => 21633, + 11634 => 21634, + 11635 => 21635, + 11636 => 21636, + 11637 => 21637, + 11638 => 21638, + 11639 => 21639, + 11640 => 21640, + 11641 => 21641, + 11642 => 21642, + 11643 => 21643, + 11644 => 21644, + 11645 => 21645, + 11646 => 21646, + 11647 => 21647, + 11648 => 21648, + 11649 => 21649, + 11650 => 21650, + 11651 => 21651, + 11652 => 21652, + 11653 => 21653, + 11654 => 21654, + 11655 => 21655, + 11656 => 21656, + 11657 => 21657, + 11658 => 21658, + 11659 => 21659, + 11660 => 21660, + 11661 => 21661, + 11662 => 21662, + 11663 => 21663, + 11664 => 21664, + 11665 => 21665, + 11666 => 21666, + 11667 => 21667, + 11668 => 21668, + 11669 => 21669, + 11670 => 21670, + 11671 => 21671, + 11672 => 21672, + 11673 => 21673, + 11674 => 21674, + 11675 => 21675, + 11676 => 21676, + 11677 => 21677, + 11678 => 21678, + 11679 => 21679, + 11680 => 21680, + 11681 => 21681, + 11682 => 21682, + 11683 => 21683, + 11684 => 21684, + 11685 => 21685, + 11686 => 21686, + 11687 => 21687, + 11688 => 21688, + 11689 => 21689, + 11690 => 21690, + 11691 => 21691, + 11692 => 21692, + 11693 => 21693, + 11694 => 21694, + 11695 => 21695, + 11696 => 21696, + 11697 => 21697, + 11698 => 21698, + 11699 => 21699, + 11700 => 21700, + 11701 => 21701, + 11702 => 21702, + 11703 => 21703, + 11704 => 21704, + 11705 => 21705, + 11706 => 21706, + 11707 => 21707, + 11708 => 21708, + 11709 => 21709, + 11710 => 21710, + 11711 => 21711, + 11712 => 21712, + 11713 => 21713, + 11714 => 21714, + 11715 => 21715, + 11716 => 21716, + 11717 => 21717, + 11718 => 21718, + 11719 => 21719, + 11720 => 21720, + 11721 => 21721, + 11722 => 21722, + 11723 => 21723, + 11724 => 21724, + 11725 => 21725, + 11726 => 21726, + 11727 => 21727, + 11728 => 21728, + 11729 => 21729, + 11730 => 21730, + 11731 => 21731, + 11732 => 21732, + 11733 => 21733, + 11734 => 21734, + 11735 => 21735, + 11736 => 21736, + 11737 => 21737, + 11738 => 21738, + 11739 => 21739, + 11740 => 21740, + 11741 => 21741, + 11742 => 21742, + 11743 => 21743, + 11744 => 21744, + 11745 => 21745, + 11746 => 21746, + 11747 => 21747, + 11748 => 21748, + 11749 => 21749, + 11750 => 21750, + 11751 => 21751, + 11752 => 21752, + 11753 => 21753, + 11754 => 21754, + 11755 => 21755, + 11756 => 21756, + 11757 => 21757, + 11758 => 21758, + 11759 => 21759, + 11760 => 21760, + 11761 => 21761, + 11762 => 21762, + 11763 => 21763, + 11764 => 21764, + 11765 => 21765, + 11766 => 21766, + 11767 => 21767, + 11768 => 21768, + 11769 => 21769, + 11770 => 21770, + 11771 => 21771, + 11772 => 21772, + 11773 => 21773, + 11774 => 21774, + 11775 => 21775, + 11776 => 21776, + 11777 => 21777, + 11778 => 21778, + 11779 => 21779, + 11780 => 21780, + 11781 => 21781, + 11782 => 21782, + 11783 => 21783, + 11784 => 21784, + 11785 => 21785, + 11786 => 21786, + 11787 => 21787, + 11788 => 21788, + 11789 => 21789, + 11790 => 21790, + 11791 => 21791, + 11792 => 21792, + 11793 => 21793, + 11794 => 21794, + 11795 => 21795, + 11796 => 21796, + 11797 => 21797, + 11798 => 21798, + 11799 => 21799, + 11800 => 21800, + 11801 => 21801, + 11802 => 21802, + 11803 => 21803, + 11804 => 21804, + 11805 => 21805, + 11806 => 21806, + 11807 => 21807, + 11808 => 21808, + 11809 => 21809, + 11810 => 21810, + 11811 => 21811, + 11812 => 21812, + 11813 => 21813, + 11814 => 21814, + 11815 => 21815, + 11816 => 21816, + 11817 => 21817, + 11818 => 21818, + 11819 => 21819, + 11820 => 21820, + 11821 => 21821, + 11822 => 21822, + 11823 => 21823, + 11824 => 21824, + 11825 => 21825, + 11826 => 21826, + 11827 => 21827, + 11828 => 21828, + 11829 => 21829, + 11830 => 21830, + 11831 => 21831, + 11832 => 21832, + 11833 => 21833, + 11834 => 21834, + 11835 => 21835, + 11836 => 21836, + 11837 => 21837, + 11838 => 21838, + 11839 => 21839, + 11840 => 21840, + 11841 => 21841, + 11842 => 21842, + 11843 => 21843, + 11844 => 21844, + 11845 => 21845, + 11846 => 21846, + 11847 => 21847, + 11848 => 21848, + 11849 => 21849, + 11850 => 21850, + 11851 => 21851, + 11852 => 21852, + 11853 => 21853, + 11854 => 21854, + 11855 => 21855, + 11856 => 21856, + 11857 => 21857, + 11858 => 21858, + 11859 => 21859, + 11860 => 21860, + 11861 => 21861, + 11862 => 21862, + 11863 => 21863, + 11864 => 21864, + 11865 => 21865, + 11866 => 21866, + 11867 => 21867, + 11868 => 21868, + 11869 => 21869, + 11870 => 21870, + 11871 => 21871, + 11872 => 21872, + 11873 => 21873, + 11874 => 21874, + 11875 => 21875, + 11876 => 21876, + 11877 => 21877, + 11878 => 21878, + 11879 => 21879, + 11880 => 21880, + 11881 => 21881, + 11882 => 21882, + 11883 => 21883, + 11884 => 21884, + 11885 => 21885, + 11886 => 21886, + 11887 => 21887, + 11888 => 21888, + 11889 => 21889, + 11890 => 21890, + 11891 => 21891, + 11892 => 21892, + 11893 => 21893, + 11894 => 21894, + 11895 => 21895, + 11896 => 21896, + 11897 => 21897, + 11898 => 21898, + 11899 => 21899, + 11900 => 21900, + 11901 => 21901, + 11902 => 21902, + 11903 => 21903, + 11904 => 21904, + 11905 => 21905, + 11906 => 21906, + 11907 => 21907, + 11908 => 21908, + 11909 => 21909, + 11910 => 21910, + 11911 => 21911, + 11912 => 21912, + 11913 => 21913, + 11914 => 21914, + 11915 => 21915, + 11916 => 21916, + 11917 => 21917, + 11918 => 21918, + 11919 => 21919, + 11920 => 21920, + 11921 => 21921, + 11922 => 21922, + 11923 => 21923, + 11924 => 21924, + 11925 => 21925, + 11926 => 21926, + 11927 => 21927, + 11928 => 21928, + 11929 => 21929, + 11930 => 21930, + 11931 => 21931, + 11932 => 21932, + 11933 => 21933, + 11934 => 21934, + 11935 => 21935, + 11936 => 21936, + 11937 => 21937, + 11938 => 21938, + 11939 => 21939, + 11940 => 21940, + 11941 => 21941, + 11942 => 21942, + 11943 => 21943, + 11944 => 21944, + 11945 => 21945, + 11946 => 21946, + 11947 => 21947, + 11948 => 21948, + 11949 => 21949, + 11950 => 21950, + 11951 => 21951, + 11952 => 21952, + 11953 => 21953, + 11954 => 21954, + 11955 => 21955, + 11956 => 21956, + 11957 => 21957, + 11958 => 21958, + 11959 => 21959, + 11960 => 21960, + 11961 => 21961, + 11962 => 21962, + 11963 => 21963, + 11964 => 21964, + 11965 => 21965, + 11966 => 21966, + 11967 => 21967, + 11968 => 21968, + 11969 => 21969, + 11970 => 21970, + 11971 => 21971, + 11972 => 21972, + 11973 => 21973, + 11974 => 21974, + 11975 => 21975, + 11976 => 21976, + 11977 => 21977, + 11978 => 21978, + 11979 => 21979, + 11980 => 21980, + 11981 => 21981, + 11982 => 21982, + 11983 => 21983, + 11984 => 21984, + 11985 => 21985, + 11986 => 21986, + 11987 => 21987, + 11988 => 21988, + 11989 => 21989, + 11990 => 21990, + 11991 => 21991, + 11992 => 21992, + 11993 => 21993, + 11994 => 21994, + 11995 => 21995, + 11996 => 21996, + 11997 => 21997, + 11998 => 21998, + 11999 => 21999, + 12000 => 22000, + 12001 => 22001, + 12002 => 22002, + 12003 => 22003, + 12004 => 22004, + 12005 => 22005, + 12006 => 22006, + 12007 => 22007, + 12008 => 22008, + 12009 => 22009, + 12010 => 22010, + 12011 => 22011, + 12012 => 22012, + 12013 => 22013, + 12014 => 22014, + 12015 => 22015, + 12016 => 22016, + 12017 => 22017, + 12018 => 22018, + 12019 => 22019, + 12020 => 22020, + 12021 => 22021, + 12022 => 22022, + 12023 => 22023, + 12024 => 22024, + 12025 => 22025, + 12026 => 22026, + 12027 => 22027, + 12028 => 22028, + 12029 => 22029, + 12030 => 22030, + 12031 => 22031, + 12032 => 22032, + 12033 => 22033, + 12034 => 22034, + 12035 => 22035, + 12036 => 22036, + 12037 => 22037, + 12038 => 22038, + 12039 => 22039, + 12040 => 22040, + 12041 => 22041, + 12042 => 22042, + 12043 => 22043, + 12044 => 22044, + 12045 => 22045, + 12046 => 22046, + 12047 => 22047, + 12048 => 22048, + 12049 => 22049, + 12050 => 22050, + 12051 => 22051, + 12052 => 22052, + 12053 => 22053, + 12054 => 22054, + 12055 => 22055, + 12056 => 22056, + 12057 => 22057, + 12058 => 22058, + 12059 => 22059, + 12060 => 22060, + 12061 => 22061, + 12062 => 22062, + 12063 => 22063, + 12064 => 22064, + 12065 => 22065, + 12066 => 22066, + 12067 => 22067, + 12068 => 22068, + 12069 => 22069, + 12070 => 22070, + 12071 => 22071, + 12072 => 22072, + 12073 => 22073, + 12074 => 22074, + 12075 => 22075, + 12076 => 22076, + 12077 => 22077, + 12078 => 22078, + 12079 => 22079, + 12080 => 22080, + 12081 => 22081, + 12082 => 22082, + 12083 => 22083, + 12084 => 22084, + 12085 => 22085, + 12086 => 22086, + 12087 => 22087, + 12088 => 22088, + 12089 => 22089, + 12090 => 22090, + 12091 => 22091, + 12092 => 22092, + 12093 => 22093, + 12094 => 22094, + 12095 => 22095, + 12096 => 22096, + 12097 => 22097, + 12098 => 22098, + 12099 => 22099, + 12100 => 22100, + 12101 => 22101, + 12102 => 22102, + 12103 => 22103, + 12104 => 22104, + 12105 => 22105, + 12106 => 22106, + 12107 => 22107, + 12108 => 22108, + 12109 => 22109, + 12110 => 22110, + 12111 => 22111, + 12112 => 22112, + 12113 => 22113, + 12114 => 22114, + 12115 => 22115, + 12116 => 22116, + 12117 => 22117, + 12118 => 22118, + 12119 => 22119, + 12120 => 22120, + 12121 => 22121, + 12122 => 22122, + 12123 => 22123, + 12124 => 22124, + 12125 => 22125, + 12126 => 22126, + 12127 => 22127, + 12128 => 22128, + 12129 => 22129, + 12130 => 22130, + 12131 => 22131, + 12132 => 22132, + 12133 => 22133, + 12134 => 22134, + 12135 => 22135, + 12136 => 22136, + 12137 => 22137, + 12138 => 22138, + 12139 => 22139, + 12140 => 22140, + 12141 => 22141, + 12142 => 22142, + 12143 => 22143, + 12144 => 22144, + 12145 => 22145, + 12146 => 22146, + 12147 => 22147, + 12148 => 22148, + 12149 => 22149, + 12150 => 22150, + 12151 => 22151, + 12152 => 22152, + 12153 => 22153, + 12154 => 22154, + 12155 => 22155, + 12156 => 22156, + 12157 => 22157, + 12158 => 22158, + 12159 => 22159, + 12160 => 22160, + 12161 => 22161, + 12162 => 22162, + 12163 => 22163, + 12164 => 22164, + 12165 => 22165, + 12166 => 22166, + 12167 => 22167, + 12168 => 22168, + 12169 => 22169, + 12170 => 22170, + 12171 => 22171, + 12172 => 22172, + 12173 => 22173, + 12174 => 22174, + 12175 => 22175, + 12176 => 22176, + 12177 => 22177, + 12178 => 22178, + 12179 => 22179, + 12180 => 22180, + 12181 => 22181, + 12182 => 22182, + 12183 => 22183, + 12184 => 22184, + 12185 => 22185, + 12186 => 22186, + 12187 => 22187, + 12188 => 22188, + 12189 => 22189, + 12190 => 22190, + 12191 => 22191, + 12192 => 22192, + 12193 => 22193, + 12194 => 22194, + 12195 => 22195, + 12196 => 22196, + 12197 => 22197, + 12198 => 22198, + 12199 => 22199, + 12200 => 22200, + 12201 => 22201, + 12202 => 22202, + 12203 => 22203, + 12204 => 22204, + 12205 => 22205, + 12206 => 22206, + 12207 => 22207, + 12208 => 22208, + 12209 => 22209, + 12210 => 22210, + 12211 => 22211, + 12212 => 22212, + 12213 => 22213, + 12214 => 22214, + 12215 => 22215, + 12216 => 22216, + 12217 => 22217, + 12218 => 22218, + 12219 => 22219, + 12220 => 22220, + 12221 => 22221, + 12222 => 22222, + 12223 => 22223, + 12224 => 22224, + 12225 => 22225, + 12226 => 22226, + 12227 => 22227, + 12228 => 22228, + 12229 => 22229, + 12230 => 22230, + 12231 => 22231, + 12232 => 22232, + 12233 => 22233, + 12234 => 22234, + 12235 => 22235, + 12236 => 22236, + 12237 => 22237, + 12238 => 22238, + 12239 => 22239, + 12240 => 22240, + 12241 => 22241, + 12242 => 22242, + 12243 => 22243, + 12244 => 22244, + 12245 => 22245, + 12246 => 22246, + 12247 => 22247, + 12248 => 22248, + 12249 => 22249, + 12250 => 22250, + 12251 => 22251, + 12252 => 22252, + 12253 => 22253, + 12254 => 22254, + 12255 => 22255, + 12256 => 22256, + 12257 => 22257, + 12258 => 22258, + 12259 => 22259, + 12260 => 22260, + 12261 => 22261, + 12262 => 22262, + 12263 => 22263, + 12264 => 22264, + 12265 => 22265, + 12266 => 22266, + 12267 => 22267, + 12268 => 22268, + 12269 => 22269, + 12270 => 22270, + 12271 => 22271, + 12272 => 22272, + 12273 => 22273, + 12274 => 22274, + 12275 => 22275, + 12276 => 22276, + 12277 => 22277, + 12278 => 22278, + 12279 => 22279, + 12280 => 22280, + 12281 => 22281, + 12282 => 22282, + 12283 => 22283, + 12284 => 22284, + 12285 => 22285, + 12286 => 22286, + 12287 => 22287, + 12288 => 22288, + 12289 => 22289, + 12290 => 22290, + 12291 => 22291, + 12292 => 22292, + 12293 => 22293, + 12294 => 22294, + 12295 => 22295, + 12296 => 22296, + 12297 => 22297, + 12298 => 22298, + 12299 => 22299, + 12300 => 22300, + 12301 => 22301, + 12302 => 22302, + 12303 => 22303, + 12304 => 22304, + 12305 => 22305, + 12306 => 22306, + 12307 => 22307, + 12308 => 22308, + 12309 => 22309, + 12310 => 22310, + 12311 => 22311, + 12312 => 22312, + 12313 => 22313, + 12314 => 22314, + 12315 => 22315, + 12316 => 22316, + 12317 => 22317, + 12318 => 22318, + 12319 => 22319, + 12320 => 22320, + 12321 => 22321, + 12322 => 22322, + 12323 => 22323, + 12324 => 22324, + 12325 => 22325, + 12326 => 22326, + 12327 => 22327, + 12328 => 22328, + 12329 => 22329, + 12330 => 22330, + 12331 => 22331, + 12332 => 22332, + 12333 => 22333, + 12334 => 22334, + 12335 => 22335, + 12336 => 22336, + 12337 => 22337, + 12338 => 22338, + 12339 => 22339, + 12340 => 22340, + 12341 => 22341, + 12342 => 22342, + 12343 => 22343, + 12344 => 22344, + 12345 => 22345, + 12346 => 22346, + 12347 => 22347, + 12348 => 22348, + 12349 => 22349, + 12350 => 22350, + 12351 => 22351, + 12352 => 22352, + 12353 => 22353, + 12354 => 22354, + 12355 => 22355, + 12356 => 22356, + 12357 => 22357, + 12358 => 22358, + 12359 => 22359, + 12360 => 22360, + 12361 => 22361, + 12362 => 22362, + 12363 => 22363, + 12364 => 22364, + 12365 => 22365, + 12366 => 22366, + 12367 => 22367, + 12368 => 22368, + 12369 => 22369, + 12370 => 22370, + 12371 => 22371, + 12372 => 22372, + 12373 => 22373, + 12374 => 22374, + 12375 => 22375, + 12376 => 22376, + 12377 => 22377, + 12378 => 22378, + 12379 => 22379, + 12380 => 22380, + 12381 => 22381, + 12382 => 22382, + 12383 => 22383, + 12384 => 22384, + 12385 => 22385, + 12386 => 22386, + 12387 => 22387, + 12388 => 22388, + 12389 => 22389, + 12390 => 22390, + 12391 => 22391, + 12392 => 22392, + 12393 => 22393, + 12394 => 22394, + 12395 => 22395, + 12396 => 22396, + 12397 => 22397, + 12398 => 22398, + 12399 => 22399, + 12400 => 22400, + 12401 => 22401, + 12402 => 22402, + 12403 => 22403, + 12404 => 22404, + 12405 => 22405, + 12406 => 22406, + 12407 => 22407, + 12408 => 22408, + 12409 => 22409, + 12410 => 22410, + 12411 => 22411, + 12412 => 22412, + 12413 => 22413, + 12414 => 22414, + 12415 => 22415, + 12416 => 22416, + 12417 => 22417, + 12418 => 22418, + 12419 => 22419, + 12420 => 22420, + 12421 => 22421, + 12422 => 22422, + 12423 => 22423, + 12424 => 22424, + 12425 => 22425, + 12426 => 22426, + 12427 => 22427, + 12428 => 22428, + 12429 => 22429, + 12430 => 22430, + 12431 => 22431, + 12432 => 22432, + 12433 => 22433, + 12434 => 22434, + 12435 => 22435, + 12436 => 22436, + 12437 => 22437, + 12438 => 22438, + 12439 => 22439, + 12440 => 22440, + 12441 => 22441, + 12442 => 22442, + 12443 => 22443, + 12444 => 22444, + 12445 => 22445, + 12446 => 22446, + 12447 => 22447, + 12448 => 22448, + 12449 => 22449, + 12450 => 22450, + 12451 => 22451, + 12452 => 22452, + 12453 => 22453, + 12454 => 22454, + 12455 => 22455, + 12456 => 22456, + 12457 => 22457, + 12458 => 22458, + 12459 => 22459, + 12460 => 22460, + 12461 => 22461, + 12462 => 22462, + 12463 => 22463, + 12464 => 22464, + 12465 => 22465, + 12466 => 22466, + 12467 => 22467, + 12468 => 22468, + 12469 => 22469, + 12470 => 22470, + 12471 => 22471, + 12472 => 22472, + 12473 => 22473, + 12474 => 22474, + 12475 => 22475, + 12476 => 22476, + 12477 => 22477, + 12478 => 22478, + 12479 => 22479, + 12480 => 22480, + 12481 => 22481, + 12482 => 22482, + 12483 => 22483, + 12484 => 22484, + 12485 => 22485, + 12486 => 22486, + 12487 => 22487, + 12488 => 22488, + 12489 => 22489, + 12490 => 22490, + 12491 => 22491, + 12492 => 22492, + 12493 => 22493, + 12494 => 22494, + 12495 => 22495, + 12496 => 22496, + 12497 => 22497, + 12498 => 22498, + 12499 => 22499, + 12500 => 22500, + 12501 => 22501, + 12502 => 22502, + 12503 => 22503, + 12504 => 22504, + 12505 => 22505, + 12506 => 22506, + 12507 => 22507, + 12508 => 22508, + 12509 => 22509, + 12510 => 22510, + 12511 => 22511, + 12512 => 22512, + 12513 => 22513, + 12514 => 22514, + 12515 => 22515, + 12516 => 22516, + 12517 => 22517, + 12518 => 22518, + 12519 => 22519, + 12520 => 22520, + 12521 => 22521, + 12522 => 22522, + 12523 => 22523, + 12524 => 22524, + 12525 => 22525, + 12526 => 22526, + 12527 => 22527, + 12528 => 22528, + 12529 => 22529, + 12530 => 22530, + 12531 => 22531, + 12532 => 22532, + 12533 => 22533, + 12534 => 22534, + 12535 => 22535, + 12536 => 22536, + 12537 => 22537, + 12538 => 22538, + 12539 => 22539, + 12540 => 22540, + 12541 => 22541, + 12542 => 22542, + 12543 => 22543, + 12544 => 22544, + 12545 => 22545, + 12546 => 22546, + 12547 => 22547, + 12548 => 22548, + 12549 => 22549, + 12550 => 22550, + 12551 => 22551, + 12552 => 22552, + 12553 => 22553, + 12554 => 22554, + 12555 => 22555, + 12556 => 22556, + 12557 => 22557, + 12558 => 22558, + 12559 => 22559, + 12560 => 22560, + 12561 => 22561, + 12562 => 22562, + 12563 => 22563, + 12564 => 22564, + 12565 => 22565, + 12566 => 22566, + 12567 => 22567, + 12568 => 22568, + 12569 => 22569, + 12570 => 22570, + 12571 => 22571, + 12572 => 22572, + 12573 => 22573, + 12574 => 22574, + 12575 => 22575, + 12576 => 22576, + 12577 => 22577, + 12578 => 22578, + 12579 => 22579, + 12580 => 22580, + 12581 => 22581, + 12582 => 22582, + 12583 => 22583, + 12584 => 22584, + 12585 => 22585, + 12586 => 22586, + 12587 => 22587, + 12588 => 22588, + 12589 => 22589, + 12590 => 22590, + 12591 => 22591, + 12592 => 22592, + 12593 => 22593, + 12594 => 22594, + 12595 => 22595, + 12596 => 22596, + 12597 => 22597, + 12598 => 22598, + 12599 => 22599, + 12600 => 22600, + 12601 => 22601, + 12602 => 22602, + 12603 => 22603, + 12604 => 22604, + 12605 => 22605, + 12606 => 22606, + 12607 => 22607, + 12608 => 22608, + 12609 => 22609, + 12610 => 22610, + 12611 => 22611, + 12612 => 22612, + 12613 => 22613, + 12614 => 22614, + 12615 => 22615, + 12616 => 22616, + 12617 => 22617, + 12618 => 22618, + 12619 => 22619, + 12620 => 22620, + 12621 => 22621, + 12622 => 22622, + 12623 => 22623, + 12624 => 22624, + 12625 => 22625, + 12626 => 22626, + 12627 => 22627, + 12628 => 22628, + 12629 => 22629, + 12630 => 22630, + 12631 => 22631, + 12632 => 22632, + 12633 => 22633, + 12634 => 22634, + 12635 => 22635, + 12636 => 22636, + 12637 => 22637, + 12638 => 22638, + 12639 => 22639, + 12640 => 22640, + 12641 => 22641, + 12642 => 22642, + 12643 => 22643, + 12644 => 22644, + 12645 => 22645, + 12646 => 22646, + 12647 => 22647, + 12648 => 22648, + 12649 => 22649, + 12650 => 22650, + 12651 => 22651, + 12652 => 22652, + 12653 => 22653, + 12654 => 22654, + 12655 => 22655, + 12656 => 22656, + 12657 => 22657, + 12658 => 22658, + 12659 => 22659, + 12660 => 22660, + 12661 => 22661, + 12662 => 22662, + 12663 => 22663, + 12664 => 22664, + 12665 => 22665, + 12666 => 22666, + 12667 => 22667, + 12668 => 22668, + 12669 => 22669, + 12670 => 22670, + 12671 => 22671, + 12672 => 22672, + 12673 => 22673, + 12674 => 22674, + 12675 => 22675, + 12676 => 22676, + 12677 => 22677, + 12678 => 22678, + 12679 => 22679, + 12680 => 22680, + 12681 => 22681, + 12682 => 22682, + 12683 => 22683, + 12684 => 22684, + 12685 => 22685, + 12686 => 22686, + 12687 => 22687, + 12688 => 22688, + 12689 => 22689, + 12690 => 22690, + 12691 => 22691, + 12692 => 22692, + 12693 => 22693, + 12694 => 22694, + 12695 => 22695, + 12696 => 22696, + 12697 => 22697, + 12698 => 22698, + 12699 => 22699, + 12700 => 22700, + 12701 => 22701, + 12702 => 22702, + 12703 => 22703, + 12704 => 22704, + 12705 => 22705, + 12706 => 22706, + 12707 => 22707, + 12708 => 22708, + 12709 => 22709, + 12710 => 22710, + 12711 => 22711, + 12712 => 22712, + 12713 => 22713, + 12714 => 22714, + 12715 => 22715, + 12716 => 22716, + 12717 => 22717, + 12718 => 22718, + 12719 => 22719, + 12720 => 22720, + 12721 => 22721, + 12722 => 22722, + 12723 => 22723, + 12724 => 22724, + 12725 => 22725, + 12726 => 22726, + 12727 => 22727, + 12728 => 22728, + 12729 => 22729, + 12730 => 22730, + 12731 => 22731, + 12732 => 22732, + 12733 => 22733, + 12734 => 22734, + 12735 => 22735, + 12736 => 22736, + 12737 => 22737, + 12738 => 22738, + 12739 => 22739, + 12740 => 22740, + 12741 => 22741, + 12742 => 22742, + 12743 => 22743, + 12744 => 22744, + 12745 => 22745, + 12746 => 22746, + 12747 => 22747, + 12748 => 22748, + 12749 => 22749, + 12750 => 22750, + 12751 => 22751, + 12752 => 22752, + 12753 => 22753, + 12754 => 22754, + 12755 => 22755, + 12756 => 22756, + 12757 => 22757, + 12758 => 22758, + 12759 => 22759, + 12760 => 22760, + 12761 => 22761, + 12762 => 22762, + 12763 => 22763, + 12764 => 22764, + 12765 => 22765, + 12766 => 22766, + 12767 => 22767, + 12768 => 22768, + 12769 => 22769, + 12770 => 22770, + 12771 => 22771, + 12772 => 22772, + 12773 => 22773, + 12774 => 22774, + 12775 => 22775, + 12776 => 22776, + 12777 => 22777, + 12778 => 22778, + 12779 => 22779, + 12780 => 22780, + 12781 => 22781, + 12782 => 22782, + 12783 => 22783, + 12784 => 22784, + 12785 => 22785, + 12786 => 22786, + 12787 => 22787, + 12788 => 22788, + 12789 => 22789, + 12790 => 22790, + 12791 => 22791, + 12792 => 22792, + 12793 => 22793, + 12794 => 22794, + 12795 => 22795, + 12796 => 22796, + 12797 => 22797, + 12798 => 22798, + 12799 => 22799, + 12800 => 22800, + 12801 => 22801, + 12802 => 22802, + 12803 => 22803, + 12804 => 22804, + 12805 => 22805, + 12806 => 22806, + 12807 => 22807, + 12808 => 22808, + 12809 => 22809, + 12810 => 22810, + 12811 => 22811, + 12812 => 22812, + 12813 => 22813, + 12814 => 22814, + 12815 => 22815, + 12816 => 22816, + 12817 => 22817, + 12818 => 22818, + 12819 => 22819, + 12820 => 22820, + 12821 => 22821, + 12822 => 22822, + 12823 => 22823, + 12824 => 22824, + 12825 => 22825, + 12826 => 22826, + 12827 => 22827, + 12828 => 22828, + 12829 => 22829, + 12830 => 22830, + 12831 => 22831, + 12832 => 22832, + 12833 => 22833, + 12834 => 22834, + 12835 => 22835, + 12836 => 22836, + 12837 => 22837, + 12838 => 22838, + 12839 => 22839, + 12840 => 22840, + 12841 => 22841, + 12842 => 22842, + 12843 => 22843, + 12844 => 22844, + 12845 => 22845, + 12846 => 22846, + 12847 => 22847, + 12848 => 22848, + 12849 => 22849, + 12850 => 22850, + 12851 => 22851, + 12852 => 22852, + 12853 => 22853, + 12854 => 22854, + 12855 => 22855, + 12856 => 22856, + 12857 => 22857, + 12858 => 22858, + 12859 => 22859, + 12860 => 22860, + 12861 => 22861, + 12862 => 22862, + 12863 => 22863, + 12864 => 22864, + 12865 => 22865, + 12866 => 22866, + 12867 => 22867, + 12868 => 22868, + 12869 => 22869, + 12870 => 22870, + 12871 => 22871, + 12872 => 22872, + 12873 => 22873, + 12874 => 22874, + 12875 => 22875, + 12876 => 22876, + 12877 => 22877, + 12878 => 22878, + 12879 => 22879, + 12880 => 22880, + 12881 => 22881, + 12882 => 22882, + 12883 => 22883, + 12884 => 22884, + 12885 => 22885, + 12886 => 22886, + 12887 => 22887, + 12888 => 22888, + 12889 => 22889, + 12890 => 22890, + 12891 => 22891, + 12892 => 22892, + 12893 => 22893, + 12894 => 22894, + 12895 => 22895, + 12896 => 22896, + 12897 => 22897, + 12898 => 22898, + 12899 => 22899, + 12900 => 22900, + 12901 => 22901, + 12902 => 22902, + 12903 => 22903, + 12904 => 22904, + 12905 => 22905, + 12906 => 22906, + 12907 => 22907, + 12908 => 22908, + 12909 => 22909, + 12910 => 22910, + 12911 => 22911, + 12912 => 22912, + 12913 => 22913, + 12914 => 22914, + 12915 => 22915, + 12916 => 22916, + 12917 => 22917, + 12918 => 22918, + 12919 => 22919, + 12920 => 22920, + 12921 => 22921, + 12922 => 22922, + 12923 => 22923, + 12924 => 22924, + 12925 => 22925, + 12926 => 22926, + 12927 => 22927, + 12928 => 22928, + 12929 => 22929, + 12930 => 22930, + 12931 => 22931, + 12932 => 22932, + 12933 => 22933, + 12934 => 22934, + 12935 => 22935, + 12936 => 22936, + 12937 => 22937, + 12938 => 22938, + 12939 => 22939, + 12940 => 22940, + 12941 => 22941, + 12942 => 22942, + 12943 => 22943, + 12944 => 22944, + 12945 => 22945, + 12946 => 22946, + 12947 => 22947, + 12948 => 22948, + 12949 => 22949, + 12950 => 22950, + 12951 => 22951, + 12952 => 22952, + 12953 => 22953, + 12954 => 22954, + 12955 => 22955, + 12956 => 22956, + 12957 => 22957, + 12958 => 22958, + 12959 => 22959, + 12960 => 22960, + 12961 => 22961, + 12962 => 22962, + 12963 => 22963, + 12964 => 22964, + 12965 => 22965, + 12966 => 22966, + 12967 => 22967, + 12968 => 22968, + 12969 => 22969, + 12970 => 22970, + 12971 => 22971, + 12972 => 22972, + 12973 => 22973, + 12974 => 22974, + 12975 => 22975, + 12976 => 22976, + 12977 => 22977, + 12978 => 22978, + 12979 => 22979, + 12980 => 22980, + 12981 => 22981, + 12982 => 22982, + 12983 => 22983, + 12984 => 22984, + 12985 => 22985, + 12986 => 22986, + 12987 => 22987, + 12988 => 22988, + 12989 => 22989, + 12990 => 22990, + 12991 => 22991, + 12992 => 22992, + 12993 => 22993, + 12994 => 22994, + 12995 => 22995, + 12996 => 22996, + 12997 => 22997, + 12998 => 22998, + 12999 => 22999, + 13000 => 23000, + 13001 => 23001, + 13002 => 23002, + 13003 => 23003, + 13004 => 23004, + 13005 => 23005, + 13006 => 23006, + 13007 => 23007, + 13008 => 23008, + 13009 => 23009, + 13010 => 23010, + 13011 => 23011, + 13012 => 23012, + 13013 => 23013, + 13014 => 23014, + 13015 => 23015, + 13016 => 23016, + 13017 => 23017, + 13018 => 23018, + 13019 => 23019, + 13020 => 23020, + 13021 => 23021, + 13022 => 23022, + 13023 => 23023, + 13024 => 23024, + 13025 => 23025, + 13026 => 23026, + 13027 => 23027, + 13028 => 23028, + 13029 => 23029, + 13030 => 23030, + 13031 => 23031, + 13032 => 23032, + 13033 => 23033, + 13034 => 23034, + 13035 => 23035, + 13036 => 23036, + 13037 => 23037, + 13038 => 23038, + 13039 => 23039, + 13040 => 23040, + 13041 => 23041, + 13042 => 23042, + 13043 => 23043, + 13044 => 23044, + 13045 => 23045, + 13046 => 23046, + 13047 => 23047, + 13048 => 23048, + 13049 => 23049, + 13050 => 23050, + 13051 => 23051, + 13052 => 23052, + 13053 => 23053, + 13054 => 23054, + 13055 => 23055, + 13056 => 23056, + 13057 => 23057, + 13058 => 23058, + 13059 => 23059, + 13060 => 23060, + 13061 => 23061, + 13062 => 23062, + 13063 => 23063, + 13064 => 23064, + 13065 => 23065, + 13066 => 23066, + 13067 => 23067, + 13068 => 23068, + 13069 => 23069, + 13070 => 23070, + 13071 => 23071, + 13072 => 23072, + 13073 => 23073, + 13074 => 23074, + 13075 => 23075, + 13076 => 23076, + 13077 => 23077, + 13078 => 23078, + 13079 => 23079, + 13080 => 23080, + 13081 => 23081, + 13082 => 23082, + 13083 => 23083, + 13084 => 23084, + 13085 => 23085, + 13086 => 23086, + 13087 => 23087, + 13088 => 23088, + 13089 => 23089, + 13090 => 23090, + 13091 => 23091, + 13092 => 23092, + 13093 => 23093, + 13094 => 23094, + 13095 => 23095, + 13096 => 23096, + 13097 => 23097, + 13098 => 23098, + 13099 => 23099, + 13100 => 23100, + 13101 => 23101, + 13102 => 23102, + 13103 => 23103, + 13104 => 23104, + 13105 => 23105, + 13106 => 23106, + 13107 => 23107, + 13108 => 23108, + 13109 => 23109, + 13110 => 23110, + 13111 => 23111, + 13112 => 23112, + 13113 => 23113, + 13114 => 23114, + 13115 => 23115, + 13116 => 23116, + 13117 => 23117, + 13118 => 23118, + 13119 => 23119, + 13120 => 23120, + 13121 => 23121, + 13122 => 23122, + 13123 => 23123, + 13124 => 23124, + 13125 => 23125, + 13126 => 23126, + 13127 => 23127, + 13128 => 23128, + 13129 => 23129, + 13130 => 23130, + 13131 => 23131, + 13132 => 23132, + 13133 => 23133, + 13134 => 23134, + 13135 => 23135, + 13136 => 23136, + 13137 => 23137, + 13138 => 23138, + 13139 => 23139, + 13140 => 23140, + 13141 => 23141, + 13142 => 23142, + 13143 => 23143, + 13144 => 23144, + 13145 => 23145, + 13146 => 23146, + 13147 => 23147, + 13148 => 23148, + 13149 => 23149, + 13150 => 23150, + 13151 => 23151, + 13152 => 23152, + 13153 => 23153, + 13154 => 23154, + 13155 => 23155, + 13156 => 23156, + 13157 => 23157, + 13158 => 23158, + 13159 => 23159, + 13160 => 23160, + 13161 => 23161, + 13162 => 23162, + 13163 => 23163, + 13164 => 23164, + 13165 => 23165, + 13166 => 23166, + 13167 => 23167, + 13168 => 23168, + 13169 => 23169, + 13170 => 23170, + 13171 => 23171, + 13172 => 23172, + 13173 => 23173, + 13174 => 23174, + 13175 => 23175, + 13176 => 23176, + 13177 => 23177, + 13178 => 23178, + 13179 => 23179, + 13180 => 23180, + 13181 => 23181, + 13182 => 23182, + 13183 => 23183, + 13184 => 23184, + 13185 => 23185, + 13186 => 23186, + 13187 => 23187, + 13188 => 23188, + 13189 => 23189, + 13190 => 23190, + 13191 => 23191, + 13192 => 23192, + 13193 => 23193, + 13194 => 23194, + 13195 => 23195, + 13196 => 23196, + 13197 => 23197, + 13198 => 23198, + 13199 => 23199, + 13200 => 23200, + 13201 => 23201, + 13202 => 23202, + 13203 => 23203, + 13204 => 23204, + 13205 => 23205, + 13206 => 23206, + 13207 => 23207, + 13208 => 23208, + 13209 => 23209, + 13210 => 23210, + 13211 => 23211, + 13212 => 23212, + 13213 => 23213, + 13214 => 23214, + 13215 => 23215, + 13216 => 23216, + 13217 => 23217, + 13218 => 23218, + 13219 => 23219, + 13220 => 23220, + 13221 => 23221, + 13222 => 23222, + 13223 => 23223, + 13224 => 23224, + 13225 => 23225, + 13226 => 23226, + 13227 => 23227, + 13228 => 23228, + 13229 => 23229, + 13230 => 23230, + 13231 => 23231, + 13232 => 23232, + 13233 => 23233, + 13234 => 23234, + 13235 => 23235, + 13236 => 23236, + 13237 => 23237, + 13238 => 23238, + 13239 => 23239, + 13240 => 23240, + 13241 => 23241, + 13242 => 23242, + 13243 => 23243, + 13244 => 23244, + 13245 => 23245, + 13246 => 23246, + 13247 => 23247, + 13248 => 23248, + 13249 => 23249, + 13250 => 23250, + 13251 => 23251, + 13252 => 23252, + 13253 => 23253, + 13254 => 23254, + 13255 => 23255, + 13256 => 23256, + 13257 => 23257, + 13258 => 23258, + 13259 => 23259, + 13260 => 23260, + 13261 => 23261, + 13262 => 23262, + 13263 => 23263, + 13264 => 23264, + 13265 => 23265, + 13266 => 23266, + 13267 => 23267, + 13268 => 23268, + 13269 => 23269, + 13270 => 23270, + 13271 => 23271, + 13272 => 23272, + 13273 => 23273, + 13274 => 23274, + 13275 => 23275, + 13276 => 23276, + 13277 => 23277, + 13278 => 23278, + 13279 => 23279, + 13280 => 23280, + 13281 => 23281, + 13282 => 23282, + 13283 => 23283, + 13284 => 23284, + 13285 => 23285, + 13286 => 23286, + 13287 => 23287, + 13288 => 23288, + 13289 => 23289, + 13290 => 23290, + 13291 => 23291, + 13292 => 23292, + 13293 => 23293, + 13294 => 23294, + 13295 => 23295, + 13296 => 23296, + 13297 => 23297, + 13298 => 23298, + 13299 => 23299, + 13300 => 23300, + 13301 => 23301, + 13302 => 23302, + 13303 => 23303, + 13304 => 23304, + 13305 => 23305, + 13306 => 23306, + 13307 => 23307, + 13308 => 23308, + 13309 => 23309, + 13310 => 23310, + 13311 => 23311, + 13312 => 23312, + 13313 => 23313, + 13314 => 23314, + 13315 => 23315, + 13316 => 23316, + 13317 => 23317, + 13318 => 23318, + 13319 => 23319, + 13320 => 23320, + 13321 => 23321, + 13322 => 23322, + 13323 => 23323, + 13324 => 23324, + 13325 => 23325, + 13326 => 23326, + 13327 => 23327, + 13328 => 23328, + 13329 => 23329, + 13330 => 23330, + 13331 => 23331, + 13332 => 23332, + 13333 => 23333, + 13334 => 23334, + 13335 => 23335, + 13336 => 23336, + 13337 => 23337, + 13338 => 23338, + 13339 => 23339, + 13340 => 23340, + 13341 => 23341, + 13342 => 23342, + 13343 => 23343, + 13344 => 23344, + 13345 => 23345, + 13346 => 23346, + 13347 => 23347, + 13348 => 23348, + 13349 => 23349, + 13350 => 23350, + 13351 => 23351, + 13352 => 23352, + 13353 => 23353, + 13354 => 23354, + 13355 => 23355, + 13356 => 23356, + 13357 => 23357, + 13358 => 23358, + 13359 => 23359, + 13360 => 23360, + 13361 => 23361, + 13362 => 23362, + 13363 => 23363, + 13364 => 23364, + 13365 => 23365, + 13366 => 23366, + 13367 => 23367, + 13368 => 23368, + 13369 => 23369, + 13370 => 23370, + 13371 => 23371, + 13372 => 23372, + 13373 => 23373, + 13374 => 23374, + 13375 => 23375, + 13376 => 23376, + 13377 => 23377, + 13378 => 23378, + 13379 => 23379, + 13380 => 23380, + 13381 => 23381, + 13382 => 23382, + 13383 => 23383, + 13384 => 23384, + 13385 => 23385, + 13386 => 23386, + 13387 => 23387, + 13388 => 23388, + 13389 => 23389, + 13390 => 23390, + 13391 => 23391, + 13392 => 23392, + 13393 => 23393, + 13394 => 23394, + 13395 => 23395, + 13396 => 23396, + 13397 => 23397, + 13398 => 23398, + 13399 => 23399, + 13400 => 23400, + 13401 => 23401, + 13402 => 23402, + 13403 => 23403, + 13404 => 23404, + 13405 => 23405, + 13406 => 23406, + 13407 => 23407, + 13408 => 23408, + 13409 => 23409, + 13410 => 23410, + 13411 => 23411, + 13412 => 23412, + 13413 => 23413, + 13414 => 23414, + 13415 => 23415, + 13416 => 23416, + 13417 => 23417, + 13418 => 23418, + 13419 => 23419, + 13420 => 23420, + 13421 => 23421, + 13422 => 23422, + 13423 => 23423, + 13424 => 23424, + 13425 => 23425, + 13426 => 23426, + 13427 => 23427, + 13428 => 23428, + 13429 => 23429, + 13430 => 23430, + 13431 => 23431, + 13432 => 23432, + 13433 => 23433, + 13434 => 23434, + 13435 => 23435, + 13436 => 23436, + 13437 => 23437, + 13438 => 23438, + 13439 => 23439, + 13440 => 23440, + 13441 => 23441, + 13442 => 23442, + 13443 => 23443, + 13444 => 23444, + 13445 => 23445, + 13446 => 23446, + 13447 => 23447, + 13448 => 23448, + 13449 => 23449, + 13450 => 23450, + 13451 => 23451, + 13452 => 23452, + 13453 => 23453, + 13454 => 23454, + 13455 => 23455, + 13456 => 23456, + 13457 => 23457, + 13458 => 23458, + 13459 => 23459, + 13460 => 23460, + 13461 => 23461, + 13462 => 23462, + 13463 => 23463, + 13464 => 23464, + 13465 => 23465, + 13466 => 23466, + 13467 => 23467, + 13468 => 23468, + 13469 => 23469, + 13470 => 23470, + 13471 => 23471, + 13472 => 23472, + 13473 => 23473, + 13474 => 23474, + 13475 => 23475, + 13476 => 23476, + 13477 => 23477, + 13478 => 23478, + 13479 => 23479, + 13480 => 23480, + 13481 => 23481, + 13482 => 23482, + 13483 => 23483, + 13484 => 23484, + 13485 => 23485, + 13486 => 23486, + 13487 => 23487, + 13488 => 23488, + 13489 => 23489, + 13490 => 23490, + 13491 => 23491, + 13492 => 23492, + 13493 => 23493, + 13494 => 23494, + 13495 => 23495, + 13496 => 23496, + 13497 => 23497, + 13498 => 23498, + 13499 => 23499, + 13500 => 23500, + 13501 => 23501, + 13502 => 23502, + 13503 => 23503, + 13504 => 23504, + 13505 => 23505, + 13506 => 23506, + 13507 => 23507, + 13508 => 23508, + 13509 => 23509, + 13510 => 23510, + 13511 => 23511, + 13512 => 23512, + 13513 => 23513, + 13514 => 23514, + 13515 => 23515, + 13516 => 23516, + 13517 => 23517, + 13518 => 23518, + 13519 => 23519, + 13520 => 23520, + 13521 => 23521, + 13522 => 23522, + 13523 => 23523, + 13524 => 23524, + 13525 => 23525, + 13526 => 23526, + 13527 => 23527, + 13528 => 23528, + 13529 => 23529, + 13530 => 23530, + 13531 => 23531, + 13532 => 23532, + 13533 => 23533, + 13534 => 23534, + 13535 => 23535, + 13536 => 23536, + 13537 => 23537, + 13538 => 23538, + 13539 => 23539, + 13540 => 23540, + 13541 => 23541, + 13542 => 23542, + 13543 => 23543, + 13544 => 23544, + 13545 => 23545, + 13546 => 23546, + 13547 => 23547, + 13548 => 23548, + 13549 => 23549, + 13550 => 23550, + 13551 => 23551, + 13552 => 23552, + 13553 => 23553, + 13554 => 23554, + 13555 => 23555, + 13556 => 23556, + 13557 => 23557, + 13558 => 23558, + 13559 => 23559, + 13560 => 23560, + 13561 => 23561, + 13562 => 23562, + 13563 => 23563, + 13564 => 23564, + 13565 => 23565, + 13566 => 23566, + 13567 => 23567, + 13568 => 23568, + 13569 => 23569, + 13570 => 23570, + 13571 => 23571, + 13572 => 23572, + 13573 => 23573, + 13574 => 23574, + 13575 => 23575, + 13576 => 23576, + 13577 => 23577, + 13578 => 23578, + 13579 => 23579, + 13580 => 23580, + 13581 => 23581, + 13582 => 23582, + 13583 => 23583, + 13584 => 23584, + 13585 => 23585, + 13586 => 23586, + 13587 => 23587, + 13588 => 23588, + 13589 => 23589, + 13590 => 23590, + 13591 => 23591, + 13592 => 23592, + 13593 => 23593, + 13594 => 23594, + 13595 => 23595, + 13596 => 23596, + 13597 => 23597, + 13598 => 23598, + 13599 => 23599, + 13600 => 23600, + 13601 => 23601, + 13602 => 23602, + 13603 => 23603, + 13604 => 23604, + 13605 => 23605, + 13606 => 23606, + 13607 => 23607, + 13608 => 23608, + 13609 => 23609, + 13610 => 23610, + 13611 => 23611, + 13612 => 23612, + 13613 => 23613, + 13614 => 23614, + 13615 => 23615, + 13616 => 23616, + 13617 => 23617, + 13618 => 23618, + 13619 => 23619, + 13620 => 23620, + 13621 => 23621, + 13622 => 23622, + 13623 => 23623, + 13624 => 23624, + 13625 => 23625, + 13626 => 23626, + 13627 => 23627, + 13628 => 23628, + 13629 => 23629, + 13630 => 23630, + 13631 => 23631, + 13632 => 23632, + 13633 => 23633, + 13634 => 23634, + 13635 => 23635, + 13636 => 23636, + 13637 => 23637, + 13638 => 23638, + 13639 => 23639, + 13640 => 23640, + 13641 => 23641, + 13642 => 23642, + 13643 => 23643, + 13644 => 23644, + 13645 => 23645, + 13646 => 23646, + 13647 => 23647, + 13648 => 23648, + 13649 => 23649, + 13650 => 23650, + 13651 => 23651, + 13652 => 23652, + 13653 => 23653, + 13654 => 23654, + 13655 => 23655, + 13656 => 23656, + 13657 => 23657, + 13658 => 23658, + 13659 => 23659, + 13660 => 23660, + 13661 => 23661, + 13662 => 23662, + 13663 => 23663, + 13664 => 23664, + 13665 => 23665, + 13666 => 23666, + 13667 => 23667, + 13668 => 23668, + 13669 => 23669, + 13670 => 23670, + 13671 => 23671, + 13672 => 23672, + 13673 => 23673, + 13674 => 23674, + 13675 => 23675, + 13676 => 23676, + 13677 => 23677, + 13678 => 23678, + 13679 => 23679, + 13680 => 23680, + 13681 => 23681, + 13682 => 23682, + 13683 => 23683, + 13684 => 23684, + 13685 => 23685, + 13686 => 23686, + 13687 => 23687, + 13688 => 23688, + 13689 => 23689, + 13690 => 23690, + 13691 => 23691, + 13692 => 23692, + 13693 => 23693, + 13694 => 23694, + 13695 => 23695, + 13696 => 23696, + 13697 => 23697, + 13698 => 23698, + 13699 => 23699, + 13700 => 23700, + 13701 => 23701, + 13702 => 23702, + 13703 => 23703, + 13704 => 23704, + 13705 => 23705, + 13706 => 23706, + 13707 => 23707, + 13708 => 23708, + 13709 => 23709, + 13710 => 23710, + 13711 => 23711, + 13712 => 23712, + 13713 => 23713, + 13714 => 23714, + 13715 => 23715, + 13716 => 23716, + 13717 => 23717, + 13718 => 23718, + 13719 => 23719, + 13720 => 23720, + 13721 => 23721, + 13722 => 23722, + 13723 => 23723, + 13724 => 23724, + 13725 => 23725, + 13726 => 23726, + 13727 => 23727, + 13728 => 23728, + 13729 => 23729, + 13730 => 23730, + 13731 => 23731, + 13732 => 23732, + 13733 => 23733, + 13734 => 23734, + 13735 => 23735, + 13736 => 23736, + 13737 => 23737, + 13738 => 23738, + 13739 => 23739, + 13740 => 23740, + 13741 => 23741, + 13742 => 23742, + 13743 => 23743, + 13744 => 23744, + 13745 => 23745, + 13746 => 23746, + 13747 => 23747, + 13748 => 23748, + 13749 => 23749, + 13750 => 23750, + 13751 => 23751, + 13752 => 23752, + 13753 => 23753, + 13754 => 23754, + 13755 => 23755, + 13756 => 23756, + 13757 => 23757, + 13758 => 23758, + 13759 => 23759, + 13760 => 23760, + 13761 => 23761, + 13762 => 23762, + 13763 => 23763, + 13764 => 23764, + 13765 => 23765, + 13766 => 23766, + 13767 => 23767, + 13768 => 23768, + 13769 => 23769, + 13770 => 23770, + 13771 => 23771, + 13772 => 23772, + 13773 => 23773, + 13774 => 23774, + 13775 => 23775, + 13776 => 23776, + 13777 => 23777, + 13778 => 23778, + 13779 => 23779, + 13780 => 23780, + 13781 => 23781, + 13782 => 23782, + 13783 => 23783, + 13784 => 23784, + 13785 => 23785, + 13786 => 23786, + 13787 => 23787, + 13788 => 23788, + 13789 => 23789, + 13790 => 23790, + 13791 => 23791, + 13792 => 23792, + 13793 => 23793, + 13794 => 23794, + 13795 => 23795, + 13796 => 23796, + 13797 => 23797, + 13798 => 23798, + 13799 => 23799, + 13800 => 23800, + 13801 => 23801, + 13802 => 23802, + 13803 => 23803, + 13804 => 23804, + 13805 => 23805, + 13806 => 23806, + 13807 => 23807, + 13808 => 23808, + 13809 => 23809, + 13810 => 23810, + 13811 => 23811, + 13812 => 23812, + 13813 => 23813, + 13814 => 23814, + 13815 => 23815, + 13816 => 23816, + 13817 => 23817, + 13818 => 23818, + 13819 => 23819, + 13820 => 23820, + 13821 => 23821, + 13822 => 23822, + 13823 => 23823, + 13824 => 23824, + 13825 => 23825, + 13826 => 23826, + 13827 => 23827, + 13828 => 23828, + 13829 => 23829, + 13830 => 23830, + 13831 => 23831, + 13832 => 23832, + 13833 => 23833, + 13834 => 23834, + 13835 => 23835, + 13836 => 23836, + 13837 => 23837, + 13838 => 23838, + 13839 => 23839, + 13840 => 23840, + 13841 => 23841, + 13842 => 23842, + 13843 => 23843, + 13844 => 23844, + 13845 => 23845, + 13846 => 23846, + 13847 => 23847, + 13848 => 23848, + 13849 => 23849, + 13850 => 23850, + 13851 => 23851, + 13852 => 23852, + 13853 => 23853, + 13854 => 23854, + 13855 => 23855, + 13856 => 23856, + 13857 => 23857, + 13858 => 23858, + 13859 => 23859, + 13860 => 23860, + 13861 => 23861, + 13862 => 23862, + 13863 => 23863, + 13864 => 23864, + 13865 => 23865, + 13866 => 23866, + 13867 => 23867, + 13868 => 23868, + 13869 => 23869, + 13870 => 23870, + 13871 => 23871, + 13872 => 23872, + 13873 => 23873, + 13874 => 23874, + 13875 => 23875, + 13876 => 23876, + 13877 => 23877, + 13878 => 23878, + 13879 => 23879, + 13880 => 23880, + 13881 => 23881, + 13882 => 23882, + 13883 => 23883, + 13884 => 23884, + 13885 => 23885, + 13886 => 23886, + 13887 => 23887, + 13888 => 23888, + 13889 => 23889, + 13890 => 23890, + 13891 => 23891, + 13892 => 23892, + 13893 => 23893, + 13894 => 23894, + 13895 => 23895, + 13896 => 23896, + 13897 => 23897, + 13898 => 23898, + 13899 => 23899, + 13900 => 23900, + 13901 => 23901, + 13902 => 23902, + 13903 => 23903, + 13904 => 23904, + 13905 => 23905, + 13906 => 23906, + 13907 => 23907, + 13908 => 23908, + 13909 => 23909, + 13910 => 23910, + 13911 => 23911, + 13912 => 23912, + 13913 => 23913, + 13914 => 23914, + 13915 => 23915, + 13916 => 23916, + 13917 => 23917, + 13918 => 23918, + 13919 => 23919, + 13920 => 23920, + 13921 => 23921, + 13922 => 23922, + 13923 => 23923, + 13924 => 23924, + 13925 => 23925, + 13926 => 23926, + 13927 => 23927, + 13928 => 23928, + 13929 => 23929, + 13930 => 23930, + 13931 => 23931, + 13932 => 23932, + 13933 => 23933, + 13934 => 23934, + 13935 => 23935, + 13936 => 23936, + 13937 => 23937, + 13938 => 23938, + 13939 => 23939, + 13940 => 23940, + 13941 => 23941, + 13942 => 23942, + 13943 => 23943, + 13944 => 23944, + 13945 => 23945, + 13946 => 23946, + 13947 => 23947, + 13948 => 23948, + 13949 => 23949, + 13950 => 23950, + 13951 => 23951, + 13952 => 23952, + 13953 => 23953, + 13954 => 23954, + 13955 => 23955, + 13956 => 23956, + 13957 => 23957, + 13958 => 23958, + 13959 => 23959, + 13960 => 23960, + 13961 => 23961, + 13962 => 23962, + 13963 => 23963, + 13964 => 23964, + 13965 => 23965, + 13966 => 23966, + 13967 => 23967, + 13968 => 23968, + 13969 => 23969, + 13970 => 23970, + 13971 => 23971, + 13972 => 23972, + 13973 => 23973, + 13974 => 23974, + 13975 => 23975, + 13976 => 23976, + 13977 => 23977, + 13978 => 23978, + 13979 => 23979, + 13980 => 23980, + 13981 => 23981, + 13982 => 23982, + 13983 => 23983, + 13984 => 23984, + 13985 => 23985, + 13986 => 23986, + 13987 => 23987, + 13988 => 23988, + 13989 => 23989, + 13990 => 23990, + 13991 => 23991, + 13992 => 23992, + 13993 => 23993, + 13994 => 23994, + 13995 => 23995, + 13996 => 23996, + 13997 => 23997, + 13998 => 23998, + 13999 => 23999, + 14000 => 24000, + 14001 => 24001, + 14002 => 24002, + 14003 => 24003, + 14004 => 24004, + 14005 => 24005, + 14006 => 24006, + 14007 => 24007, + 14008 => 24008, + 14009 => 24009, + 14010 => 24010, + 14011 => 24011, + 14012 => 24012, + 14013 => 24013, + 14014 => 24014, + 14015 => 24015, + 14016 => 24016, + 14017 => 24017, + 14018 => 24018, + 14019 => 24019, + 14020 => 24020, + 14021 => 24021, + 14022 => 24022, + 14023 => 24023, + 14024 => 24024, + 14025 => 24025, + 14026 => 24026, + 14027 => 24027, + 14028 => 24028, + 14029 => 24029, + 14030 => 24030, + 14031 => 24031, + 14032 => 24032, + 14033 => 24033, + 14034 => 24034, + 14035 => 24035, + 14036 => 24036, + 14037 => 24037, + 14038 => 24038, + 14039 => 24039, + 14040 => 24040, + 14041 => 24041, + 14042 => 24042, + 14043 => 24043, + 14044 => 24044, + 14045 => 24045, + 14046 => 24046, + 14047 => 24047, + 14048 => 24048, + 14049 => 24049, + 14050 => 24050, + 14051 => 24051, + 14052 => 24052, + 14053 => 24053, + 14054 => 24054, + 14055 => 24055, + 14056 => 24056, + 14057 => 24057, + 14058 => 24058, + 14059 => 24059, + 14060 => 24060, + 14061 => 24061, + 14062 => 24062, + 14063 => 24063, + 14064 => 24064, + 14065 => 24065, + 14066 => 24066, + 14067 => 24067, + 14068 => 24068, + 14069 => 24069, + 14070 => 24070, + 14071 => 24071, + 14072 => 24072, + 14073 => 24073, + 14074 => 24074, + 14075 => 24075, + 14076 => 24076, + 14077 => 24077, + 14078 => 24078, + 14079 => 24079, + 14080 => 24080, + 14081 => 24081, + 14082 => 24082, + 14083 => 24083, + 14084 => 24084, + 14085 => 24085, + 14086 => 24086, + 14087 => 24087, + 14088 => 24088, + 14089 => 24089, + 14090 => 24090, + 14091 => 24091, + 14092 => 24092, + 14093 => 24093, + 14094 => 24094, + 14095 => 24095, + 14096 => 24096, + 14097 => 24097, + 14098 => 24098, + 14099 => 24099, + 14100 => 24100, + 14101 => 24101, + 14102 => 24102, + 14103 => 24103, + 14104 => 24104, + 14105 => 24105, + 14106 => 24106, + 14107 => 24107, + 14108 => 24108, + 14109 => 24109, + 14110 => 24110, + 14111 => 24111, + 14112 => 24112, + 14113 => 24113, + 14114 => 24114, + 14115 => 24115, + 14116 => 24116, + 14117 => 24117, + 14118 => 24118, + 14119 => 24119, + 14120 => 24120, + 14121 => 24121, + 14122 => 24122, + 14123 => 24123, + 14124 => 24124, + 14125 => 24125, + 14126 => 24126, + 14127 => 24127, + 14128 => 24128, + 14129 => 24129, + 14130 => 24130, + 14131 => 24131, + 14132 => 24132, + 14133 => 24133, + 14134 => 24134, + 14135 => 24135, + 14136 => 24136, + 14137 => 24137, + 14138 => 24138, + 14139 => 24139, + 14140 => 24140, + 14141 => 24141, + 14142 => 24142, + 14143 => 24143, + 14144 => 24144, + 14145 => 24145, + 14146 => 24146, + 14147 => 24147, + 14148 => 24148, + 14149 => 24149, + 14150 => 24150, + 14151 => 24151, + 14152 => 24152, + 14153 => 24153, + 14154 => 24154, + 14155 => 24155, + 14156 => 24156, + 14157 => 24157, + 14158 => 24158, + 14159 => 24159, + 14160 => 24160, + 14161 => 24161, + 14162 => 24162, + 14163 => 24163, + 14164 => 24164, + 14165 => 24165, + 14166 => 24166, + 14167 => 24167, + 14168 => 24168, + 14169 => 24169, + 14170 => 24170, + 14171 => 24171, + 14172 => 24172, + 14173 => 24173, + 14174 => 24174, + 14175 => 24175, + 14176 => 24176, + 14177 => 24177, + 14178 => 24178, + 14179 => 24179, + 14180 => 24180, + 14181 => 24181, + 14182 => 24182, + 14183 => 24183, + 14184 => 24184, + 14185 => 24185, + 14186 => 24186, + 14187 => 24187, + 14188 => 24188, + 14189 => 24189, + 14190 => 24190, + 14191 => 24191, + 14192 => 24192, + 14193 => 24193, + 14194 => 24194, + 14195 => 24195, + 14196 => 24196, + 14197 => 24197, + 14198 => 24198, + 14199 => 24199, + 14200 => 24200, + 14201 => 24201, + 14202 => 24202, + 14203 => 24203, + 14204 => 24204, + 14205 => 24205, + 14206 => 24206, + 14207 => 24207, + 14208 => 24208, + 14209 => 24209, + 14210 => 24210, + 14211 => 24211, + 14212 => 24212, + 14213 => 24213, + 14214 => 24214, + 14215 => 24215, + 14216 => 24216, + 14217 => 24217, + 14218 => 24218, + 14219 => 24219, + 14220 => 24220, + 14221 => 24221, + 14222 => 24222, + 14223 => 24223, + 14224 => 24224, + 14225 => 24225, + 14226 => 24226, + 14227 => 24227, + 14228 => 24228, + 14229 => 24229, + 14230 => 24230, + 14231 => 24231, + 14232 => 24232, + 14233 => 24233, + 14234 => 24234, + 14235 => 24235, + 14236 => 24236, + 14237 => 24237, + 14238 => 24238, + 14239 => 24239, + 14240 => 24240, + 14241 => 24241, + 14242 => 24242, + 14243 => 24243, + 14244 => 24244, + 14245 => 24245, + 14246 => 24246, + 14247 => 24247, + 14248 => 24248, + 14249 => 24249, + 14250 => 24250, + 14251 => 24251, + 14252 => 24252, + 14253 => 24253, + 14254 => 24254, + 14255 => 24255, + 14256 => 24256, + 14257 => 24257, + 14258 => 24258, + 14259 => 24259, + 14260 => 24260, + 14261 => 24261, + 14262 => 24262, + 14263 => 24263, + 14264 => 24264, + 14265 => 24265, + 14266 => 24266, + 14267 => 24267, + 14268 => 24268, + 14269 => 24269, + 14270 => 24270, + 14271 => 24271, + 14272 => 24272, + 14273 => 24273, + 14274 => 24274, + 14275 => 24275, + 14276 => 24276, + 14277 => 24277, + 14278 => 24278, + 14279 => 24279, + 14280 => 24280, + 14281 => 24281, + 14282 => 24282, + 14283 => 24283, + 14284 => 24284, + 14285 => 24285, + 14286 => 24286, + 14287 => 24287, + 14288 => 24288, + 14289 => 24289, + 14290 => 24290, + 14291 => 24291, + 14292 => 24292, + 14293 => 24293, + 14294 => 24294, + 14295 => 24295, + 14296 => 24296, + 14297 => 24297, + 14298 => 24298, + 14299 => 24299, + 14300 => 24300, + 14301 => 24301, + 14302 => 24302, + 14303 => 24303, + 14304 => 24304, + 14305 => 24305, + 14306 => 24306, + 14307 => 24307, + 14308 => 24308, + 14309 => 24309, + 14310 => 24310, + 14311 => 24311, + 14312 => 24312, + 14313 => 24313, + 14314 => 24314, + 14315 => 24315, + 14316 => 24316, + 14317 => 24317, + 14318 => 24318, + 14319 => 24319, + 14320 => 24320, + 14321 => 24321, + 14322 => 24322, + 14323 => 24323, + 14324 => 24324, + 14325 => 24325, + 14326 => 24326, + 14327 => 24327, + 14328 => 24328, + 14329 => 24329, + 14330 => 24330, + 14331 => 24331, + 14332 => 24332, + 14333 => 24333, + 14334 => 24334, + 14335 => 24335, + 14336 => 24336, + 14337 => 24337, + 14338 => 24338, + 14339 => 24339, + 14340 => 24340, + 14341 => 24341, + 14342 => 24342, + 14343 => 24343, + 14344 => 24344, + 14345 => 24345, + 14346 => 24346, + 14347 => 24347, + 14348 => 24348, + 14349 => 24349, + 14350 => 24350, + 14351 => 24351, + 14352 => 24352, + 14353 => 24353, + 14354 => 24354, + 14355 => 24355, + 14356 => 24356, + 14357 => 24357, + 14358 => 24358, + 14359 => 24359, + 14360 => 24360, + 14361 => 24361, + 14362 => 24362, + 14363 => 24363, + 14364 => 24364, + 14365 => 24365, + 14366 => 24366, + 14367 => 24367, + 14368 => 24368, + 14369 => 24369, + 14370 => 24370, + 14371 => 24371, + 14372 => 24372, + 14373 => 24373, + 14374 => 24374, + 14375 => 24375, + 14376 => 24376, + 14377 => 24377, + 14378 => 24378, + 14379 => 24379, + 14380 => 24380, + 14381 => 24381, + 14382 => 24382, + 14383 => 24383, + 14384 => 24384, + 14385 => 24385, + 14386 => 24386, + 14387 => 24387, + 14388 => 24388, + 14389 => 24389, + 14390 => 24390, + 14391 => 24391, + 14392 => 24392, + 14393 => 24393, + 14394 => 24394, + 14395 => 24395, + 14396 => 24396, + 14397 => 24397, + 14398 => 24398, + 14399 => 24399, + 14400 => 24400, + 14401 => 24401, + 14402 => 24402, + 14403 => 24403, + 14404 => 24404, + 14405 => 24405, + 14406 => 24406, + 14407 => 24407, + 14408 => 24408, + 14409 => 24409, + 14410 => 24410, + 14411 => 24411, + 14412 => 24412, + 14413 => 24413, + 14414 => 24414, + 14415 => 24415, + 14416 => 24416, + 14417 => 24417, + 14418 => 24418, + 14419 => 24419, + 14420 => 24420, + 14421 => 24421, + 14422 => 24422, + 14423 => 24423, + 14424 => 24424, + 14425 => 24425, + 14426 => 24426, + 14427 => 24427, + 14428 => 24428, + 14429 => 24429, + 14430 => 24430, + 14431 => 24431, + 14432 => 24432, + 14433 => 24433, + 14434 => 24434, + 14435 => 24435, + 14436 => 24436, + 14437 => 24437, + 14438 => 24438, + 14439 => 24439, + 14440 => 24440, + 14441 => 24441, + 14442 => 24442, + 14443 => 24443, + 14444 => 24444, + 14445 => 24445, + 14446 => 24446, + 14447 => 24447, + 14448 => 24448, + 14449 => 24449, + 14450 => 24450, + 14451 => 24451, + 14452 => 24452, + 14453 => 24453, + 14454 => 24454, + 14455 => 24455, + 14456 => 24456, + 14457 => 24457, + 14458 => 24458, + 14459 => 24459, + 14460 => 24460, + 14461 => 24461, + 14462 => 24462, + 14463 => 24463, + 14464 => 24464, + 14465 => 24465, + 14466 => 24466, + 14467 => 24467, + 14468 => 24468, + 14469 => 24469, + 14470 => 24470, + 14471 => 24471, + 14472 => 24472, + 14473 => 24473, + 14474 => 24474, + 14475 => 24475, + 14476 => 24476, + 14477 => 24477, + 14478 => 24478, + 14479 => 24479, + 14480 => 24480, + 14481 => 24481, + 14482 => 24482, + 14483 => 24483, + 14484 => 24484, + 14485 => 24485, + 14486 => 24486, + 14487 => 24487, + 14488 => 24488, + 14489 => 24489, + 14490 => 24490, + 14491 => 24491, + 14492 => 24492, + 14493 => 24493, + 14494 => 24494, + 14495 => 24495, + 14496 => 24496, + 14497 => 24497, + 14498 => 24498, + 14499 => 24499, + 14500 => 24500, + 14501 => 24501, + 14502 => 24502, + 14503 => 24503, + 14504 => 24504, + 14505 => 24505, + 14506 => 24506, + 14507 => 24507, + 14508 => 24508, + 14509 => 24509, + 14510 => 24510, + 14511 => 24511, + 14512 => 24512, + 14513 => 24513, + 14514 => 24514, + 14515 => 24515, + 14516 => 24516, + 14517 => 24517, + 14518 => 24518, + 14519 => 24519, + 14520 => 24520, + 14521 => 24521, + 14522 => 24522, + 14523 => 24523, + 14524 => 24524, + 14525 => 24525, + 14526 => 24526, + 14527 => 24527, + 14528 => 24528, + 14529 => 24529, + 14530 => 24530, + 14531 => 24531, + 14532 => 24532, + 14533 => 24533, + 14534 => 24534, + 14535 => 24535, + 14536 => 24536, + 14537 => 24537, + 14538 => 24538, + 14539 => 24539, + 14540 => 24540, + 14541 => 24541, + 14542 => 24542, + 14543 => 24543, + 14544 => 24544, + 14545 => 24545, + 14546 => 24546, + 14547 => 24547, + 14548 => 24548, + 14549 => 24549, + 14550 => 24550, + 14551 => 24551, + 14552 => 24552, + 14553 => 24553, + 14554 => 24554, + 14555 => 24555, + 14556 => 24556, + 14557 => 24557, + 14558 => 24558, + 14559 => 24559, + 14560 => 24560, + 14561 => 24561, + 14562 => 24562, + 14563 => 24563, + 14564 => 24564, + 14565 => 24565, + 14566 => 24566, + 14567 => 24567, + 14568 => 24568, + 14569 => 24569, + 14570 => 24570, + 14571 => 24571, + 14572 => 24572, + 14573 => 24573, + 14574 => 24574, + 14575 => 24575, + 14576 => 24576, + 14577 => 24577, + 14578 => 24578, + 14579 => 24579, + 14580 => 24580, + 14581 => 24581, + 14582 => 24582, + 14583 => 24583, + 14584 => 24584, + 14585 => 24585, + 14586 => 24586, + 14587 => 24587, + 14588 => 24588, + 14589 => 24589, + 14590 => 24590, + 14591 => 24591, + 14592 => 24592, + 14593 => 24593, + 14594 => 24594, + 14595 => 24595, + 14596 => 24596, + 14597 => 24597, + 14598 => 24598, + 14599 => 24599, + 14600 => 24600, + 14601 => 24601, + 14602 => 24602, + 14603 => 24603, + 14604 => 24604, + 14605 => 24605, + 14606 => 24606, + 14607 => 24607, + 14608 => 24608, + 14609 => 24609, + 14610 => 24610, + 14611 => 24611, + 14612 => 24612, + 14613 => 24613, + 14614 => 24614, + 14615 => 24615, + 14616 => 24616, + 14617 => 24617, + 14618 => 24618, + 14619 => 24619, + 14620 => 24620, + 14621 => 24621, + 14622 => 24622, + 14623 => 24623, + 14624 => 24624, + 14625 => 24625, + 14626 => 24626, + 14627 => 24627, + 14628 => 24628, + 14629 => 24629, + 14630 => 24630, + 14631 => 24631, + 14632 => 24632, + 14633 => 24633, + 14634 => 24634, + 14635 => 24635, + 14636 => 24636, + 14637 => 24637, + 14638 => 24638, + 14639 => 24639, + 14640 => 24640, + 14641 => 24641, + 14642 => 24642, + 14643 => 24643, + 14644 => 24644, + 14645 => 24645, + 14646 => 24646, + 14647 => 24647, + 14648 => 24648, + 14649 => 24649, + 14650 => 24650, + 14651 => 24651, + 14652 => 24652, + 14653 => 24653, + 14654 => 24654, + 14655 => 24655, + 14656 => 24656, + 14657 => 24657, + 14658 => 24658, + 14659 => 24659, + 14660 => 24660, + 14661 => 24661, + 14662 => 24662, + 14663 => 24663, + 14664 => 24664, + 14665 => 24665, + 14666 => 24666, + 14667 => 24667, + 14668 => 24668, + 14669 => 24669, + 14670 => 24670, + 14671 => 24671, + 14672 => 24672, + 14673 => 24673, + 14674 => 24674, + 14675 => 24675, + 14676 => 24676, + 14677 => 24677, + 14678 => 24678, + 14679 => 24679, + 14680 => 24680, + 14681 => 24681, + 14682 => 24682, + 14683 => 24683, + 14684 => 24684, + 14685 => 24685, + 14686 => 24686, + 14687 => 24687, + 14688 => 24688, + 14689 => 24689, + 14690 => 24690, + 14691 => 24691, + 14692 => 24692, + 14693 => 24693, + 14694 => 24694, + 14695 => 24695, + 14696 => 24696, + 14697 => 24697, + 14698 => 24698, + 14699 => 24699, + 14700 => 24700, + 14701 => 24701, + 14702 => 24702, + 14703 => 24703, + 14704 => 24704, + 14705 => 24705, + 14706 => 24706, + 14707 => 24707, + 14708 => 24708, + 14709 => 24709, + 14710 => 24710, + 14711 => 24711, + 14712 => 24712, + 14713 => 24713, + 14714 => 24714, + 14715 => 24715, + 14716 => 24716, + 14717 => 24717, + 14718 => 24718, + 14719 => 24719, + 14720 => 24720, + 14721 => 24721, + 14722 => 24722, + 14723 => 24723, + 14724 => 24724, + 14725 => 24725, + 14726 => 24726, + 14727 => 24727, + 14728 => 24728, + 14729 => 24729, + 14730 => 24730, + 14731 => 24731, + 14732 => 24732, + 14733 => 24733, + 14734 => 24734, + 14735 => 24735, + 14736 => 24736, + 14737 => 24737, + 14738 => 24738, + 14739 => 24739, + 14740 => 24740, + 14741 => 24741, + 14742 => 24742, + 14743 => 24743, + 14744 => 24744, + 14745 => 24745, + 14746 => 24746, + 14747 => 24747, + 14748 => 24748, + 14749 => 24749, + 14750 => 24750, + 14751 => 24751, + 14752 => 24752, + 14753 => 24753, + 14754 => 24754, + 14755 => 24755, + 14756 => 24756, + 14757 => 24757, + 14758 => 24758, + 14759 => 24759, + 14760 => 24760, + 14761 => 24761, + 14762 => 24762, + 14763 => 24763, + 14764 => 24764, + 14765 => 24765, + 14766 => 24766, + 14767 => 24767, + 14768 => 24768, + 14769 => 24769, + 14770 => 24770, + 14771 => 24771, + 14772 => 24772, + 14773 => 24773, + 14774 => 24774, + 14775 => 24775, + 14776 => 24776, + 14777 => 24777, + 14778 => 24778, + 14779 => 24779, + 14780 => 24780, + 14781 => 24781, + 14782 => 24782, + 14783 => 24783, + 14784 => 24784, + 14785 => 24785, + 14786 => 24786, + 14787 => 24787, + 14788 => 24788, + 14789 => 24789, + 14790 => 24790, + 14791 => 24791, + 14792 => 24792, + 14793 => 24793, + 14794 => 24794, + 14795 => 24795, + 14796 => 24796, + 14797 => 24797, + 14798 => 24798, + 14799 => 24799, + 14800 => 24800, + 14801 => 24801, + 14802 => 24802, + 14803 => 24803, + 14804 => 24804, + 14805 => 24805, + 14806 => 24806, + 14807 => 24807, + 14808 => 24808, + 14809 => 24809, + 14810 => 24810, + 14811 => 24811, + 14812 => 24812, + 14813 => 24813, + 14814 => 24814, + 14815 => 24815, + 14816 => 24816, + 14817 => 24817, + 14818 => 24818, + 14819 => 24819, + 14820 => 24820, + 14821 => 24821, + 14822 => 24822, + 14823 => 24823, + 14824 => 24824, + 14825 => 24825, + 14826 => 24826, + 14827 => 24827, + 14828 => 24828, + 14829 => 24829, + 14830 => 24830, + 14831 => 24831, + 14832 => 24832, + 14833 => 24833, + 14834 => 24834, + 14835 => 24835, + 14836 => 24836, + 14837 => 24837, + 14838 => 24838, + 14839 => 24839, + 14840 => 24840, + 14841 => 24841, + 14842 => 24842, + 14843 => 24843, + 14844 => 24844, + 14845 => 24845, + 14846 => 24846, + 14847 => 24847, + 14848 => 24848, + 14849 => 24849, + 14850 => 24850, + 14851 => 24851, + 14852 => 24852, + 14853 => 24853, + 14854 => 24854, + 14855 => 24855, + 14856 => 24856, + 14857 => 24857, + 14858 => 24858, + 14859 => 24859, + 14860 => 24860, + 14861 => 24861, + 14862 => 24862, + 14863 => 24863, + 14864 => 24864, + 14865 => 24865, + 14866 => 24866, + 14867 => 24867, + 14868 => 24868, + 14869 => 24869, + 14870 => 24870, + 14871 => 24871, + 14872 => 24872, + 14873 => 24873, + 14874 => 24874, + 14875 => 24875, + 14876 => 24876, + 14877 => 24877, + 14878 => 24878, + 14879 => 24879, + 14880 => 24880, + 14881 => 24881, + 14882 => 24882, + 14883 => 24883, + 14884 => 24884, + 14885 => 24885, + 14886 => 24886, + 14887 => 24887, + 14888 => 24888, + 14889 => 24889, + 14890 => 24890, + 14891 => 24891, + 14892 => 24892, + 14893 => 24893, + 14894 => 24894, + 14895 => 24895, + 14896 => 24896, + 14897 => 24897, + 14898 => 24898, + 14899 => 24899, + 14900 => 24900, + 14901 => 24901, + 14902 => 24902, + 14903 => 24903, + 14904 => 24904, + 14905 => 24905, + 14906 => 24906, + 14907 => 24907, + 14908 => 24908, + 14909 => 24909, + 14910 => 24910, + 14911 => 24911, + 14912 => 24912, + 14913 => 24913, + 14914 => 24914, + 14915 => 24915, + 14916 => 24916, + 14917 => 24917, + 14918 => 24918, + 14919 => 24919, + 14920 => 24920, + 14921 => 24921, + 14922 => 24922, + 14923 => 24923, + 14924 => 24924, + 14925 => 24925, + 14926 => 24926, + 14927 => 24927, + 14928 => 24928, + 14929 => 24929, + 14930 => 24930, + 14931 => 24931, + 14932 => 24932, + 14933 => 24933, + 14934 => 24934, + 14935 => 24935, + 14936 => 24936, + 14937 => 24937, + 14938 => 24938, + 14939 => 24939, + 14940 => 24940, + 14941 => 24941, + 14942 => 24942, + 14943 => 24943, + 14944 => 24944, + 14945 => 24945, + 14946 => 24946, + 14947 => 24947, + 14948 => 24948, + 14949 => 24949, + 14950 => 24950, + 14951 => 24951, + 14952 => 24952, + 14953 => 24953, + 14954 => 24954, + 14955 => 24955, + 14956 => 24956, + 14957 => 24957, + 14958 => 24958, + 14959 => 24959, + 14960 => 24960, + 14961 => 24961, + 14962 => 24962, + 14963 => 24963, + 14964 => 24964, + 14965 => 24965, + 14966 => 24966, + 14967 => 24967, + 14968 => 24968, + 14969 => 24969, + 14970 => 24970, + 14971 => 24971, + 14972 => 24972, + 14973 => 24973, + 14974 => 24974, + 14975 => 24975, + 14976 => 24976, + 14977 => 24977, + 14978 => 24978, + 14979 => 24979, + 14980 => 24980, + 14981 => 24981, + 14982 => 24982, + 14983 => 24983, + 14984 => 24984, + 14985 => 24985, + 14986 => 24986, + 14987 => 24987, + 14988 => 24988, + 14989 => 24989, + 14990 => 24990, + 14991 => 24991, + 14992 => 24992, + 14993 => 24993, + 14994 => 24994, + 14995 => 24995, + 14996 => 24996, + 14997 => 24997, + 14998 => 24998, + 14999 => 24999, + 15000 => 25000, + 15001 => 25001, + 15002 => 25002, + 15003 => 25003, + 15004 => 25004, + 15005 => 25005, + 15006 => 25006, + 15007 => 25007, + 15008 => 25008, + 15009 => 25009, + 15010 => 25010, + 15011 => 25011, + 15012 => 25012, + 15013 => 25013, + 15014 => 25014, + 15015 => 25015, + 15016 => 25016, + 15017 => 25017, + 15018 => 25018, + 15019 => 25019, + 15020 => 25020, + 15021 => 25021, + 15022 => 25022, + 15023 => 25023, + 15024 => 25024, + 15025 => 25025, + 15026 => 25026, + 15027 => 25027, + 15028 => 25028, + 15029 => 25029, + 15030 => 25030, + 15031 => 25031, + 15032 => 25032, + 15033 => 25033, + 15034 => 25034, + 15035 => 25035, + 15036 => 25036, + 15037 => 25037, + 15038 => 25038, + 15039 => 25039, + 15040 => 25040, + 15041 => 25041, + 15042 => 25042, + 15043 => 25043, + 15044 => 25044, + 15045 => 25045, + 15046 => 25046, + 15047 => 25047, + 15048 => 25048, + 15049 => 25049, + 15050 => 25050, + 15051 => 25051, + 15052 => 25052, + 15053 => 25053, + 15054 => 25054, + 15055 => 25055, + 15056 => 25056, + 15057 => 25057, + 15058 => 25058, + 15059 => 25059, + 15060 => 25060, + 15061 => 25061, + 15062 => 25062, + 15063 => 25063, + 15064 => 25064, + 15065 => 25065, + 15066 => 25066, + 15067 => 25067, + 15068 => 25068, + 15069 => 25069, + 15070 => 25070, + 15071 => 25071, + 15072 => 25072, + 15073 => 25073, + 15074 => 25074, + 15075 => 25075, + 15076 => 25076, + 15077 => 25077, + 15078 => 25078, + 15079 => 25079, + 15080 => 25080, + 15081 => 25081, + 15082 => 25082, + 15083 => 25083, + 15084 => 25084, + 15085 => 25085, + 15086 => 25086, + 15087 => 25087, + 15088 => 25088, + 15089 => 25089, + 15090 => 25090, + 15091 => 25091, + 15092 => 25092, + 15093 => 25093, + 15094 => 25094, + 15095 => 25095, + 15096 => 25096, + 15097 => 25097, + 15098 => 25098, + 15099 => 25099, + 15100 => 25100, + 15101 => 25101, + 15102 => 25102, + 15103 => 25103, + 15104 => 25104, + 15105 => 25105, + 15106 => 25106, + 15107 => 25107, + 15108 => 25108, + 15109 => 25109, + 15110 => 25110, + 15111 => 25111, + 15112 => 25112, + 15113 => 25113, + 15114 => 25114, + 15115 => 25115, + 15116 => 25116, + 15117 => 25117, + 15118 => 25118, + 15119 => 25119, + 15120 => 25120, + 15121 => 25121, + 15122 => 25122, + 15123 => 25123, + 15124 => 25124, + 15125 => 25125, + 15126 => 25126, + 15127 => 25127, + 15128 => 25128, + 15129 => 25129, + 15130 => 25130, + 15131 => 25131, + 15132 => 25132, + 15133 => 25133, + 15134 => 25134, + 15135 => 25135, + 15136 => 25136, + 15137 => 25137, + 15138 => 25138, + 15139 => 25139, + 15140 => 25140, + 15141 => 25141, + 15142 => 25142, + 15143 => 25143, + 15144 => 25144, + 15145 => 25145, + 15146 => 25146, + 15147 => 25147, + 15148 => 25148, + 15149 => 25149, + 15150 => 25150, + 15151 => 25151, + 15152 => 25152, + 15153 => 25153, + 15154 => 25154, + 15155 => 25155, + 15156 => 25156, + 15157 => 25157, + 15158 => 25158, + 15159 => 25159, + 15160 => 25160, + 15161 => 25161, + 15162 => 25162, + 15163 => 25163, + 15164 => 25164, + 15165 => 25165, + 15166 => 25166, + 15167 => 25167, + 15168 => 25168, + 15169 => 25169, + 15170 => 25170, + 15171 => 25171, + 15172 => 25172, + 15173 => 25173, + 15174 => 25174, + 15175 => 25175, + 15176 => 25176, + 15177 => 25177, + 15178 => 25178, + 15179 => 25179, + 15180 => 25180, + 15181 => 25181, + 15182 => 25182, + 15183 => 25183, + 15184 => 25184, + 15185 => 25185, + 15186 => 25186, + 15187 => 25187, + 15188 => 25188, + 15189 => 25189, + 15190 => 25190, + 15191 => 25191, + 15192 => 25192, + 15193 => 25193, + 15194 => 25194, + 15195 => 25195, + 15196 => 25196, + 15197 => 25197, + 15198 => 25198, + 15199 => 25199, + 15200 => 25200, + 15201 => 25201, + 15202 => 25202, + 15203 => 25203, + 15204 => 25204, + 15205 => 25205, + 15206 => 25206, + 15207 => 25207, + 15208 => 25208, + 15209 => 25209, + 15210 => 25210, + 15211 => 25211, + 15212 => 25212, + 15213 => 25213, + 15214 => 25214, + 15215 => 25215, + 15216 => 25216, + 15217 => 25217, + 15218 => 25218, + 15219 => 25219, + 15220 => 25220, + 15221 => 25221, + 15222 => 25222, + 15223 => 25223, + 15224 => 25224, + 15225 => 25225, + 15226 => 25226, + 15227 => 25227, + 15228 => 25228, + 15229 => 25229, + 15230 => 25230, + 15231 => 25231, + 15232 => 25232, + 15233 => 25233, + 15234 => 25234, + 15235 => 25235, + 15236 => 25236, + 15237 => 25237, + 15238 => 25238, + 15239 => 25239, + 15240 => 25240, + 15241 => 25241, + 15242 => 25242, + 15243 => 25243, + 15244 => 25244, + 15245 => 25245, + 15246 => 25246, + 15247 => 25247, + 15248 => 25248, + 15249 => 25249, + 15250 => 25250, + 15251 => 25251, + 15252 => 25252, + 15253 => 25253, + 15254 => 25254, + 15255 => 25255, + 15256 => 25256, + 15257 => 25257, + 15258 => 25258, + 15259 => 25259, + 15260 => 25260, + 15261 => 25261, + 15262 => 25262, + 15263 => 25263, + 15264 => 25264, + 15265 => 25265, + 15266 => 25266, + 15267 => 25267, + 15268 => 25268, + 15269 => 25269, + 15270 => 25270, + 15271 => 25271, + 15272 => 25272, + 15273 => 25273, + 15274 => 25274, + 15275 => 25275, + 15276 => 25276, + 15277 => 25277, + 15278 => 25278, + 15279 => 25279, + 15280 => 25280, + 15281 => 25281, + 15282 => 25282, + 15283 => 25283, + 15284 => 25284, + 15285 => 25285, + 15286 => 25286, + 15287 => 25287, + 15288 => 25288, + 15289 => 25289, + 15290 => 25290, + 15291 => 25291, + 15292 => 25292, + 15293 => 25293, + 15294 => 25294, + 15295 => 25295, + 15296 => 25296, + 15297 => 25297, + 15298 => 25298, + 15299 => 25299, + 15300 => 25300, + 15301 => 25301, + 15302 => 25302, + 15303 => 25303, + 15304 => 25304, + 15305 => 25305, + 15306 => 25306, + 15307 => 25307, + 15308 => 25308, + 15309 => 25309, + 15310 => 25310, + 15311 => 25311, + 15312 => 25312, + 15313 => 25313, + 15314 => 25314, + 15315 => 25315, + 15316 => 25316, + 15317 => 25317, + 15318 => 25318, + 15319 => 25319, + 15320 => 25320, + 15321 => 25321, + 15322 => 25322, + 15323 => 25323, + 15324 => 25324, + 15325 => 25325, + 15326 => 25326, + 15327 => 25327, + 15328 => 25328, + 15329 => 25329, + 15330 => 25330, + 15331 => 25331, + 15332 => 25332, + 15333 => 25333, + 15334 => 25334, + 15335 => 25335, + 15336 => 25336, + 15337 => 25337, + 15338 => 25338, + 15339 => 25339, + 15340 => 25340, + 15341 => 25341, + 15342 => 25342, + 15343 => 25343, + 15344 => 25344, + 15345 => 25345, + 15346 => 25346, + 15347 => 25347, + 15348 => 25348, + 15349 => 25349, + 15350 => 25350, + 15351 => 25351, + 15352 => 25352, + 15353 => 25353, + 15354 => 25354, + 15355 => 25355, + 15356 => 25356, + 15357 => 25357, + 15358 => 25358, + 15359 => 25359, + 15360 => 25360, + 15361 => 25361, + 15362 => 25362, + 15363 => 25363, + 15364 => 25364, + 15365 => 25365, + 15366 => 25366, + 15367 => 25367, + 15368 => 25368, + 15369 => 25369, + 15370 => 25370, + 15371 => 25371, + 15372 => 25372, + 15373 => 25373, + 15374 => 25374, + 15375 => 25375, + 15376 => 25376, + 15377 => 25377, + 15378 => 25378, + 15379 => 25379, + 15380 => 25380, + 15381 => 25381, + 15382 => 25382, + 15383 => 25383, + 15384 => 25384, + 15385 => 25385, + 15386 => 25386, + 15387 => 25387, + 15388 => 25388, + 15389 => 25389, + 15390 => 25390, + 15391 => 25391, + 15392 => 25392, + 15393 => 25393, + 15394 => 25394, + 15395 => 25395, + 15396 => 25396, + 15397 => 25397, + 15398 => 25398, + 15399 => 25399, + 15400 => 25400, + 15401 => 25401, + 15402 => 25402, + 15403 => 25403, + 15404 => 25404, + 15405 => 25405, + 15406 => 25406, + 15407 => 25407, + 15408 => 25408, + 15409 => 25409, + 15410 => 25410, + 15411 => 25411, + 15412 => 25412, + 15413 => 25413, + 15414 => 25414, + 15415 => 25415, + 15416 => 25416, + 15417 => 25417, + 15418 => 25418, + 15419 => 25419, + 15420 => 25420, + 15421 => 25421, + 15422 => 25422, + 15423 => 25423, + 15424 => 25424, + 15425 => 25425, + 15426 => 25426, + 15427 => 25427, + 15428 => 25428, + 15429 => 25429, + 15430 => 25430, + 15431 => 25431, + 15432 => 25432, + 15433 => 25433, + 15434 => 25434, + 15435 => 25435, + 15436 => 25436, + 15437 => 25437, + 15438 => 25438, + 15439 => 25439, + 15440 => 25440, + 15441 => 25441, + 15442 => 25442, + 15443 => 25443, + 15444 => 25444, + 15445 => 25445, + 15446 => 25446, + 15447 => 25447, + 15448 => 25448, + 15449 => 25449, + 15450 => 25450, + 15451 => 25451, + 15452 => 25452, + 15453 => 25453, + 15454 => 25454, + 15455 => 25455, + 15456 => 25456, + 15457 => 25457, + 15458 => 25458, + 15459 => 25459, + 15460 => 25460, + 15461 => 25461, + 15462 => 25462, + 15463 => 25463, + 15464 => 25464, + 15465 => 25465, + 15466 => 25466, + 15467 => 25467, + 15468 => 25468, + 15469 => 25469, + 15470 => 25470, + 15471 => 25471, + 15472 => 25472, + 15473 => 25473, + 15474 => 25474, + 15475 => 25475, + 15476 => 25476, + 15477 => 25477, + 15478 => 25478, + 15479 => 25479, + 15480 => 25480, + 15481 => 25481, + 15482 => 25482, + 15483 => 25483, + 15484 => 25484, + 15485 => 25485, + 15486 => 25486, + 15487 => 25487, + 15488 => 25488, + 15489 => 25489, + 15490 => 25490, + 15491 => 25491, + 15492 => 25492, + 15493 => 25493, + 15494 => 25494, + 15495 => 25495, + 15496 => 25496, + 15497 => 25497, + 15498 => 25498, + 15499 => 25499, + 15500 => 25500, + 15501 => 25501, + 15502 => 25502, + 15503 => 25503, + 15504 => 25504, + 15505 => 25505, + 15506 => 25506, + 15507 => 25507, + 15508 => 25508, + 15509 => 25509, + 15510 => 25510, + 15511 => 25511, + 15512 => 25512, + 15513 => 25513, + 15514 => 25514, + 15515 => 25515, + 15516 => 25516, + 15517 => 25517, + 15518 => 25518, + 15519 => 25519, + 15520 => 25520, + 15521 => 25521, + 15522 => 25522, + 15523 => 25523, + 15524 => 25524, + 15525 => 25525, + 15526 => 25526, + 15527 => 25527, + 15528 => 25528, + 15529 => 25529, + 15530 => 25530, + 15531 => 25531, + 15532 => 25532, + 15533 => 25533, + 15534 => 25534, + 15535 => 25535, + 15536 => 25536, + 15537 => 25537, + 15538 => 25538, + 15539 => 25539, + 15540 => 25540, + 15541 => 25541, + 15542 => 25542, + 15543 => 25543, + 15544 => 25544, + 15545 => 25545, + 15546 => 25546, + 15547 => 25547, + 15548 => 25548, + 15549 => 25549, + 15550 => 25550, + 15551 => 25551, + 15552 => 25552, + 15553 => 25553, + 15554 => 25554, + 15555 => 25555, + 15556 => 25556, + 15557 => 25557, + 15558 => 25558, + 15559 => 25559, + 15560 => 25560, + 15561 => 25561, + 15562 => 25562, + 15563 => 25563, + 15564 => 25564, + 15565 => 25565, + 15566 => 25566, + 15567 => 25567, + 15568 => 25568, + 15569 => 25569, + 15570 => 25570, + 15571 => 25571, + 15572 => 25572, + 15573 => 25573, + 15574 => 25574, + 15575 => 25575, + 15576 => 25576, + 15577 => 25577, + 15578 => 25578, + 15579 => 25579, + 15580 => 25580, + 15581 => 25581, + 15582 => 25582, + 15583 => 25583, + 15584 => 25584, + 15585 => 25585, + 15586 => 25586, + 15587 => 25587, + 15588 => 25588, + 15589 => 25589, + 15590 => 25590, + 15591 => 25591, + 15592 => 25592, + 15593 => 25593, + 15594 => 25594, + 15595 => 25595, + 15596 => 25596, + 15597 => 25597, + 15598 => 25598, + 15599 => 25599, + 15600 => 25600, + 15601 => 25601, + 15602 => 25602, + 15603 => 25603, + 15604 => 25604, + 15605 => 25605, + 15606 => 25606, + 15607 => 25607, + 15608 => 25608, + 15609 => 25609, + 15610 => 25610, + 15611 => 25611, + 15612 => 25612, + 15613 => 25613, + 15614 => 25614, + 15615 => 25615, + 15616 => 25616, + 15617 => 25617, + 15618 => 25618, + 15619 => 25619, + 15620 => 25620, + 15621 => 25621, + 15622 => 25622, + 15623 => 25623, + 15624 => 25624, + 15625 => 25625, + 15626 => 25626, + 15627 => 25627, + 15628 => 25628, + 15629 => 25629, + 15630 => 25630, + 15631 => 25631, + 15632 => 25632, + 15633 => 25633, + 15634 => 25634, + 15635 => 25635, + 15636 => 25636, + 15637 => 25637, + 15638 => 25638, + 15639 => 25639, + 15640 => 25640, + 15641 => 25641, + 15642 => 25642, + 15643 => 25643, + 15644 => 25644, + 15645 => 25645, + 15646 => 25646, + 15647 => 25647, + 15648 => 25648, + 15649 => 25649, + 15650 => 25650, + 15651 => 25651, + 15652 => 25652, + 15653 => 25653, + 15654 => 25654, + 15655 => 25655, + 15656 => 25656, + 15657 => 25657, + 15658 => 25658, + 15659 => 25659, + 15660 => 25660, + 15661 => 25661, + 15662 => 25662, + 15663 => 25663, + 15664 => 25664, + 15665 => 25665, + 15666 => 25666, + 15667 => 25667, + 15668 => 25668, + 15669 => 25669, + 15670 => 25670, + 15671 => 25671, + 15672 => 25672, + 15673 => 25673, + 15674 => 25674, + 15675 => 25675, + 15676 => 25676, + 15677 => 25677, + 15678 => 25678, + 15679 => 25679, + 15680 => 25680, + 15681 => 25681, + 15682 => 25682, + 15683 => 25683, + 15684 => 25684, + 15685 => 25685, + 15686 => 25686, + 15687 => 25687, + 15688 => 25688, + 15689 => 25689, + 15690 => 25690, + 15691 => 25691, + 15692 => 25692, + 15693 => 25693, + 15694 => 25694, + 15695 => 25695, + 15696 => 25696, + 15697 => 25697, + 15698 => 25698, + 15699 => 25699, + 15700 => 25700, + 15701 => 25701, + 15702 => 25702, + 15703 => 25703, + 15704 => 25704, + 15705 => 25705, + 15706 => 25706, + 15707 => 25707, + 15708 => 25708, + 15709 => 25709, + 15710 => 25710, + 15711 => 25711, + 15712 => 25712, + 15713 => 25713, + 15714 => 25714, + 15715 => 25715, + 15716 => 25716, + 15717 => 25717, + 15718 => 25718, + 15719 => 25719, + 15720 => 25720, + 15721 => 25721, + 15722 => 25722, + 15723 => 25723, + 15724 => 25724, + 15725 => 25725, + 15726 => 25726, + 15727 => 25727, + 15728 => 25728, + 15729 => 25729, + 15730 => 25730, + 15731 => 25731, + 15732 => 25732, + 15733 => 25733, + 15734 => 25734, + 15735 => 25735, + 15736 => 25736, + 15737 => 25737, + 15738 => 25738, + 15739 => 25739, + 15740 => 25740, + 15741 => 25741, + 15742 => 25742, + 15743 => 25743, + 15744 => 25744, + 15745 => 25745, + 15746 => 25746, + 15747 => 25747, + 15748 => 25748, + 15749 => 25749, + 15750 => 25750, + 15751 => 25751, + 15752 => 25752, + 15753 => 25753, + 15754 => 25754, + 15755 => 25755, + 15756 => 25756, + 15757 => 25757, + 15758 => 25758, + 15759 => 25759, + 15760 => 25760, + 15761 => 25761, + 15762 => 25762, + 15763 => 25763, + 15764 => 25764, + 15765 => 25765, + 15766 => 25766, + 15767 => 25767, + 15768 => 25768, + 15769 => 25769, + 15770 => 25770, + 15771 => 25771, + 15772 => 25772, + 15773 => 25773, + 15774 => 25774, + 15775 => 25775, + 15776 => 25776, + 15777 => 25777, + 15778 => 25778, + 15779 => 25779, + 15780 => 25780, + 15781 => 25781, + 15782 => 25782, + 15783 => 25783, + 15784 => 25784, + 15785 => 25785, + 15786 => 25786, + 15787 => 25787, + 15788 => 25788, + 15789 => 25789, + 15790 => 25790, + 15791 => 25791, + 15792 => 25792, + 15793 => 25793, + 15794 => 25794, + 15795 => 25795, + 15796 => 25796, + 15797 => 25797, + 15798 => 25798, + 15799 => 25799, + 15800 => 25800, + 15801 => 25801, + 15802 => 25802, + 15803 => 25803, + 15804 => 25804, + 15805 => 25805, + 15806 => 25806, + 15807 => 25807, + 15808 => 25808, + 15809 => 25809, + 15810 => 25810, + 15811 => 25811, + 15812 => 25812, + 15813 => 25813, + 15814 => 25814, + 15815 => 25815, + 15816 => 25816, + 15817 => 25817, + 15818 => 25818, + 15819 => 25819, + 15820 => 25820, + 15821 => 25821, + 15822 => 25822, + 15823 => 25823, + 15824 => 25824, + 15825 => 25825, + 15826 => 25826, + 15827 => 25827, + 15828 => 25828, + 15829 => 25829, + 15830 => 25830, + 15831 => 25831, + 15832 => 25832, + 15833 => 25833, + 15834 => 25834, + 15835 => 25835, + 15836 => 25836, + 15837 => 25837, + 15838 => 25838, + 15839 => 25839, + 15840 => 25840, + 15841 => 25841, + 15842 => 25842, + 15843 => 25843, + 15844 => 25844, + 15845 => 25845, + 15846 => 25846, + 15847 => 25847, + 15848 => 25848, + 15849 => 25849, + 15850 => 25850, + 15851 => 25851, + 15852 => 25852, + 15853 => 25853, + 15854 => 25854, + 15855 => 25855, + 15856 => 25856, + 15857 => 25857, + 15858 => 25858, + 15859 => 25859, + 15860 => 25860, + 15861 => 25861, + 15862 => 25862, + 15863 => 25863, + 15864 => 25864, + 15865 => 25865, + 15866 => 25866, + 15867 => 25867, + 15868 => 25868, + 15869 => 25869, + 15870 => 25870, + 15871 => 25871, + 15872 => 25872, + 15873 => 25873, + 15874 => 25874, + 15875 => 25875, + 15876 => 25876, + 15877 => 25877, + 15878 => 25878, + 15879 => 25879, + 15880 => 25880, + 15881 => 25881, + 15882 => 25882, + 15883 => 25883, + 15884 => 25884, + 15885 => 25885, + 15886 => 25886, + 15887 => 25887, + 15888 => 25888, + 15889 => 25889, + 15890 => 25890, + 15891 => 25891, + 15892 => 25892, + 15893 => 25893, + 15894 => 25894, + 15895 => 25895, + 15896 => 25896, + 15897 => 25897, + 15898 => 25898, + 15899 => 25899, + 15900 => 25900, + 15901 => 25901, + 15902 => 25902, + 15903 => 25903, + 15904 => 25904, + 15905 => 25905, + 15906 => 25906, + 15907 => 25907, + 15908 => 25908, + 15909 => 25909, + 15910 => 25910, + 15911 => 25911, + 15912 => 25912, + 15913 => 25913, + 15914 => 25914, + 15915 => 25915, + 15916 => 25916, + 15917 => 25917, + 15918 => 25918, + 15919 => 25919, + 15920 => 25920, + 15921 => 25921, + 15922 => 25922, + 15923 => 25923, + 15924 => 25924, + 15925 => 25925, + 15926 => 25926, + 15927 => 25927, + 15928 => 25928, + 15929 => 25929, + 15930 => 25930, + 15931 => 25931, + 15932 => 25932, + 15933 => 25933, + 15934 => 25934, + 15935 => 25935, + 15936 => 25936, + 15937 => 25937, + 15938 => 25938, + 15939 => 25939, + 15940 => 25940, + 15941 => 25941, + 15942 => 25942, + 15943 => 25943, + 15944 => 25944, + 15945 => 25945, + 15946 => 25946, + 15947 => 25947, + 15948 => 25948, + 15949 => 25949, + 15950 => 25950, + 15951 => 25951, + 15952 => 25952, + 15953 => 25953, + 15954 => 25954, + 15955 => 25955, + 15956 => 25956, + 15957 => 25957, + 15958 => 25958, + 15959 => 25959, + 15960 => 25960, + 15961 => 25961, + 15962 => 25962, + 15963 => 25963, + 15964 => 25964, + 15965 => 25965, + 15966 => 25966, + 15967 => 25967, + 15968 => 25968, + 15969 => 25969, + 15970 => 25970, + 15971 => 25971, + 15972 => 25972, + 15973 => 25973, + 15974 => 25974, + 15975 => 25975, + 15976 => 25976, + 15977 => 25977, + 15978 => 25978, + 15979 => 25979, + 15980 => 25980, + 15981 => 25981, + 15982 => 25982, + 15983 => 25983, + 15984 => 25984, + 15985 => 25985, + 15986 => 25986, + 15987 => 25987, + 15988 => 25988, + 15989 => 25989, + 15990 => 25990, + 15991 => 25991, + 15992 => 25992, + 15993 => 25993, + 15994 => 25994, + 15995 => 25995, + 15996 => 25996, + 15997 => 25997, + 15998 => 25998, + 15999 => 25999, + 16000 => 26000, + 16001 => 26001, + 16002 => 26002, + 16003 => 26003, + 16004 => 26004, + 16005 => 26005, + 16006 => 26006, + 16007 => 26007, + 16008 => 26008, + 16009 => 26009, + 16010 => 26010, + 16011 => 26011, + 16012 => 26012, + 16013 => 26013, + 16014 => 26014, + 16015 => 26015, + 16016 => 26016, + 16017 => 26017, + 16018 => 26018, + 16019 => 26019, + 16020 => 26020, + 16021 => 26021, + 16022 => 26022, + 16023 => 26023, + 16024 => 26024, + 16025 => 26025, + 16026 => 26026, + 16027 => 26027, + 16028 => 26028, + 16029 => 26029, + 16030 => 26030, + 16031 => 26031, + 16032 => 26032, + 16033 => 26033, + 16034 => 26034, + 16035 => 26035, + 16036 => 26036, + 16037 => 26037, + 16038 => 26038, + 16039 => 26039, + 16040 => 26040, + 16041 => 26041, + 16042 => 26042, + 16043 => 26043, + 16044 => 26044, + 16045 => 26045, + 16046 => 26046, + 16047 => 26047, + 16048 => 26048, + 16049 => 26049, + 16050 => 26050, + 16051 => 26051, + 16052 => 26052, + 16053 => 26053, + 16054 => 26054, + 16055 => 26055, + 16056 => 26056, + 16057 => 26057, + 16058 => 26058, + 16059 => 26059, + 16060 => 26060, + 16061 => 26061, + 16062 => 26062, + 16063 => 26063, + 16064 => 26064, + 16065 => 26065, + 16066 => 26066, + 16067 => 26067, + 16068 => 26068, + 16069 => 26069, + 16070 => 26070, + 16071 => 26071, + 16072 => 26072, + 16073 => 26073, + 16074 => 26074, + 16075 => 26075, + 16076 => 26076, + 16077 => 26077, + 16078 => 26078, + 16079 => 26079, + 16080 => 26080, + 16081 => 26081, + 16082 => 26082, + 16083 => 26083, + 16084 => 26084, + 16085 => 26085, + 16086 => 26086, + 16087 => 26087, + 16088 => 26088, + 16089 => 26089, + 16090 => 26090, + 16091 => 26091, + 16092 => 26092, + 16093 => 26093, + 16094 => 26094, + 16095 => 26095, + 16096 => 26096, + 16097 => 26097, + 16098 => 26098, + 16099 => 26099, + 16100 => 26100, + 16101 => 26101, + 16102 => 26102, + 16103 => 26103, + 16104 => 26104, + 16105 => 26105, + 16106 => 26106, + 16107 => 26107, + 16108 => 26108, + 16109 => 26109, + 16110 => 26110, + 16111 => 26111, + 16112 => 26112, + 16113 => 26113, + 16114 => 26114, + 16115 => 26115, + 16116 => 26116, + 16117 => 26117, + 16118 => 26118, + 16119 => 26119, + 16120 => 26120, + 16121 => 26121, + 16122 => 26122, + 16123 => 26123, + 16124 => 26124, + 16125 => 26125, + 16126 => 26126, + 16127 => 26127, + 16128 => 26128, + 16129 => 26129, + 16130 => 26130, + 16131 => 26131, + 16132 => 26132, + 16133 => 26133, + 16134 => 26134, + 16135 => 26135, + 16136 => 26136, + 16137 => 26137, + 16138 => 26138, + 16139 => 26139, + 16140 => 26140, + 16141 => 26141, + 16142 => 26142, + 16143 => 26143, + 16144 => 26144, + 16145 => 26145, + 16146 => 26146, + 16147 => 26147, + 16148 => 26148, + 16149 => 26149, + 16150 => 26150, + 16151 => 26151, + 16152 => 26152, + 16153 => 26153, + 16154 => 26154, + 16155 => 26155, + 16156 => 26156, + 16157 => 26157, + 16158 => 26158, + 16159 => 26159, + 16160 => 26160, + 16161 => 26161, + 16162 => 26162, + 16163 => 26163, + 16164 => 26164, + 16165 => 26165, + 16166 => 26166, + 16167 => 26167, + 16168 => 26168, + 16169 => 26169, + 16170 => 26170, + 16171 => 26171, + 16172 => 26172, + 16173 => 26173, + 16174 => 26174, + 16175 => 26175, + 16176 => 26176, + 16177 => 26177, + 16178 => 26178, + 16179 => 26179, + 16180 => 26180, + 16181 => 26181, + 16182 => 26182, + 16183 => 26183, + 16184 => 26184, + 16185 => 26185, + 16186 => 26186, + 16187 => 26187, + 16188 => 26188, + 16189 => 26189, + 16190 => 26190, + 16191 => 26191, + 16192 => 26192, + 16193 => 26193, + 16194 => 26194, + 16195 => 26195, + 16196 => 26196, + 16197 => 26197, + 16198 => 26198, + 16199 => 26199, + 16200 => 26200, + 16201 => 26201, + 16202 => 26202, + 16203 => 26203, + 16204 => 26204, + 16205 => 26205, + 16206 => 26206, + 16207 => 26207, + 16208 => 26208, + 16209 => 26209, + 16210 => 26210, + 16211 => 26211, + 16212 => 26212, + 16213 => 26213, + 16214 => 26214, + 16215 => 26215, + 16216 => 26216, + 16217 => 26217, + 16218 => 26218, + 16219 => 26219, + 16220 => 26220, + 16221 => 26221, + 16222 => 26222, + 16223 => 26223, + 16224 => 26224, + 16225 => 26225, + 16226 => 26226, + 16227 => 26227, + 16228 => 26228, + 16229 => 26229, + 16230 => 26230, + 16231 => 26231, + 16232 => 26232, + 16233 => 26233, + 16234 => 26234, + 16235 => 26235, + 16236 => 26236, + 16237 => 26237, + 16238 => 26238, + 16239 => 26239, + 16240 => 26240, + 16241 => 26241, + 16242 => 26242, + 16243 => 26243, + 16244 => 26244, + 16245 => 26245, + 16246 => 26246, + 16247 => 26247, + 16248 => 26248, + 16249 => 26249, + 16250 => 26250, + 16251 => 26251, + 16252 => 26252, + 16253 => 26253, + 16254 => 26254, + 16255 => 26255, + 16256 => 26256, + 16257 => 26257, + 16258 => 26258, + 16259 => 26259, + 16260 => 26260, + 16261 => 26261, + 16262 => 26262, + 16263 => 26263, + 16264 => 26264, + 16265 => 26265, + 16266 => 26266, + 16267 => 26267, + 16268 => 26268, + 16269 => 26269, + 16270 => 26270, + 16271 => 26271, + 16272 => 26272, + 16273 => 26273, + 16274 => 26274, + 16275 => 26275, + 16276 => 26276, + 16277 => 26277, + 16278 => 26278, + 16279 => 26279, + 16280 => 26280, + 16281 => 26281, + 16282 => 26282, + 16283 => 26283, + 16284 => 26284, + 16285 => 26285, + 16286 => 26286, + 16287 => 26287, + 16288 => 26288, + 16289 => 26289, + 16290 => 26290, + 16291 => 26291, + 16292 => 26292, + 16293 => 26293, + 16294 => 26294, + 16295 => 26295, + 16296 => 26296, + 16297 => 26297, + 16298 => 26298, + 16299 => 26299, + 16300 => 26300, + 16301 => 26301, + 16302 => 26302, + 16303 => 26303, + 16304 => 26304, + 16305 => 26305, + 16306 => 26306, + 16307 => 26307, + 16308 => 26308, + 16309 => 26309, + 16310 => 26310, + 16311 => 26311, + 16312 => 26312, + 16313 => 26313, + 16314 => 26314, + 16315 => 26315, + 16316 => 26316, + 16317 => 26317, + 16318 => 26318, + 16319 => 26319, + 16320 => 26320, + 16321 => 26321, + 16322 => 26322, + 16323 => 26323, + 16324 => 26324, + 16325 => 26325, + 16326 => 26326, + 16327 => 26327, + 16328 => 26328, + 16329 => 26329, + 16330 => 26330, + 16331 => 26331, + 16332 => 26332, + 16333 => 26333, + 16334 => 26334, + 16335 => 26335, + 16336 => 26336, + 16337 => 26337, + 16338 => 26338, + 16339 => 26339, + 16340 => 26340, + 16341 => 26341, + 16342 => 26342, + 16343 => 26343, + 16344 => 26344, + 16345 => 26345, + 16346 => 26346, + 16347 => 26347, + 16348 => 26348, + 16349 => 26349, + 16350 => 26350, + 16351 => 26351, + 16352 => 26352, + 16353 => 26353, + 16354 => 26354, + 16355 => 26355, + 16356 => 26356, + 16357 => 26357, + 16358 => 26358, + 16359 => 26359, + 16360 => 26360, + 16361 => 26361, + 16362 => 26362, + 16363 => 26363, + 16364 => 26364, + 16365 => 26365, + 16366 => 26366, + 16367 => 26367, + 16368 => 26368, + 16369 => 26369, + 16370 => 26370, + 16371 => 26371, + 16372 => 26372, + 16373 => 26373, + 16374 => 26374, + 16375 => 26375, + 16376 => 26376, + 16377 => 26377, + 16378 => 26378, + 16379 => 26379, + 16380 => 26380, + 16381 => 26381, + 16382 => 26382, + 16383 => 26383, + 16384 => 26384, + 16385 => 26385, + 16386 => 26386, + 16387 => 26387, + 16388 => 26388, + 16389 => 26389, + 16390 => 26390, + 16391 => 26391, + 16392 => 26392, + 16393 => 26393, + 16394 => 26394, + 16395 => 26395, + 16396 => 26396, + 16397 => 26397, + 16398 => 26398, + 16399 => 26399, + 16400 => 26400, + 16401 => 26401, + 16402 => 26402, + 16403 => 26403, + 16404 => 26404, + 16405 => 26405, + 16406 => 26406, + 16407 => 26407, + 16408 => 26408, + 16409 => 26409, + 16410 => 26410, + 16411 => 26411, + 16412 => 26412, + 16413 => 26413, + 16414 => 26414, + 16415 => 26415, + 16416 => 26416, + 16417 => 26417, + 16418 => 26418, + 16419 => 26419, + 16420 => 26420, + 16421 => 26421, + 16422 => 26422, + 16423 => 26423, + 16424 => 26424, + 16425 => 26425, + 16426 => 26426, + 16427 => 26427, + 16428 => 26428, + 16429 => 26429, + 16430 => 26430, + 16431 => 26431, + 16432 => 26432, + 16433 => 26433, + 16434 => 26434, + 16435 => 26435, + 16436 => 26436, + 16437 => 26437, + 16438 => 26438, + 16439 => 26439, + 16440 => 26440, + 16441 => 26441, + 16442 => 26442, + 16443 => 26443, + 16444 => 26444, + 16445 => 26445, + 16446 => 26446, + 16447 => 26447, + 16448 => 26448, + 16449 => 26449, + 16450 => 26450, + 16451 => 26451, + 16452 => 26452, + 16453 => 26453, + 16454 => 26454, + 16455 => 26455, + 16456 => 26456, + 16457 => 26457, + 16458 => 26458, + 16459 => 26459, + 16460 => 26460, + 16461 => 26461, + 16462 => 26462, + 16463 => 26463, + 16464 => 26464, + 16465 => 26465, + 16466 => 26466, + 16467 => 26467, + 16468 => 26468, + 16469 => 26469, + 16470 => 26470, + 16471 => 26471, + 16472 => 26472, + 16473 => 26473, + 16474 => 26474, + 16475 => 26475, + 16476 => 26476, + 16477 => 26477, + 16478 => 26478, + 16479 => 26479, + 16480 => 26480, + 16481 => 26481, + 16482 => 26482, + 16483 => 26483, + 16484 => 26484, + 16485 => 26485, + 16486 => 26486, + 16487 => 26487, + 16488 => 26488, + 16489 => 26489, + 16490 => 26490, + 16491 => 26491, + 16492 => 26492, + 16493 => 26493, + 16494 => 26494, + 16495 => 26495, + 16496 => 26496, + 16497 => 26497, + 16498 => 26498, + 16499 => 26499, + 16500 => 26500, +]; + +$firstArrayValue = TEST_ARRAY_1[rand(1, 6500)]; +$secondArrayValue = TEST_ARRAY_2[$firstArrayValue] ?? null; diff --git a/tests/PHPStan/Analyser/data/bug-8225.php b/tests/PHPStan/Analyser/data/bug-8225.php new file mode 100644 index 0000000000..5c8663ab64 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8225.php @@ -0,0 +1,28 @@ +, + * } $array + */ + public function sayHello(array $array, string $string): void + { + assertType('array{notImportant: bool, attributesRequiredLogistic?: array}', $array); + unset($array[$string]); + assertType('array{notImportant?: bool, attributesRequiredLogistic?: array}', $array); + } + + public function edgeCase(): void + { + $arr = [1,2,3]; + unset($arr['1']); + assertType('array{0: 1, 2: 3}', $arr); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8242.php b/tests/PHPStan/Analyser/data/bug-8242.php new file mode 100644 index 0000000000..3e516a7199 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8242.php @@ -0,0 +1,60 @@ + foo()]; + + if (is_int($x['x'])) { + assertType('array{x: int}', $x); + assertType('int', $x['x']); + assertType('true', is_int($x['x'])); + } else { + assertType('array{x: mixed~int}', $x); + assertType('mixed~int', $x['x']); + assertType('false', is_int($x['x'])); + } +}; diff --git a/tests/PHPStan/Analyser/data/bug-8272.php b/tests/PHPStan/Analyser/data/bug-8272.php new file mode 100644 index 0000000000..e26fba40f4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8272.php @@ -0,0 +1,10 @@ +', mt_rand(1, 5)); + assertType('int<0, max>', mt_rand()); +}; diff --git a/tests/PHPStan/Analyser/data/bug-8361.php b/tests/PHPStan/Analyser/data/bug-8361.php new file mode 100644 index 0000000000..cf0bb34b9b --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8361.php @@ -0,0 +1,32 @@ +format(DateTimeInterface::ATOM); + assertType('true', $from || $to); + assertType('DateTimeInterface', $from ?? $to); + } + } + + public function sayHello2(?DateTimeInterface $from = null, ?DateTimeInterface $to = null): void + { + if ($from || $to) { + $operator = $from ? 'notBefore' : 'notAfter'; + $date = ($from ?? $to)->format(DateTimeInterface::ATOM); + $date = ($from ?? $to)->format(DateTimeInterface::ATOM); + assertType('true', $from || $to); + assertType('DateTimeInterface', $from ?? $to); + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8366.php b/tests/PHPStan/Analyser/data/bug-8366.php new file mode 100644 index 0000000000..fd6c65e972 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8366.php @@ -0,0 +1,36 @@ + $untilDate) { + assertType('DateTimeImmutable', $untilDate); + assertType('null', $count); + throw new \InvalidArgumentException('End date must not be greater than until date.'); + } + + if ($count !== null && $count < 1) { + assertType('null', $untilDate); + assertType('int', $count); + throw new \InvalidArgumentException('Count must be positive.'); + } + + assertType('DateTimeImmutable|null', $untilDate); + assertType('int<1, max>|null', $count); +} diff --git a/tests/PHPStan/Analyser/data/bug-8373.php b/tests/PHPStan/Analyser/data/bug-8373.php new file mode 100644 index 0000000000..54471fb19f --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8373.php @@ -0,0 +1,19 @@ +foo($a); + assertType('int', $a); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8442.php b/tests/PHPStan/Analyser/data/bug-8442.php new file mode 100644 index 0000000000..96005d7d85 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8442.php @@ -0,0 +1,41 @@ + + * + * @phpstan-param list|null $mirrors + */ + protected function getUrls(?string $url, ?array $mirrors, ?string $ref, ?string $type, string $urlType): array + { + if (!$url) { + return []; + } + + if ($urlType === 'dist' && false !== strpos($url, '%')) { + assertType('string|null', $type); + $url = 'test'; + } + assertType('non-falsy-string', $url); + + $urls = [$url]; + if ($mirrors) { + foreach ($mirrors as $mirror) { + if ($urlType === 'dist') { + assertType('string|null', $type); + } elseif ($urlType === 'source' && $type === 'git') { + assertType("'git'", $type); + } elseif ($urlType === 'source' && $type === 'hg') { + assertType("'hg'", $type); + } else { + continue; + } + } + } + + return $urls; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8486.php b/tests/PHPStan/Analyser/data/bug-8486.php new file mode 100644 index 0000000000..1f2025276a --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8486.php @@ -0,0 +1,88 @@ += 8.1 + +namespace Bug8486; + +use function PHPStan\Testing\assertType; + +enum Operator: string +{ + case Foo = 'foo'; + case Bar = 'bar'; + case None = ''; + + public function explode(): void + { + $character = match ($this) { + self::None => 'baz', + default => $this->value, + }; + + assertType("'bar'|'baz'|'foo'", $character); + } + + public function typeInference(): void + { + match ($this) { + self::None => 'baz', + default => assertType('$this(Bug8486\Operator~Bug8486\Operator::None)', $this), + }; + } + + public function typeInference2(): void + { + if ($this === self::None) { + return; + } + + assertType("'Bar'|'Foo'", $this->name); + assertType("'bar'|'foo'", $this->value); + } +} + +class Foo +{ + + public function doFoo(Operator $operator) + { + $character = match ($operator) { + Operator::None => 'baz', + default => $operator->value, + }; + + assertType("'bar'|'baz'|'foo'", $character); + } + + public function typeInference(Operator $operator): void + { + match ($operator) { + Operator::None => 'baz', + default => assertType('Bug8486\Operator~Bug8486\Operator::None', $operator), + }; + } + + public function typeInference2(Operator $operator): void + { + if ($operator === Operator::None) { + return; + } + + assertType("'Bar'|'Foo'", $operator->name); + assertType("'bar'|'foo'", $operator->value); + } + + public function typeInference3(Operator $operator): void + { + if ($operator === Operator::None) { + return; + } + + if ($operator === Operator::Foo) { + return; + } + + assertType("Bug8486\Operator::Bar", $operator); + assertType("'Bar'", $operator->name); + assertType("'bar'", $operator->value); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-8503.php b/tests/PHPStan/Analyser/data/bug-8503.php new file mode 100644 index 0000000000..a118d03d01 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8503.php @@ -0,0 +1,46 @@ +prepare('SELECT x FROM z'); + $rows = $qry->fetchAll() ?: []; + + foreach($rows as $row) { + $matrix[$row['x']] = []; + + foreach($rows as $row2) { + $matrix[$row['x']][$row2['x']] = []; + + foreach($rows as $row3) { + $matrix[$row['x']][$row2['x']][$row3['x']] = []; + + foreach($rows as $row4) { + $matrix[$row['x']][$row2['x']][$row3['x']][$row4['x']] = []; + + foreach($rows as $row5) { + $matrix[$row['x']][$row2['x']][$row3['x']][$row4['x']][$row5['x']] = []; + + foreach($rows as $row6) { + $matrix[$row['x']][$row2['x']][$row3['x']][$row4['x']][$row5['x']][$row6['x']] = []; + + foreach($rows as $row7) { + $matrix[$row['x']][$row2['x']][$row3['x']][$row4['x']][$row5['x']][$row6['x']][$row7['x']] = []; + + foreach($rows as $row8) { + $matrix[$row['x']][$row2['x']][$row3['x']][$row4['x']][$row5['x']][$row6['x']][$row7['x']][$row8['x']] = []; + } + } + } + } + } + } + } + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8517.php b/tests/PHPStan/Analyser/data/bug-8517.php new file mode 100644 index 0000000000..ab6570b796 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8517.php @@ -0,0 +1,15 @@ +attributes->get('_route_params', []); + assertType('stdClass|null', $request); + $routeParams = $request?->attributes->get('_route_params', []) ?? []; + $param = $request?->attributes->get('_param') ?? $routeParams['_param']; + assertType('stdClass|null', $request); +} diff --git a/tests/PHPStan/Analyser/data/bug-8520.php b/tests/PHPStan/Analyser/data/bug-8520.php new file mode 100644 index 0000000000..d5d6c605fd --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8520.php @@ -0,0 +1,15 @@ +', $i); + $tryMax = true; + while ($tryMax) { + $tryMax = false; + } +} + +assertType('int<7, max>', $i); diff --git a/tests/PHPStan/Analyser/data/bug-8537.php b/tests/PHPStan/Analyser/data/bug-8537.php new file mode 100644 index 0000000000..b0f36e6623 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8537.php @@ -0,0 +1,39 @@ += 8.0 + +namespace Bug8537; + +/** + * @property int $x + */ +interface SampleInterface +{ +} + +class Sample implements SampleInterface +{ + /** @param array $data */ + public function __construct(private array $data = []) + { + } + + public function __set(string $key, mixed $value): void + { + $this->data[$key] = $value; + } + + public function __get(string $key): mixed + { + return $this->data[$key] ?? null; + } + + public function __isset(string $key): bool + { + return array_key_exists($key, $this->data); + } +} + +function (): void { + $test = new Sample(); + $test->x = 3; + echo $test->x; +}; diff --git a/tests/PHPStan/Analyser/data/bug-8543.php b/tests/PHPStan/Analyser/data/bug-8543.php new file mode 100644 index 0000000000..01d7b93c45 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8543.php @@ -0,0 +1,31 @@ += 8.1 + +namespace Bug8543; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public readonly int $i; + + public int $j; + + public function invalidate(): void + { + } +} + +function (HelloWorld $hw): void { + $hw->i = 1; + $hw->j = 2; + assertType('1', $hw->i); + assertType('2', $hw->j); + + $hw->invalidate(); + assertType('1', $hw->i); + assertType('int', $hw->j); + + $hw = new HelloWorld(); + assertType('int', $hw->i); + assertType('int', $hw->j); +}; diff --git a/tests/PHPStan/Analyser/data/bug-8568.php b/tests/PHPStan/Analyser/data/bug-8568.php new file mode 100644 index 0000000000..71db7a6c98 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8568.php @@ -0,0 +1,26 @@ +get()); + } + + public function get(): ?int + { + return rand() ? 5 : null; + } + + /** + * @param numeric-string $numericS + */ + public function intersections($numericS): void { + assertType('non-falsy-string', 'a'. $numericS); + assertType('numeric-string', (string) $numericS); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8609.php b/tests/PHPStan/Analyser/data/bug-8609.php new file mode 100644 index 0000000000..bac619be6a --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8609.php @@ -0,0 +1,38 @@ + $f + * @param Foo $g + * @param Foo $h + * @param Foo $i + */ + public function doFoo(Foo $f, Foo $g, Foo $h, Foo $i): void + { + assertType('\'foo\'', $f->doFoo()); + assertType('\'bar\'', $g->doFoo()); + assertType('\'bar\'|\'foo\'', $h->doFoo()); + assertType('\'bar\'|\'foo\'', $i->doFoo()); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-8621.php b/tests/PHPStan/Analyser/data/bug-8621.php new file mode 100644 index 0000000000..9bec8c9138 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8621.php @@ -0,0 +1,27 @@ + $data + */ + public function rows (array $data): void + { + $even = true; + + echo ""; + foreach ($data as $datum) + { + $even = !$even; + assertType('bool', $even); + + echo ""; + echo ""; + } + echo "
{$datum}
"; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8625.php b/tests/PHPStan/Analyser/data/bug-8625.php new file mode 100644 index 0000000000..0558b314b9 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8625.php @@ -0,0 +1,23 @@ +id = $id; + } + + public function getId(): ?int + { + return $this->id; + } + + public function setUsername(?string $username = null): void + { + $this->username = $username; + } + + public function getUsername(): ?string + { + return $this->username; + } +} + +class DataObject +{ + protected ?UserObject $user = null; + + public function setUser(?UserObject $user = null): void + { + $this->user = $user; + } + + public function getUser(): ?UserObject + { + return $this->user; + } +} + +class Test +{ + public function test(): void + { + $data = new DataObject(); + + $userObject = $data->getUser(); + + if ($userObject?->getId() > 0) { + $userId = $userObject->getId(); + + var_dump($userId); + } + + if (null !== $userObject?->getUsername()) { + $userUsername = $userObject->getUsername(); + + var_dump($userUsername); + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8752.php b/tests/PHPStan/Analyser/data/bug-8752.php new file mode 100644 index 0000000000..8c9117a47e --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8752.php @@ -0,0 +1,21 @@ +abc(); + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8775.php b/tests/PHPStan/Analyser/data/bug-8775.php new file mode 100644 index 0000000000..3a1678e919 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8775.php @@ -0,0 +1,279 @@ +format('N') + $offset; + if ($value > 7) { + } + + $value2 = $offset + $from->format('N'); + $value3 = '1e3' + $offset; + $value4 = $offset + '1e3'; + + assertType("'1'|'2'|'3'|'4'|'5'|'6'|'7'", $from->format('N')); + assertType('int<1, 14>', $offset); + assertType('int<2, 21>', $value); + assertType('int<2, 21>', $value2); + assertType('float', $value3); + assertType('float', $value4); + } + } + + public function testWithMixed(mixed $a, mixed $b): void + { + assertType('(array|float|int)', $a + $b); + assertType('(float|int)', 3 + $b); + assertType('(float|int)', $a + 3); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8827.php b/tests/PHPStan/Analyser/data/bug-8827.php new file mode 100644 index 0000000000..fae38f26b2 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8827.php @@ -0,0 +1,27 @@ +', $efferent); // Expected: int<0, $nbElements> | Actual: 0|1 + assertType('int<0, max>', $afferent); // Expected: int<0, $nbElements> | Actual: 0|1 + + $instability = ($efferent + $afferent > 0) ? $efferent / ($afferent + $efferent) : 0; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8917.php b/tests/PHPStan/Analyser/data/bug-8917.php new file mode 100644 index 0000000000..2cc2106202 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8917.php @@ -0,0 +1,22 @@ + 1]], 'a'); + + assertType('array{1}', $array); + assertType('1', count($array)); + assertType('true', array_key_exists(0, $array)); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-8924.php b/tests/PHPStan/Analyser/data/bug-8924.php new file mode 100644 index 0000000000..ccb3ccdf45 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8924.php @@ -0,0 +1,32 @@ + $array + */ +function foo(array $array): void { + foreach ($array as $element) { + assertType('int', $element); + $array = null; + } +} + +function makeValidNumbers(): array +{ + $validNumbers = [1, 2]; + foreach ($validNumbers as $k => $v) { + assertType("non-empty-list<-2|-1|1|2|' 1'|' 2'>", $validNumbers); + assertType('0|1', $k); + assertType('1|2', $v); + $validNumbers[] = -$v; + $validNumbers[] = ' ' . (string)$v; + assertType("non-empty-list<-2|-1|1|2|' 1'|' 2'>", $validNumbers); + } + + assertType("non-empty-list<-2|-1|1|2|' 1'|' 2'>", $validNumbers); + + return $validNumbers; +} diff --git a/tests/PHPStan/Analyser/data/bug-8956.php b/tests/PHPStan/Analyser/data/bug-8956.php new file mode 100644 index 0000000000..15ba7b8dfd --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8956.php @@ -0,0 +1,29 @@ +', array_chunk(range(0, 10), 60)); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-8983.php b/tests/PHPStan/Analyser/data/bug-8983.php new file mode 100644 index 0000000000..d75242220d --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8983.php @@ -0,0 +1,30 @@ += 8.1 + +namespace Bug8983; + +use function PHPStan\Testing\assertType; + +enum Enum1: string +{ + + case FOO = 'foo'; + +} + +enum Enum2: string +{ + + case BAR = 'bar'; + +} + +class Foo +{ + + /** @param value-of $bar */ + public function doFoo($bar): void + { + assertType("'bar'|'foo'", $bar); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-9000.php b/tests/PHPStan/Analyser/data/bug-9000.php new file mode 100644 index 0000000000..281a6156be --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9000.php @@ -0,0 +1,31 @@ += 8.1 + +namespace Bug9000; + +use function PHPStan\Testing\assertType; + +enum A:string { + case A = "A"; + case B = "B"; + case C = "C"; +} + +const A_ARRAY = [ + 'A' => A::A, + 'B' => A::B, +]; + +/** + * @param string $key + * @return value-of + */ +function testA(string $key): A +{ + return A_ARRAY[$key]; +} + +function (): void { + $test = testA('A'); + assertType('Bug9000\A::A|Bug9000\A::B', $test); + assertType("'A'|'B'", $test->value); +}; diff --git a/tests/PHPStan/Analyser/data/bug-9008.php b/tests/PHPStan/Analyser/data/bug-9008.php new file mode 100644 index 0000000000..fb71337bd5 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9008.php @@ -0,0 +1,69 @@ +shouldWorkOne($alpha)); + assertType('Bug9008\\A|Bug9008\\B|Bug9008\\C', $this->shouldWorkTwo($alpha)); + assertType('Bug9008\\A|Bug9008\\B|Bug9008\\C', $this->worksButExtraVerboseOne($alpha)); + assertType('Bug9008\\A|Bug9008\\B|Bug9008\\C', $this->worksButExtraVerboseTwo($alpha)); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-9039.php b/tests/PHPStan/Analyser/data/bug-9039.php new file mode 100644 index 0000000000..9411171099 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9039.php @@ -0,0 +1,19 @@ + + */ +class Test extends Voter +{ + public const FOO = 'Foo'; + private const RULES = [self::FOO]; +} diff --git a/tests/PHPStan/Analyser/data/bug-9062.php b/tests/PHPStan/Analyser/data/bug-9062.php new file mode 100644 index 0000000000..a4c8cc6251 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9062.php @@ -0,0 +1,42 @@ +port = $value; + } elseif (is_string($value) && strspn($value, '0123456789') === strlen($value)) { + $this->port = (int) $value; + } else { + throw new \Exception("Property {$name} can only be a null, an int or a string containing the latter."); + } + } else { + throw new \Exception("Unknown property {$name}."); + } + } + + public function __get(string $name): mixed { + if ($name === 'port') { + return $this->port; + } else { + throw new \Exception("Unknown property {$name}."); + } + } +} + +function (): void { + $foo = new Foo; + $foo->port = "66"; + + assertType('int|null', $foo->port); +}; diff --git a/tests/PHPStan/Analyser/data/bug-9084.php b/tests/PHPStan/Analyser/data/bug-9084.php new file mode 100644 index 0000000000..b44c1f9010 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9084.php @@ -0,0 +1,76 @@ += 8.1 + +namespace Bug9084; + +use function PHPStan\Testing\assertType; + +enum UnitType +{ + case Mass; + + case Length; +} + +/** + * @template TUnitType of UnitType::* + */ +interface UnitInterface +{ + public function getValue(): float; +} + +/** + * @implements UnitInterface + */ +enum MassUnit: int implements UnitInterface +{ + case KiloGram = 1000000; + + case Gram = 1000; + + case MilliGram = 1; + + public function getValue(): float + { + return $this->value; + } +} + +/** + * @template TUnit of UnitType::* + */ +class Value +{ + public function __construct( + public readonly float $value, + /** @var UnitInterface */ + public readonly UnitInterface $unit + ) { + } + + /** + * @param UnitInterface $unit + * @return Value + */ + public function convert(UnitInterface $unit): Value + { + return new Value($this->value / $unit->getValue(), $unit); + } +} + +/** + * @template S + * @param S $value + * @return S + */ +function duplicate($value) +{ + return clone $value; +} + +function (): void { + $a = new Value(10, MassUnit::KiloGram); + assertType('Bug9084\Value', $a); + $b = duplicate($a); + assertType('Bug9084\Value', $b); +}; diff --git a/tests/PHPStan/Analyser/data/bug-9105.php b/tests/PHPStan/Analyser/data/bug-9105.php new file mode 100644 index 0000000000..956d53f055 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9105.php @@ -0,0 +1,24 @@ +b); + if ($this->b?->a < 5) { + echo '<5', PHP_EOL; + } + assertType('Bug9105\H|null', $this->b); + if ($this->b?->a > 0) { + echo '>0', PHP_EOL; + } + assertType('Bug9105\H|null', $this->b); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-9123.php b/tests/PHPStan/Analyser/data/bug-9123.php new file mode 100644 index 0000000000..d1d307fff1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9123.php @@ -0,0 +1,53 @@ + */ +final class Implementation implements EventListener +{ + public function canBeListen(Event $event): bool + { + return $event instanceof MyEvent; + } + + public function listen(Event $event): void + { + if (! $this->canBeListen($event)) { + return; + } + + \PHPStan\Testing\assertType('Bug9123\MyEvent', $event); + } +} + +/** @implements EventListener */ +final class Implementation2 implements EventListener +{ + /** @phpstan-assert-if-true MyEvent $event */ + public function canBeListen(Event $event): bool + { + return $event instanceof MyEvent; + } + + public function listen(Event $event): void + { + if (! $this->canBeListen($event)) { + return; + } + + \PHPStan\Testing\assertType('Bug9123\MyEvent', $event); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-9131.php b/tests/PHPStan/Analyser/data/bug-9131.php new file mode 100644 index 0000000000..97302bf1d3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9131.php @@ -0,0 +1,37 @@ + $b + * @param array, string> $c + * @param array|string, string> $d + * @return void + */ + public function doFoo( + array $a, + array $b, + array $c, + array $d + ): void + { + $a[] = 'foo'; + assertType('non-empty-array', $a); + + $b[] = 'foo'; + assertType('non-empty-array', $b); + + $c[] = 'foo'; + assertType('non-empty-array, string>', $c); + + $d[] = 'foo'; + assertType('non-empty-array|string, string>', $d); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-9208.php b/tests/PHPStan/Analyser/data/bug-9208.php new file mode 100644 index 0000000000..fa9ec21a30 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9208.php @@ -0,0 +1,22 @@ + $id_or_ids + * @return non-empty-list + */ +function f(int|array $id_or_ids): array +{ + if (is_array($id_or_ids)) { + assertType('non-empty-list', (array)$id_or_ids); + } else { + assertType('array{int}', (array)$id_or_ids); + } + + $ids = (array)$id_or_ids; + assertType('non-empty-list', $ids); + return $ids; +} diff --git a/tests/PHPStan/Analyser/data/bug-9274.php b/tests/PHPStan/Analyser/data/bug-9274.php new file mode 100644 index 0000000000..c01521ff1d --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9274.php @@ -0,0 +1,35 @@ + */ +class A extends SplDoublyLinkedList {} +/** @extends SplQueue */ +class B extends SplQueue {} + +function testSplDoublyLinkedList(): void +{ + $dll = new A(); + $p1 = $dll[0]; + + assertType('Bug9274\Point', $p1); + assertType('int', $p1->x); +} + +function testSplQueue(): void +{ + $queue = new B(); + $p2 = $queue[0]; + + assertType('Bug9274\Point', $p2); + assertType('int', $p2->x); +} diff --git a/tests/PHPStan/Analyser/data/bug-9293.php b/tests/PHPStan/Analyser/data/bug-9293.php new file mode 100644 index 0000000000..a095e58011 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9293.php @@ -0,0 +1,33 @@ += 8.0 + +declare(strict_types=1); + +namespace Bug9293; + +use function PHPStan\Testing\assertType; + +class B +{ + public function int(): int + { + return 0; + } + + public function mixed(): mixed + { + return new self(); + } +} + +/** + * @var null|B $b + */ +$b = null; + +assertType('Bug9293\B|null', $b); + +$b?->mixed()->int() ?? 0; + +assertType('Bug9293\B|null', $b); + +$b?->int() ?? 0; diff --git a/tests/PHPStan/Analyser/data/bug-9307.php b/tests/PHPStan/Analyser/data/bug-9307.php new file mode 100644 index 0000000000..f5b2cb7906 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9307.php @@ -0,0 +1,56 @@ +getIds() as $id) { + if (! array_key_exists($id, $itemCache)) { + $items = $this->getObjects(); + $itemCache[$id] = $items; + } else { + $items = $itemCache[$id]; + } + + // It works when the following line is uncommented. + //$items = $this->getObjects(); + + foreach ($items as $item) { + $objects[$item->id] = $item; + } + } + + assertType('array', $objects); + + $this->acceptObjects($objects); + } + + /** @return array */ + public function getIds(): array + { + return []; + } + + /** @return array */ + public function getObjects(): array + { + return []; + } + + /** @param array $objects */ + public function acceptObjects(array $objects): void + { + + } +} diff --git a/tests/PHPStan/Analyser/data/bug-9341.php b/tests/PHPStan/Analyser/data/bug-9341.php new file mode 100644 index 0000000000..2c1a90f5bd --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9341.php @@ -0,0 +1,32 @@ +', $class); + if (!is_a($class, MyInterface::class, true)) { + return false; + } + assertType('class-string', $class); + $fileObject = new $class(); + assertType('Bug9341\MyInterface&static(Bug9341\MyAbstractBase)', $fileObject); + return $fileObject; + } +} + +abstract class MyAbstractBase { + use MyTrait; +} + +class MyClass extends MyAbstractBase implements MyInterface +{ + +} diff --git a/tests/PHPStan/Analyser/data/bug-9394.php b/tests/PHPStan/Analyser/data/bug-9394.php new file mode 100644 index 0000000000..834a19656c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9394.php @@ -0,0 +1,18 @@ +is_pre_order === false) { + return; + } + + assertType(Order::class . '|null', $order); +}; diff --git a/tests/PHPStan/Analyser/data/bug-9397.php b/tests/PHPStan/Analyser/data/bug-9397.php new file mode 100644 index 0000000000..f197e3b438 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9397.php @@ -0,0 +1,101 @@ + + * If the above type has 63 or more properties, the bug occurs + */ + private static function callable(): array { + return []; + } + + public function callsite(): void { + $result = self::callable(); + foreach ($result as $id => $p) { + assertType(Money::class, $p['foo1']); + assertType(Money::class . '|null', $p['foo2']); + assertType('string', $p['foo3']); + + $baseDeposit = $p['foo2'] ?? Money::zero(); + assertType(Money::class, $p['foo1']); + assertType(Money::class . '|null', $p['foo2']); + assertType('string', $p['foo3']); + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-9404.php b/tests/PHPStan/Analyser/data/bug-9404.php new file mode 100644 index 0000000000..e03c4cd386 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9404.php @@ -0,0 +1,12 @@ + $iterable + * @param (Closure(Tv): T) $function + * + * @return list + */ +function map(array $iterable, Closure $function): array +{ + $result = []; + foreach ($iterable as $value) { + $result[] = $function($value); + } + + return $result; +} + +function (): void { + /** @var list */ + $nonEmptyStrings = []; + + map($nonEmptyStrings, static function (string $variable) { + assertType('non-empty-string', $variable); + return $variable; + }); +}; + +/** + * @template Type + * @param Type $x + * @return Type + */ +function identity($x) { + return $x; +} + +function (): void { + $x = rand() > 5 ? 'a' : 'b'; + assertType('\'a\'|\'b\'', $x); + $y = identity($x); + assertType('\'a\'|\'b\'', $y); +}; + +/** + * @template ParseResultType + * @param callable():ParseResultType $parseFunction + * @return ParseResultType|null + */ +function tryParse(callable $parseFunction) { + try { + return $parseFunction(); + } catch (\Exception $e) { + return null; + } +} + +/** @return array{type: 'typeA'|'typeB'} */ +function parseData(mixed $data): array { + return ['type' => 'typeA']; +} + +function (): void { + $data = tryParse(fn() => parseData('whatever')); + assertType('array{type: \'typeA\'|\'typeB\'}|null', $data); +}; diff --git a/tests/PHPStan/Analyser/data/bug-9573.php b/tests/PHPStan/Analyser/data/bug-9573.php new file mode 100644 index 0000000000..05cb570274 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9573.php @@ -0,0 +1,17 @@ + */ + public array $array; + + /** + * @param positive-int $count + */ + public function __construct(int $count) { + $this->array = range(1, $count); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-9690.php b/tests/PHPStan/Analyser/data/bug-9690.php new file mode 100644 index 0000000000..547285c2cb --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9690.php @@ -0,0 +1,174 @@ +xpath('//data'); + assertType('array', $elements); + } +} + diff --git a/tests/PHPStan/Analyser/data/bug-9721.php b/tests/PHPStan/Analyser/data/bug-9721.php new file mode 100644 index 0000000000..3be9804bb5 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9721.php @@ -0,0 +1,28 @@ += 8.0 + +namespace Bug9721; + +use function PHPStan\Testing\assertType; + +class Example { + public function mergeWith(): self + { + return $this; + } +} + +function () { + $mergedExample = null; + $loop = 2; + + do { + + $example = new Example(); + $mergedExample = $mergedExample?->mergeWith() ?? $example; + + assertType(Example::class, $mergedExample); + + $loop--; + } while ($loop); + +}; diff --git a/tests/PHPStan/Analyser/data/bug-9734.php b/tests/PHPStan/Analyser/data/bug-9734.php new file mode 100644 index 0000000000..ce75fa7197 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9734.php @@ -0,0 +1,47 @@ + $a + * @return void + */ + public function doFoo(array $a): void + { + if (array_is_list($a)) { + assertType('list', $a); + } else { + assertType('array', $a); // could be non-empty-array + } + } + + public function doFoo2(): void + { + $a = []; + if (array_is_list($a)) { + assertType('array{}', $a); + } else { + assertType('*NEVER*', $a); + } + } + + /** + * @param non-empty-array $a + * @return void + */ + public function doFoo3(array $a): void + { + if (array_is_list($a)) { + assertType('non-empty-list', $a); + } else { + assertType('non-empty-array', $a); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-9753.php b/tests/PHPStan/Analyser/data/bug-9753.php new file mode 100644 index 0000000000..0d521cd960 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9753.php @@ -0,0 +1,24 @@ +|null', $items); + if (isset($items)) { + if (count($items) > 2) { + $items = null; + } else { + $items[] = $entry; + } + } + assertType('non-empty-list<1|2|3|4|5>|null', $items); + } + + assertType('non-empty-list<1|2|3|4|5>|null', $items); +}; diff --git a/tests/PHPStan/Analyser/data/bug-9764.php b/tests/PHPStan/Analyser/data/bug-9764.php new file mode 100644 index 0000000000..15807d0b1e --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9764.php @@ -0,0 +1,25 @@ + $a */ + $a = []; + $c = static fn (): array => $a; + assertType('Closure(): array', $c); + + $r = result($c); + assertType('array', $r); +}; diff --git a/tests/PHPStan/Analyser/data/bug-9778.php b/tests/PHPStan/Analyser/data/bug-9778.php new file mode 100644 index 0000000000..240fb5bbc7 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9778.php @@ -0,0 +1,29 @@ + $articles, + 'farben' => null, + 'artikel_ids' => [], + ]; + + // collect article ids + foreach ($result['artikel'] as $article) { + $result['artikel_ids'][] = 1; + } + + assertType('array{artikel: Iterator, farben: null, artikel_ids: list<1>}', $result); + assertType('list<1>', $result['artikel_ids']); + + if ($result['artikel_ids'] !== []) { + $result['farben'] = new stdClass(); + } + + // $result['farben'] might be also null + assertType('stdClass|null', $result['farben']); + if ($result['farben'] instanceof stdClass) { + echo '123'; + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-9860.php b/tests/PHPStan/Analyser/data/bug-9860.php new file mode 100644 index 0000000000..d3702fed7a --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9860.php @@ -0,0 +1,37 @@ + new ANode(), + $b instanceof B => new BNode(), + default => new CNode(), + }; + } + + public function test(): void { + assertType('Bug9860\\ANode', $this->b(new A())); + assertType('Bug9860\\BNode', $this->b(new B())); + assertType('Bug9860\\ANode|Bug9860\\BNode', $this->b($this->a())); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-9867.php b/tests/PHPStan/Analyser/data/bug-9867.php new file mode 100644 index 0000000000..7c677aa8d6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9867.php @@ -0,0 +1,77 @@ + */ +class MyMinHeap extends \SplMinHeap +{ + public function test(): void + { + assertType('DateTime', parent::current()); + assertType('DateTime', parent::extract()); + assertType('DateTime', parent::top()); + } + + public function insert(mixed $value): bool + { + assertType('DateTime', $value); + + return parent::insert($value); + } + + protected function compare(mixed $value1, mixed $value2) + { + assertType('DateTime', $value1); + assertType('DateTime', $value2); + + return parent::compare($value1, $value2); + } +} + +/** @extends \SplMaxHeap<\DateTime> */ +class MyMaxHeap extends \SplMaxHeap +{ + public function test(): void + { + assertType('DateTime', parent::current()); + assertType('DateTime', parent::extract()); + assertType('DateTime', parent::top()); + } + + public function insert(mixed $value): bool + { + assertType('DateTime', $value); + + return parent::insert($value); + } + + protected function compare(mixed $value1, mixed $value2) + { + assertType('DateTime', $value1); + assertType('DateTime', $value2); + + return parent::compare($value1, $value2); + } +} + +/** @extends \SplHeap<\DateTime> */ +abstract class MyHeap extends \SplHeap +{ + public function test(): void + { + assertType('DateTime', parent::current()); + assertType('DateTime', parent::extract()); + assertType('DateTime', parent::top()); + } + + public function insert(mixed $value): bool + { + assertType('DateTime', $value); + + return parent::insert($value); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-9881.php b/tests/PHPStan/Analyser/data/bug-9881.php new file mode 100644 index 0000000000..129ca9c87f --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9881.php @@ -0,0 +1,27 @@ += 8.1 + +namespace Bug9881; + +use BackedEnum; +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + /** + * @template B of BackedEnum + * @param B[] $enums + * @return value-of[] + */ + public static function arrayEnumToStrings(array $enums): array + { + return array_map(static fn (BackedEnum $code): string|int => $code->value, $enums); + } +} + +enum Test: string { + case DA = 'da'; +} + +function (Test ...$da): void { + assertType('array<\'da\'>', HelloWorld::arrayEnumToStrings($da)); +}; diff --git a/tests/PHPStan/Analyser/data/bug-9939.php b/tests/PHPStan/Analyser/data/bug-9939.php new file mode 100644 index 0000000000..16e977e202 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9939.php @@ -0,0 +1,63 @@ += 8.1 + +namespace Bug9939; + +use function PHPStan\Testing\assertType; + +enum Combinator +{ + case NEXT_SIBLING; + case CHILD; + case FOLLOWING_SIBLING; + + public function getText(): string + { + return match ($this) { + self::NEXT_SIBLING => '+', + self::CHILD => '>', + self::FOLLOWING_SIBLING => '~', + }; + } +} + +/** + * @template T of string|\Stringable|array|Combinator|null + */ +class CssValue +{ + /** + * @param T $value + */ + public function __construct(private readonly mixed $value) + { + } + + /** + * @return T + */ + public function getValue(): mixed + { + return $this->value; + } + + public function __toString(): string + { + assertType('T of array|Bug9939\Combinator|string|Stringable|null (class Bug9939\CssValue, argument)', $this->value); + + if ($this->value instanceof Combinator) { + assertType('T of Bug9939\Combinator (class Bug9939\CssValue, argument)', $this->value); + return $this->value->getText(); + } + + assertType('T of array|string|Stringable|null (class Bug9939\CssValue, argument)', $this->value); + + if (\is_array($this->value)) { + assertType('T of array (class Bug9939\CssValue, argument)', $this->value); + return implode($this->value); + } + + assertType('T of string|Stringable|null (class Bug9939\CssValue, argument)', $this->value); + + return (string) $this->value; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-9963.php b/tests/PHPStan/Analyser/data/bug-9963.php new file mode 100644 index 0000000000..e5d8444afd --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9963.php @@ -0,0 +1,23 @@ +|Bug9963\HelloWorld|false', $h->find($something)); +}; diff --git a/tests/PHPStan/Analyser/data/bug-9985.php b/tests/PHPStan/Analyser/data/bug-9985.php new file mode 100644 index 0000000000..edbfebffc5 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9985.php @@ -0,0 +1,25 @@ += 1) { + $warnings['a'] = true; + } + + if (rand(0, 100) >= 2) { + $warnings['b'] = true; + } elseif (rand(0, 100) >= 3) { + $warnings['c'] = true; + } + + assertType('array{}|array{a?: true, b: true}|array{a?: true, c?: true}', $warnings); + + if (!empty($warnings)) { + assertType('array{a?: true, b: true}|(array{a?: true, c?: true}&non-empty-array)', $warnings); + } +}; diff --git a/tests/PHPStan/Analyser/data/bug-9994.php b/tests/PHPStan/Analyser/data/bug-9994.php new file mode 100644 index 0000000000..f87e4efdde --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9994.php @@ -0,0 +1,16 @@ += 8.1 + +namespace Bug9994; + +function (): void { + + $arr = [ + 1, + 2, + 3, + null, + ]; + + + var_dump(array_filter($arr, !is_null(...))); +}; diff --git a/tests/PHPStan/Analyser/data/bug-9995.php b/tests/PHPStan/Analyser/data/bug-9995.php new file mode 100644 index 0000000000..c4fe4d6ada --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9995.php @@ -0,0 +1,15 @@ +format('c'); +} diff --git a/tests/PHPStan/Analyser/data/bug-nullsafe-prop-static-access.php b/tests/PHPStan/Analyser/data/bug-nullsafe-prop-static-access.php new file mode 100644 index 0000000000..65ba442a78 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-nullsafe-prop-static-access.php @@ -0,0 +1,28 @@ += 8.0 + +namespace BugNullsafePropStaticAccess; + +class A +{ + public function __construct(public readonly B $b) + {} +} + +class B +{ + public static int $value = 0; + + public static function get(): string + { + return 'B'; + } +} + +function foo(?A $a): void +{ + \PHPStan\Testing\assertType('string|null', $a?->b::get()); + \PHPStan\Testing\assertType('string|null', $a?->b->get()); + + \PHPStan\Testing\assertType('int|null', $a?->b::$value); + \PHPStan\Testing\assertType('int|null', $a?->b->value); +} diff --git a/tests/PHPStan/Analyser/data/call-user-func-php7.php b/tests/PHPStan/Analyser/data/call-user-func-php7.php new file mode 100644 index 0000000000..9f88a8d20c --- /dev/null +++ b/tests/PHPStan/Analyser/data/call-user-func-php7.php @@ -0,0 +1,26 @@ +', call_user_func('CallUserFuncPhp7\generic', $params)); + } +} diff --git a/tests/PHPStan/Analyser/data/call-user-func-php8.php b/tests/PHPStan/Analyser/data/call-user-func-php8.php new file mode 100644 index 0000000000..32a2c81266 --- /dev/null +++ b/tests/PHPStan/Analyser/data/call-user-func-php8.php @@ -0,0 +1,55 @@ +', call_user_func('CallUserFuncPhp8\generic', $params)); + } + + function doNamed() { + assertType('1', call_user_func('CallUserFuncPhp8\generic', t: 1)); + assertType('array{1, 2, 3}', call_user_func('CallUserFuncPhp8\generic', t: [1, 2, 3])); + + assertType('array{1, 2, 3}', call_user_func('CallUserFuncPhp8\generic3', t: [1, 2, 3])); + assertType('\'\'', call_user_func('CallUserFuncPhp8\generic3', b: 150)); + assertType('\'\'', call_user_func('CallUserFuncPhp8\generic3', c: 'lala')); + assertType('\'\'', call_user_func(c: 'lala', callback: 'CallUserFuncPhp8\generic3')); + + assertType('int', call_user_func('CallUserFuncPhp8\fun3', a: [1, 2, 3])); + assertType('int', call_user_func('CallUserFuncPhp8\fun3', b: [1, 2, 3])); + assertType('int', call_user_func('CallUserFuncPhp8\fun3', c: [1, 2, 3])); + assertType('int', call_user_func('CallUserFuncPhp8\fun3', a: [1, 2, 3], c: 'c')); + } +} diff --git a/tests/PHPStan/Analyser/data/call-user-func.php b/tests/PHPStan/Analyser/data/call-user-func.php new file mode 100644 index 0000000000..b54952ce4c --- /dev/null +++ b/tests/PHPStan/Analyser/data/call-user-func.php @@ -0,0 +1,63 @@ +', call_user_func('CallUserFunc\generic', $strings)); + + assertType('int', call_user_func('CallUserFunc\fun')); + assertType('int', call_user_func('CallUserFunc\fun3', 1 ,2 ,3)); + assertType('string', call_user_func(['CallUserFunc\c', 'm'])); + } +} diff --git a/tests/PHPStan/Analyser/data/callable-in-union.php b/tests/PHPStan/Analyser/data/callable-in-union.php index b72c0725cc..24db62b428 100644 --- a/tests/PHPStan/Analyser/data/callable-in-union.php +++ b/tests/PHPStan/Analyser/data/callable-in-union.php @@ -1,4 +1,4 @@ -= 7.4 namespace CallableInUnion; @@ -15,3 +15,18 @@ function acceptArrayOrCallable($_) assertType('array', $parameter); return $parameter; }); + +/** + * @param (callable(string): void)|callable(int): void $a + * @return void + */ +function acceptCallableOrCallableLikeArray($a): void +{ + +} + +acceptCallableOrCallableLikeArray(function ($p) { + assertType('int|string', $p); +}); + +acceptCallableOrCallableLikeArray(fn ($p) => assertType('int|string', $p)); diff --git a/tests/PHPStan/Analyser/data/callable-object.php b/tests/PHPStan/Analyser/data/callable-object.php new file mode 100644 index 0000000000..f9f3dee69c --- /dev/null +++ b/tests/PHPStan/Analyser/data/callable-object.php @@ -0,0 +1,38 @@ +|numeric-string|true', $mixed); + } else { + assertType('mixed~int<0, max>|numeric-string|true', $mixed); + } + assertType('mixed', $mixed); + + if (ctype_digit((int) $mixed)) { + assertType('mixed', $mixed); // could be *NEVER* + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (ctype_digit((string) $int)) { + assertType('int', $int); + } else { + assertType('int', $int); + } + assertType('int', $int); + + if (ctype_digit((int) $int)) { + assertType('int', $int); // could be *NEVER* + } else { + assertType('int', $int); + } + assertType('int', $int); + + if (ctype_digit((string) $string)) { + assertType('numeric-string', $string); + } else { + assertType('string', $string); + } + assertType('string', $string); + + if (ctype_digit((int) $string)) { + assertType('string', $string); // could be *NEVER* + } else { + assertType('string', $string); + } + assertType('string', $string); + + if (ctype_digit((string) $numericString)) { + assertType('numeric-string', $numericString); + } else { + assertType('*NEVER*', $numericString); + } + assertType('numeric-string', $numericString); + + if (ctype_digit((string) $bool)) { + assertType('true', $bool); + } else { + assertType('false', $bool); + } + assertType('bool', $bool); + } + +} diff --git a/tests/PHPStan/Analyser/data/class-constant-native-type.php b/tests/PHPStan/Analyser/data/class-constant-native-type.php new file mode 100644 index 0000000000..f8db2259e0 --- /dev/null +++ b/tests/PHPStan/Analyser/data/class-constant-native-type.php @@ -0,0 +1,63 @@ += 8.3 + +namespace ClassConstantNativeType; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public const int FOO = 1; + + public function doFoo(): void + { + assertType('1', self::FOO); + assertType('int', static::FOO); + assertType('int', $this::FOO); + } + +} + +final class FinalFoo +{ + + public const int FOO = 1; + + public function doFoo(): void + { + assertType('1', self::FOO); + assertType('1', static::FOO); + assertType('1', $this::FOO); + } + +} + +class FooWithPhpDoc +{ + + /** @var positive-int */ + public const int FOO = 1; + + public function doFoo(): void + { + assertType('1', self::FOO); + assertType('int<1, max>', static::FOO); + assertType('int<1, max>', $this::FOO); + } + +} + +final class FinalFooWithPhpDoc +{ + + /** @var positive-int */ + public const int FOO = 1; + + public function doFoo(): void + { + assertType('1', self::FOO); + assertType('1', static::FOO); + assertType('1', $this::FOO); + } + +} diff --git a/tests/PHPStan/Analyser/data/class-implements.php b/tests/PHPStan/Analyser/data/class-implements.php new file mode 100644 index 0000000000..acd6a616ae --- /dev/null +++ b/tests/PHPStan/Analyser/data/class-implements.php @@ -0,0 +1,128 @@ +', class_implements($object)); + assertType('(array|false)', class_implements($objectOrClassString)); + assertType('array|false', class_implements($objectOrString)); + assertType('(array|false)', class_implements($classString)); + assertType('array|false', class_implements($string)); + assertType('false', class_implements('thisIsNotAClass')); + + assertType('array', class_implements($object, true)); + assertType('(array|false)', class_implements($objectOrClassString, true)); + assertType('array|false', class_implements($objectOrString, true)); + assertType('(array|false)', class_implements($classString, true)); + assertType('array|false', class_implements($string, true)); + assertType('false', class_implements('thisIsNotAClass', true)); + + assertType('array', class_implements($object, false)); + assertType('array|false', class_implements($objectOrClassString, false)); + assertType('array|false', class_implements($objectOrString, false)); + assertType('array|false', class_implements($classString, false)); + assertType('array|false', class_implements($string, false)); + assertType('false', class_implements('thisIsNotAClass', false)); + + assertType('array', class_implements($object, $bool)); + assertType('array|false', class_implements($objectOrClassString, $bool)); + assertType('array|false', class_implements($objectOrString, $bool)); + assertType('array|false', class_implements($classString, $bool)); + assertType('array|false', class_implements($string, $bool)); + assertType('false', class_implements('thisIsNotAClass', $bool)); + + assertType('array', class_implements($object, $mixed)); + assertType('array|false', class_implements($objectOrClassString, $mixed)); + assertType('array|false', class_implements($objectOrString, $mixed)); + assertType('array|false', class_implements($classString, $mixed)); + assertType('array|false', class_implements($string, $mixed)); + assertType('false', class_implements('thisIsNotAClass', $mixed)); + + assertType('array', class_uses($object)); + assertType('(array|false)', class_uses($objectOrClassString)); + assertType('array|false', class_uses($objectOrString)); + assertType('(array|false)', class_uses($classString)); + assertType('array|false', class_uses($string)); + assertType('false', class_uses('thisIsNotAClass')); + + assertType('array', class_uses($object, true)); + assertType('(array|false)', class_uses($objectOrClassString, true)); + assertType('array|false', class_uses($objectOrString, true)); + assertType('(array|false)', class_uses($classString, true)); + assertType('array|false', class_uses($string, true)); + assertType('false', class_uses('thisIsNotAClass', true)); + + assertType('array', class_uses($object, false)); + assertType('array|false', class_uses($objectOrClassString, false)); + assertType('array|false', class_uses($objectOrString, false)); + assertType('array|false', class_uses($classString, false)); + assertType('array|false', class_uses($string, false)); + assertType('false', class_uses('thisIsNotAClass', false)); + + assertType('array', class_uses($object, $bool)); + assertType('array|false', class_uses($objectOrClassString, $bool)); + assertType('array|false', class_uses($objectOrString, $bool)); + assertType('array|false', class_uses($classString, $bool)); + assertType('array|false', class_uses($string, $bool)); + assertType('false', class_uses('thisIsNotAClass', $bool)); + + assertType('array', class_uses($object, $mixed)); + assertType('array|false', class_uses($objectOrClassString, $mixed)); + assertType('array|false', class_uses($objectOrString, $mixed)); + assertType('array|false', class_uses($classString, $mixed)); + assertType('array|false', class_uses($string, $mixed)); + assertType('false', class_uses('thisIsNotAClass', $mixed)); + + assertType('array', class_parents($object)); + assertType('(array|false)', class_parents($objectOrClassString)); + assertType('array|false', class_parents($objectOrString)); + assertType('(array|false)', class_parents($classString)); + assertType('array|false', class_parents($string)); + assertType('false', class_parents('thisIsNotAClass')); + + assertType('array', class_parents($object, true)); + assertType('(array|false)', class_parents($objectOrClassString, true)); + assertType('array|false', class_parents($objectOrString, true)); + assertType('(array|false)', class_parents($classString, true)); + assertType('array|false', class_parents($string, true)); + assertType('false', class_parents('thisIsNotAClass', true)); + + assertType('array', class_parents($object, false)); + assertType('array|false', class_parents($objectOrClassString, false)); + assertType('array|false', class_parents($objectOrString, false)); + assertType('array|false', class_parents($classString, false)); + assertType('array|false', class_parents($string, false)); + assertType('false', class_parents('thisIsNotAClass', false)); + + assertType('array', class_parents($object, $bool)); + assertType('array|false', class_parents($objectOrClassString, $bool)); + assertType('array|false', class_parents($objectOrString, $bool)); + assertType('array|false', class_parents($classString, $bool)); + assertType('array|false', class_parents($string, $bool)); + assertType('false', class_parents('thisIsNotAClass', $bool)); + + assertType('array', class_parents($object, $mixed)); + assertType('array|false', class_parents($objectOrClassString, $mixed)); + assertType('array|false', class_parents($objectOrString, $mixed)); + assertType('array|false', class_parents($classString, $mixed)); + assertType('array|false', class_parents($string, $mixed)); + assertType('false', class_parents('thisIsNotAClass', $mixed)); + } + +} diff --git a/tests/PHPStan/Analyser/data/classPhpDocs-phpstanPropertyPrefix.php b/tests/PHPStan/Analyser/data/classPhpDocs-phpstanPropertyPrefix.php index c528b5cffe..4e2429e6dc 100644 --- a/tests/PHPStan/Analyser/data/classPhpDocs-phpstanPropertyPrefix.php +++ b/tests/PHPStan/Analyser/data/classPhpDocs-phpstanPropertyPrefix.php @@ -25,6 +25,6 @@ public function doFoo() assertType('string', $this->base); assertType('int', $this->foo); assertType('int', $this->bar); - assertType('int', $this->baz); + assertType('*NEVER*', $this->baz); } } diff --git a/tests/PHPStan/Analyser/data/classPhpDocs.php b/tests/PHPStan/Analyser/data/classPhpDocs.php index 0d447a3d49..f0024022ce 100644 --- a/tests/PHPStan/Analyser/data/classPhpDocs.php +++ b/tests/PHPStan/Analyser/data/classPhpDocs.php @@ -9,6 +9,7 @@ * @method array arrayOfStrings() * @psalm-method array arrayOfStrings() * @phpstan-method array arrayOfInts() + * @phan-method array arrayOfStrings() * @method array arrayOfInts() * @method mixed overrodeMethod() * @method static mixed overrodeStaticMethod() diff --git a/tests/PHPStan/Analyser/data/cli-globals.php b/tests/PHPStan/Analyser/data/cli-globals.php index 110d500101..9dc930c395 100644 --- a/tests/PHPStan/Analyser/data/cli-globals.php +++ b/tests/PHPStan/Analyser/data/cli-globals.php @@ -5,7 +5,7 @@ use function PHPStan\Testing\assertType; assertType('int<1, max>', $argc); -assertType('non-empty-array', $argv); +assertType('non-empty-list', $argv); function f() { assertType('*ERROR*', $argc); diff --git a/tests/PHPStan/Analyser/data/closure-retain-expression-types.php b/tests/PHPStan/Analyser/data/closure-retain-expression-types.php new file mode 100644 index 0000000000..0db3aa730e --- /dev/null +++ b/tests/PHPStan/Analyser/data/closure-retain-expression-types.php @@ -0,0 +1,23 @@ +call($newThis, new class {}); assertType('true', $returnType); + +$staticallyBoundClosureCaseInsensitive = \closure::bind($closure, $newThis); +assertType('Closure(object): true', $staticallyBoundClosureCaseInsensitive); diff --git a/tests/PHPStan/Analyser/data/closure-return-type.php b/tests/PHPStan/Analyser/data/closure-return-type.php index f71b056a55..386fec990c 100644 --- a/tests/PHPStan/Analyser/data/closure-return-type.php +++ b/tests/PHPStan/Analyser/data/closure-return-type.php @@ -111,12 +111,12 @@ public function doBaz(): void $f = function() { $this->returnNever(); }; - assertType('*NEVER*', $f()); + assertType('never', $f()); $f = function(): void { $this->returnNever(); }; - assertType('*NEVER*', $f()); + assertType('never', $f()); $f = function() { if (rand(0, 1)) { @@ -134,7 +134,7 @@ public function doBaz(): void $this->returnNever(); }; - assertType('*NEVER*', $f([])); + assertType('never', $f([])); $f = function(array $a) { foreach ($a as $v) { @@ -148,12 +148,12 @@ public function doBaz(): void $this->returnNever(); } }; - assertType('*NEVER*', $f()); + assertType('never', $f()); $f = function (): \stdClass { throw new \Exception(); }; - assertType('*NEVER*', $f()); + assertType('never', $f()); } } diff --git a/tests/PHPStan/Analyser/data/closure-types.php b/tests/PHPStan/Analyser/data/closure-types.php index 72adfe762a..64a168e99a 100644 --- a/tests/PHPStan/Analyser/data/closure-types.php +++ b/tests/PHPStan/Analyser/data/closure-types.php @@ -2,6 +2,8 @@ namespace ClosureTypes; +use DateTimeInterface; +use stdClass; use function PHPStan\Testing\assertType; class Foo @@ -47,4 +49,22 @@ public function doBaz(): void }); } + public function closureNewThisIntersection(stdClass $foo) { + if (!$foo instanceof DateTimeInterface) { + return; + } + + (function () { + assertType('DateTimeInterface&stdClass', $this); + })->call($foo); + } + + public function arrowFunctionNewThisIntersection(stdClass $foo) { + if (!$foo instanceof DateTimeInterface) { + return; + } + + (fn () => assertType('DateTimeInterface&stdClass', $this))->call($foo); + } + } diff --git a/tests/PHPStan/Analyser/data/collected-data.php b/tests/PHPStan/Analyser/data/collected-data.php index eca89df65c..10c6d5fd8c 100644 --- a/tests/PHPStan/Analyser/data/collected-data.php +++ b/tests/PHPStan/Analyser/data/collected-data.php @@ -30,7 +30,7 @@ class Foo public function doFoo(CollectedDataNode $node): void { - assertType('array>', $node->get(TestCollector::class)); + assertType('array>', $node->get(TestCollector::class)); } } diff --git a/tests/PHPStan/Analyser/data/composer-treatPhpDocTypesAsCertainBug.php b/tests/PHPStan/Analyser/data/composer-treatPhpDocTypesAsCertainBug.php new file mode 100644 index 0000000000..4754ea1db4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/composer-treatPhpDocTypesAsCertainBug.php @@ -0,0 +1,42 @@ + $files + */ + function setupDummyRepo(array $files): void + { + assertType('array', $files); + assertNativeType('array', $files); + foreach ($files as $path => $content) { + assertType('non-empty-array', $files); + assertNativeType('non-empty-array', $files); + assertType('string', $path); + assertNativeType('(int|string)', $path); + assertType('string|null', $content); + assertNativeType('mixed', $content); + assertType('string|null', $files[$path]); + assertNativeType('mixed', $files[$path]); + if ($files[$path] === null) { + assertType('null', $files[$path]); + assertNativeType('null', $files[$path]); + $files[$path] = 'content'; + assertType('\'content\'', $files[$path]); + assertNativeType('\'content\'', $files[$path]); + } + + assertType('string', $files[$path]); + assertNativeType('mixed~null', $files[$path]); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/conditional-expression-infinite-loop.php b/tests/PHPStan/Analyser/data/conditional-expression-infinite-loop.php new file mode 100644 index 0000000000..9dda0d65b1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/conditional-expression-infinite-loop.php @@ -0,0 +1,14 @@ + $foo = 1, + $isFoo || $isBar => $foo = 2, + default => $foo = null, + }; + } +} diff --git a/tests/PHPStan/Analyser/data/conditional-types.php b/tests/PHPStan/Analyser/data/conditional-types.php index 5ed3c5531c..0cdc741503 100644 --- a/tests/PHPStan/Analyser/data/conditional-types.php +++ b/tests/PHPStan/Analyser/data/conditional-types.php @@ -27,13 +27,13 @@ abstract public function arrayKeys(array $array); */ public function testArrayKeys(array $array, array $nonEmptyArray, array $intArray, array $nonEmptyIntArray, array $emptyArray): void { - assertType('array', $this->arrayKeys($array)); - assertType('array', $this->arrayKeys($intArray)); + assertType('list<(int|string)>', $this->arrayKeys($array)); + assertType('list', $this->arrayKeys($intArray)); - assertType('non-empty-array', $this->arrayKeys($nonEmptyArray)); - assertType('non-empty-array', $this->arrayKeys($nonEmptyIntArray)); + assertType('non-empty-list<(int|string)>', $this->arrayKeys($nonEmptyArray)); + assertType('non-empty-list', $this->arrayKeys($nonEmptyIntArray)); - assertType('array', $this->arrayKeys($emptyArray)); + assertType('list<*NEVER*>', $this->arrayKeys($emptyArray)); } /** @@ -151,9 +151,9 @@ abstract public function maybeNever(int $option): void; public function testMaybeNever(): void { - assertType('void', $this->maybeNever(0)); - assertType('*NEVER*', $this->maybeNever(1)); - assertType('void', $this->maybeNever(2)); + assertType('null', $this->maybeNever(0)); + assertType('never', $this->maybeNever(1)); + assertType('null', $this->maybeNever(2)); } /** diff --git a/tests/PHPStan/Analyser/data/conditional-vars.php b/tests/PHPStan/Analyser/data/conditional-vars.php new file mode 100644 index 0000000000..6d86c88014 --- /dev/null +++ b/tests/PHPStan/Analyser/data/conditional-vars.php @@ -0,0 +1,36 @@ + $innerHits */ + public function conditionalVarInTernary(array $innerHits): void + { + if (array_key_exists('nearest_premise', $innerHits) || array_key_exists('matching_premises', $innerHits)) { + assertType('array', $innerHits); + $x = array_key_exists('nearest_premise', $innerHits) + ? assertType("array&hasOffset('nearest_premise')", $innerHits) + : assertType('array', $innerHits); + + assertType('array', $innerHits); + } + } + + /** @param array $innerHits */ + public function conditionalVarInIf(array $innerHits): void + { + if (array_key_exists('nearest_premise', $innerHits) || array_key_exists('matching_premises', $innerHits)) { + assertType('array', $innerHits); + if (array_key_exists('nearest_premise', $innerHits)) { + assertType("array&hasOffset('nearest_premise')", $innerHits); + } else { + assertType('array', $innerHits); + } + + assertType('array', $innerHits); + } + } +} diff --git a/tests/PHPStan/Analyser/data/constant-array-optional-set.php b/tests/PHPStan/Analyser/data/constant-array-optional-set.php index 40cd005009..6faaa574c1 100644 --- a/tests/PHPStan/Analyser/data/constant-array-optional-set.php +++ b/tests/PHPStan/Analyser/data/constant-array-optional-set.php @@ -60,13 +60,13 @@ class Bar */ public function doFoo($nextAutoIndexes) { - assertType('non-empty-array|int', $nextAutoIndexes); + assertType('non-empty-list|int', $nextAutoIndexes); if (is_int($nextAutoIndexes)) { assertType('int', $nextAutoIndexes); } else { - assertType('non-empty-array', $nextAutoIndexes); + assertType('non-empty-list', $nextAutoIndexes); } - assertType('non-empty-array|int', $nextAutoIndexes); + assertType('non-empty-list|int', $nextAutoIndexes); } /** @@ -75,14 +75,14 @@ public function doFoo($nextAutoIndexes) */ public function doBar($nextAutoIndexes) { - assertType('non-empty-array|int', $nextAutoIndexes); + assertType('non-empty-list|int', $nextAutoIndexes); if (is_int($nextAutoIndexes)) { $nextAutoIndexes = [$nextAutoIndexes]; assertType('array{int}', $nextAutoIndexes); } else { - assertType('non-empty-array', $nextAutoIndexes); + assertType('non-empty-list', $nextAutoIndexes); } - assertType('non-empty-array', $nextAutoIndexes); + assertType('non-empty-list', $nextAutoIndexes); } } diff --git a/tests/PHPStan/Analyser/data/bug-6439.php b/tests/PHPStan/Analyser/data/constant-string-unions.php similarity index 62% rename from tests/PHPStan/Analyser/data/bug-6439.php rename to tests/PHPStan/Analyser/data/constant-string-unions.php index ed72a284d1..06881808e8 100644 --- a/tests/PHPStan/Analyser/data/bug-6439.php +++ b/tests/PHPStan/Analyser/data/constant-string-unions.php @@ -1,10 +1,10 @@ = 8.1 + +namespace Constant; + +use function PHPStan\Testing\assertType; + +define('FOO', 'foo'); +const BAR = 'bar'; + +class Baz +{ + const BAZ = 'baz'; +} + +enum Suit +{ + case Hearts; +} + +function doFoo(string $constantName): void +{ + assertType('mixed', constant($constantName)); +} + +assertType("'foo'", FOO); +assertType("'foo'", constant('FOO')); +assertType("*ERROR*", constant('\Constant\FOO')); + +assertType("'bar'", BAR); +assertType("*ERROR*", constant('BAR')); +assertType("'bar'", constant('\Constant\BAR')); + +assertType("'bar'|'foo'", constant(rand(0, 1) ? 'FOO' : '\Constant\BAR')); + +assertType("'baz'", constant('\Constant\Baz::BAZ')); + +assertType('Constant\Suit::Hearts', Suit::Hearts); +assertType('Constant\Suit::Hearts', constant('\Constant\Suit::Hearts')); + +assertType('*ERROR*', constant('UNDEFINED')); diff --git a/tests/PHPStan/Analyser/data/countable.php b/tests/PHPStan/Analyser/data/countable.php index 2d18e25faa..740430a4c5 100644 --- a/tests/PHPStan/Analyser/data/countable.php +++ b/tests/PHPStan/Analyser/data/countable.php @@ -15,3 +15,9 @@ static public function doFoo() { } } +class NonCountable {} + +function doFoo() { + assertType('int<0, max>', count(new Foo())); + assertType('*ERROR*', count(new NonCountable())); +} diff --git a/tests/PHPStan/Analyser/data/curl_getinfo.php b/tests/PHPStan/Analyser/data/curl_getinfo.php index 80275cb925..453b835778 100644 --- a/tests/PHPStan/Analyser/data/curl_getinfo.php +++ b/tests/PHPStan/Analyser/data/curl_getinfo.php @@ -15,7 +15,7 @@ public function bar() assertType('mixed', CuRl_GeTiNfO()); assertType('false', curl_getinfo($handle, 'Invalid Argument')); assertType('(array{url: string, content_type: string|null, http_code: int, header_size: int, request_size: int, filetime: int, ssl_verify_result: int, redirect_count: int, total_time: float, namelookup_time: float, connect_time: float, pretransfer_time: float, size_upload: float, size_download: float, speed_download: float, speed_upload: float, download_content_length: float, upload_content_length: float, starttransfer_time: float, redirect_time: float, redirect_url: string, primary_ip: string, certinfo: array>, primary_port: int, local_ip: string, local_port: int, http_version: int, protocol: int, ssl_verifyresult: int, scheme: string}|false)', curl_getinfo($handle, PHP_INT_MAX)); - assertType('(array{url: string, content_type: string|null, http_code: int, header_size: int, request_size: int, filetime: int, ssl_verify_result: int, redirect_count: int, total_time: float, namelookup_time: float, connect_time: float, pretransfer_time: float, size_upload: float, size_download: float, speed_download: float, speed_upload: float, download_content_length: float, upload_content_length: float, starttransfer_time: float, redirect_time: float, redirect_url: string, primary_ip: string, certinfo: array>, primary_port: int, local_ip: string, local_port: int, http_version: int, protocol: int, ssl_verifyresult: int, scheme: string}|false)', curl_getinfo($handle, PHP_EOL)); + assertType('false', curl_getinfo($handle, PHP_EOL)); assertType('(array{url: string, content_type: string|null, http_code: int, header_size: int, request_size: int, filetime: int, ssl_verify_result: int, redirect_count: int, total_time: float, namelookup_time: float, connect_time: float, pretransfer_time: float, size_upload: float, size_download: float, speed_download: float, speed_upload: float, download_content_length: float, upload_content_length: float, starttransfer_time: float, redirect_time: float, redirect_url: string, primary_ip: string, certinfo: array>, primary_port: int, local_ip: string, local_port: int, http_version: int, protocol: int, ssl_verifyresult: int, scheme: string}|false)', curl_getinfo($handle)); assertType('(array{url: string, content_type: string|null, http_code: int, header_size: int, request_size: int, filetime: int, ssl_verify_result: int, redirect_count: int, total_time: float, namelookup_time: float, connect_time: float, pretransfer_time: float, size_upload: float, size_download: float, speed_download: float, speed_upload: float, download_content_length: float, upload_content_length: float, starttransfer_time: float, redirect_time: float, redirect_url: string, primary_ip: string, certinfo: array>, primary_port: int, local_ip: string, local_port: int, http_version: int, protocol: int, ssl_verifyresult: int, scheme: string}|false)', curl_getinfo($handle, null)); assertType('string', curl_getinfo($handle, CURLINFO_EFFECTIVE_URL)); diff --git a/tests/PHPStan/Analyser/data/custom-function-in-signature-map.php b/tests/PHPStan/Analyser/data/custom-function-in-signature-map.php index f79aea7dd3..420d5e089c 100644 --- a/tests/PHPStan/Analyser/data/custom-function-in-signature-map.php +++ b/tests/PHPStan/Analyser/data/custom-function-in-signature-map.php @@ -2,5 +2,5 @@ function bcompiler_write_file(): void { - + echo 'test'; } diff --git a/tests/PHPStan/Analyser/data/date-format.php b/tests/PHPStan/Analyser/data/date-format.php index 0c4c9557cd..e8a6878521 100644 --- a/tests/PHPStan/Analyser/data/date-format.php +++ b/tests/PHPStan/Analyser/data/date-format.php @@ -43,3 +43,7 @@ function (\DateTimeImmutable $dt, string $s): void { assertType('numeric-string', $dt->format('Y')); assertType('numeric-string', $dt->format('Ghi')); }; + +function (?\DateTimeImmutable $d): void { + assertType('DateTimeImmutable|null', $d->modify('+1 day')); +}; diff --git a/tests/PHPStan/Analyser/data/dependent-expression-certainty.php b/tests/PHPStan/Analyser/data/dependent-expression-certainty.php new file mode 100644 index 0000000000..302d82b609 --- /dev/null +++ b/tests/PHPStan/Analyser/data/dependent-expression-certainty.php @@ -0,0 +1,233 @@ +', $itemsCounter); } - assertType('Generator&iterable', $associationData); + assertType('Generator', $associationData); assertType('int<0, max>', $itemsCounter); } diff --git a/tests/PHPStan/Analyser/data/discussion-10285-php8.php b/tests/PHPStan/Analyser/data/discussion-10285-php8.php new file mode 100644 index 0000000000..67bf3afe8d --- /dev/null +++ b/tests/PHPStan/Analyser/data/discussion-10285-php8.php @@ -0,0 +1,21 @@ +', $read); + assertType('array', $write); + assertType('null', $except); + } +} diff --git a/tests/PHPStan/Analyser/data/discussion-10285.php b/tests/PHPStan/Analyser/data/discussion-10285.php new file mode 100644 index 0000000000..704cd94448 --- /dev/null +++ b/tests/PHPStan/Analyser/data/discussion-10285.php @@ -0,0 +1,21 @@ +', $read); + assertType('array', $write); + assertType('null', $except); + } +} diff --git a/tests/PHPStan/Analyser/data/discussion-8447.php b/tests/PHPStan/Analyser/data/discussion-8447.php new file mode 100644 index 0000000000..21bd7d4dda --- /dev/null +++ b/tests/PHPStan/Analyser/data/discussion-8447.php @@ -0,0 +1,45 @@ + + * @param TLead $lead + * @return TQuote + */ + public function store(Lead $lead): Quote + { + assertType('TQuote of Discussion8447\Quote (class Discussion8447\Controller, argument)', $lead->quoteRepository()->create()); + return $lead->quoteRepository()->create(); + } +} diff --git a/tests/PHPStan/Analyser/data/discussion-9053.php b/tests/PHPStan/Analyser/data/discussion-9053.php new file mode 100644 index 0000000000..e02627602c --- /dev/null +++ b/tests/PHPStan/Analyser/data/discussion-9053.php @@ -0,0 +1,110 @@ += 8.0 + +namespace Discussion9053; + +use function PHPStan\Testing\assertType; + +/** + * @template TChild of ChildInterface + */ +interface ModelInterface { + /** + * @return TChild[] + */ + public function getChildren(): array; +} + +/** + * @implements ModelInterface + */ +class Model implements ModelInterface +{ + /** + * @var Child[] + */ + public array $children; + + public function getChildren(): array + { + return $this->children; + } +} + +/** + * @template T of ModelInterface + */ +interface ChildInterface { + /** + * @return T + */ + public function getModel(): ModelInterface; +} + + +/** + * @implements ChildInterface + */ +class Child implements ChildInterface +{ + public function __construct(private Model $model) + { + } + + public function getModel(): Model + { + return $this->model; + } +} + +/** + * @template T of ModelInterface + */ +class Helper +{ + /** + * @param T $model + */ + public function __construct(private ModelInterface $model) + {} + + /** + * @return template-type + */ + public function getFirstChildren(): ChildInterface + { + $firstChildren = $this->model->getChildren()[0] ?? null; + + if (!$firstChildren) { + throw new \RuntimeException('No first child found.'); + } + + return $firstChildren; + } +} + +class Other { + /** + * @template TChild of ChildInterface + * @template TModel of ModelInterface + * @param Helper $helper + * @return TChild + */ + public function getFirstChildren(Helper $helper): ChildInterface { + $child = $helper->getFirstChildren(); + assertType('TChild of Discussion9053\ChildInterface (method Discussion9053\Other::getFirstChildren(), argument)', $child); + + return $child; + } +} + +function (): void { + $model = new Model(); + $helper = new Helper($model); + assertType('Discussion9053\Helper', $helper); + $child = $helper->getFirstChildren(); + assertType('Discussion9053\Child', $child); + + $other = new Other(); + $child2 = $other->getFirstChildren($helper); + assertType('Discussion9053\Child', $child2); +}; diff --git a/tests/PHPStan/Analyser/data/discussion-9134.php b/tests/PHPStan/Analyser/data/discussion-9134.php new file mode 100644 index 0000000000..330b51cbaa --- /dev/null +++ b/tests/PHPStan/Analyser/data/discussion-9134.php @@ -0,0 +1,12 @@ +|false', $res); +if (is_array($res) === false) { + throw new \RuntimeException(); +} diff --git a/tests/PHPStan/Analyser/data/discussion-9972.php b/tests/PHPStan/Analyser/data/discussion-9972.php new file mode 100644 index 0000000000..c0e4eabf23 --- /dev/null +++ b/tests/PHPStan/Analyser/data/discussion-9972.php @@ -0,0 +1,26 @@ +helper($myBool); + + if ($myBool) { + assertVariableCertainty(TrinaryLogic::createYes(), $myObject); + } + } + + protected function helper(bool $input): void + { + } +} diff --git a/tests/PHPStan/Analyser/data/div-by-zero.php b/tests/PHPStan/Analyser/data/div-by-zero.php index 2dae4d4767..ad027888bc 100644 --- a/tests/PHPStan/Analyser/data/div-by-zero.php +++ b/tests/PHPStan/Analyser/data/div-by-zero.php @@ -13,9 +13,9 @@ class Foo */ public function doFoo(int $range1, int $range2, int $int): void { - assertType('(float|int)', 5 / $range1); - assertType('(float|int)', 5 / $range2); - assertType('(float|int)', $range1 / $range2); + assertType('float|int<1, 5>', 5 / $range1); + assertType('float|int<-5, -1>', 5 / $range2); + assertType('float|int', $range1 / $range2); assertType('(float|int)', 5 / $int); assertType('*ERROR*', 5 / 0); diff --git a/tests/PHPStan/Analyser/data/do-not-remember-impure-functions.php b/tests/PHPStan/Analyser/data/do-not-remember-impure-functions.php index 4b086bc15c..6900709f99 100644 --- a/tests/PHPStan/Analyser/data/do-not-remember-impure-functions.php +++ b/tests/PHPStan/Analyser/data/do-not-remember-impure-functions.php @@ -74,3 +74,50 @@ public function doDolor() } } + +class ToBeExtended +{ + + /** @phpstan-pure */ + public function pure(): int + { + + } + + /** @phpstan-impure */ + public function impure(): int + { + echo 'test'; + return 1; + } + +} + +class ExtendingClass extends ToBeExtended +{ + + /** + * @return int + */ + public function pure(): int + { + echo 'test'; + return 1; + } + + /** + * @return int + */ + public function impure(): int + { + return 1; + } + +} + +function (ExtendingClass $e): void { + assert($e->pure() === 1); + assertType('1', $e->pure()); + $e->impure(); + assertType('int', $e->pure()); +}; diff --git a/tests/PHPStan/Analyser/data/ds-copy.php b/tests/PHPStan/Analyser/data/ds-copy.php new file mode 100644 index 0000000000..8bf29a71ba --- /dev/null +++ b/tests/PHPStan/Analyser/data/ds-copy.php @@ -0,0 +1,65 @@ + $col + * @param Sequence $seq + * @param Vector $vec + * @param Deque $deque + * @param Map $map + * @param Queue $queue + * @param Stack $stack + * @param PriorityQueue $pq + * @param Set $set + */ + public function __construct( + private readonly Collection $col, + private readonly Sequence $seq, + private readonly Vector $vec, + private readonly Deque $deque, + private readonly Map $map, + private readonly Queue $queue, + private readonly Stack $stack, + private readonly PriorityQueue $pq, + private readonly Set $set, + ) { + } + + public function copy(): void + { + $col = $this->col->copy(); + $seq = $this->seq->copy(); + $vec = $this->vec->copy(); + $deque = $this->deque->copy(); + $map = $this->map->copy(); + $queue = $this->queue->copy(); + $stack = $this->stack->copy(); + $pq = $this->pq->copy(); + $set = $this->set->copy(); + + assertType('Ds\Collection', $col); + assertType('Ds\Sequence', $seq); + assertType('Ds\Vector', $vec); + assertType('Ds\Deque', $deque); + assertType('Ds\Map', $map); + assertType('Ds\Queue', $queue); + assertType('Ds\Stack', $stack); + assertType('Ds\PriorityQueue', $pq); + assertType('Ds\Set', $set); + } +} diff --git a/tests/PHPStan/Analyser/data/dynamic-constant-native-types.php b/tests/PHPStan/Analyser/data/dynamic-constant-native-types.php new file mode 100644 index 0000000000..c39dca6e35 --- /dev/null +++ b/tests/PHPStan/Analyser/data/dynamic-constant-native-types.php @@ -0,0 +1,15 @@ += 8.3 + +namespace DynamicConstantNativeTypes; + +final class Foo +{ + + public const int FOO = 123; + public const int|string BAR = 123; + +} + +function (Foo $foo): void { + die; +}; diff --git a/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension-named-args-fixture.php b/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension-named-args-fixture.php new file mode 100644 index 0000000000..7da45ec443 --- /dev/null +++ b/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension-named-args-fixture.php @@ -0,0 +1,68 @@ += 8.0 + +namespace DynamicMethodThrowTypeExtensionNamedArgs; + +use PHPStan\TrinaryLogic; +use function PHPStan\Testing\assertVariableCertainty; + +class Foo +{ + + /** @throws \Exception */ + public function throwOrNot(bool $need): int + { + if ($need) { + throw new \Exception(); + } + + return 1; + } + + /** @throws \Exception */ + public static function staticThrowOrNot(bool $need): int + { + if ($need) { + throw new \Exception(); + } + + return 1; + } + + public function doFoo1() + { + try { + $result = $this->throwOrNot(need: true); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $result); + } + } + + public function doFoo2() + { + try { + $result = $this->throwOrNot(need: false); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $result); + } + } + + public function doFoo3() + { + try { + $result = self::staticThrowOrNot(need: true); + } finally { + assertVariableCertainty(TrinaryLogic::createMaybe(), $result); + } + } + + public function doFoo4() + { + try { + $result = self::staticThrowOrNot(need: false); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $result); + } + } + +} + diff --git a/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension-named-args.php b/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension-named-args.php index ccbbd25085..727352a332 100644 --- a/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension-named-args.php +++ b/tests/PHPStan/Analyser/data/dynamic-method-throw-type-extension-named-args.php @@ -6,12 +6,10 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicMethodThrowTypeExtension; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\Type; -use function PHPStan\Testing\assertVariableCertainty; class MethodThrowTypeExtension implements DynamicMethodThrowTypeExtension { @@ -60,64 +58,3 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect } } - -class Foo -{ - - /** @throws \Exception */ - public function throwOrNot(bool $need): int - { - if ($need) { - throw new \Exception(); - } - - return 1; - } - - /** @throws \Exception */ - public static function staticThrowOrNot(bool $need): int - { - if ($need) { - throw new \Exception(); - } - - return 1; - } - - public function doFoo1() - { - try { - $result = $this->throwOrNot(need: true); - } finally { - assertVariableCertainty(TrinaryLogic::createMaybe(), $result); - } - } - - public function doFoo2() - { - try { - $result = $this->throwOrNot(need: false); - } finally { - assertVariableCertainty(TrinaryLogic::createYes(), $result); - } - } - - public function doFoo3() - { - try { - $result = self::staticThrowOrNot(need: true); - } finally { - assertVariableCertainty(TrinaryLogic::createMaybe(), $result); - } - } - - public function doFoo4() - { - try { - $result = self::staticThrowOrNot(need: false); - } finally { - assertVariableCertainty(TrinaryLogic::createYes(), $result); - } - } - -} diff --git a/tests/PHPStan/Analyser/data/dynamic-sprintf.php b/tests/PHPStan/Analyser/data/dynamic-sprintf.php new file mode 100644 index 0000000000..17bff757cd --- /dev/null +++ b/tests/PHPStan/Analyser/data/dynamic-sprintf.php @@ -0,0 +1,21 @@ +key()); - assertType('*NEVER*', $it->current()); - assertType('void', $it->next()); + assertType('never', $it->key()); + assertType('never', $it->current()); + assertType('null', $it->next()); assertType('false', $it->valid()); } diff --git a/tests/PHPStan/Analyser/data/enum-from.php b/tests/PHPStan/Analyser/data/enum-from.php new file mode 100644 index 0000000000..2a51131141 --- /dev/null +++ b/tests/PHPStan/Analyser/data/enum-from.php @@ -0,0 +1,92 @@ += 8.1 + +namespace EnumFrom; + +use function PHPStan\Testing\assertType; + +enum FooIntegerEnum: int +{ + + case BAR = 1; + case BAZ = 2; + +} + +enum FooIntegerEnumSubset: int +{ + + case BAR = 1; + +} + +enum FooStringEnum: string +{ + + case BAR = 'bar'; + case BAZ = 'baz'; + +} + +enum FooNumericStringEnum: string +{ + + case ONE = '1'; + +} + +class Foo +{ + + public function doFoo(): void + { + assertType('1', FooIntegerEnum::BAR->value); + assertType('EnumFrom\FooIntegerEnum::BAR', FooIntegerEnum::BAR); + + assertType('null', FooIntegerEnum::tryFrom(0)); + assertType(FooIntegerEnum::class, FooIntegerEnum::from(0)); + assertType('EnumFrom\FooIntegerEnum::BAR', FooIntegerEnum::tryFrom(0 + 1)); + assertType('EnumFrom\FooIntegerEnum::BAR', FooIntegerEnum::from(1 * FooIntegerEnum::BAR->value)); + + assertType('EnumFrom\FooIntegerEnum::BAZ', FooIntegerEnum::tryFrom(2)); + assertType('EnumFrom\FooIntegerEnum::BAZ', FooIntegerEnum::tryFrom(FooIntegerEnum::BAZ->value)); + assertType('EnumFrom\FooIntegerEnum::BAZ', FooIntegerEnum::from(FooIntegerEnum::BAZ->value)); + + assertType("'bar'", FooStringEnum::BAR->value); + assertType('EnumFrom\FooStringEnum::BAR', FooStringEnum::BAR); + + assertType('null', FooStringEnum::tryFrom('barz')); + assertType(FooStringEnum::class, FooStringEnum::from('barz')); + + assertType('EnumFrom\FooStringEnum::BAR', FooStringEnum::tryFrom('ba' . 'r')); + assertType('EnumFrom\FooStringEnum::BAR', FooStringEnum::from(sprintf('%s%s', 'ba', 'r'))); + + assertType('EnumFrom\FooStringEnum::BAZ', FooStringEnum::tryFrom('baz')); + assertType('EnumFrom\FooStringEnum::BAZ', FooStringEnum::tryFrom(FooStringEnum::BAZ->value)); + assertType('EnumFrom\FooStringEnum::BAZ', FooStringEnum::from(FooStringEnum::BAZ->value)); + + assertType('null', FooIntegerEnum::tryFrom('1')); + assertType('null', FooIntegerEnum::tryFrom(1.0)); + assertType('null', FooIntegerEnum::tryFrom(1.0001)); + assertType('null', FooIntegerEnum::tryFrom(true)); + assertType('null', FooNumericStringEnum::tryFrom(1)); + } + + public function supersetToSubset(FooIntegerEnum $foo): void + { + assertType('EnumFrom\FooIntegerEnumSubset::BAR|null', FooIntegerEnumSubset::tryFrom($foo->value)); + assertType('EnumFrom\FooIntegerEnumSubset::BAR', FooIntegerEnumSubset::from($foo->value)); + } + + public function subsetToSuperset(FooIntegerEnumSubset $foo): void + { + assertType('EnumFrom\FooIntegerEnum::BAR', FooIntegerEnum::tryFrom($foo->value)); + assertType('EnumFrom\FooIntegerEnum::BAR', FooIntegerEnum::from($foo->value)); + } + + public function doCaseInsensitive(): void + { + assertType('1', FooInTeGerEnum::BAR->value); + assertType('null', FooInTeGerEnum::tryFrom(0)); + } + +} diff --git a/tests/PHPStan/Analyser/data/enum-reflection-php81.php b/tests/PHPStan/Analyser/data/enum-reflection-php81.php new file mode 100644 index 0000000000..502de6eebd --- /dev/null +++ b/tests/PHPStan/Analyser/data/enum-reflection-php81.php @@ -0,0 +1,23 @@ += 8.1 + +namespace EnumReflection81; + +use ReflectionEnum; +use ReflectionEnumBackedCase; +use ReflectionEnumUnitCase; +use function PHPStan\Testing\assertType; + +enum Foo: int +{ + + case FOO = 1; + case BAR = 2; +} + +function testNarrowGetBackingTypeAfterIsBacked() { + $r = new ReflectionEnum(Foo::class); + assertType('ReflectionType|null', $r->getBackingType()); + if ($r->isBacked()) { + assertType('ReflectionType', $r->getBackingType()); + } +} diff --git a/tests/PHPStan/Analyser/data/enum-reflection-php82.php b/tests/PHPStan/Analyser/data/enum-reflection-php82.php new file mode 100644 index 0000000000..f67ede0a36 --- /dev/null +++ b/tests/PHPStan/Analyser/data/enum-reflection-php82.php @@ -0,0 +1,23 @@ += 8.1 + +namespace EnumReflection82; + +use ReflectionEnum; +use ReflectionEnumBackedCase; +use ReflectionEnumUnitCase; +use function PHPStan\Testing\assertType; + +enum Foo: int +{ + + case FOO = 1; + case BAR = 2; +} + +function testNarrowGetBackingTypeAfterIsBacked() { + $r = new ReflectionEnum(Foo::class); + assertType('ReflectionNamedType|null', $r->getBackingType()); + if ($r->isBacked()) { + assertType('ReflectionNamedType', $r->getBackingType()); + } +} diff --git a/tests/PHPStan/Analyser/data/enum-reflection.php b/tests/PHPStan/Analyser/data/enum-reflection.php new file mode 100644 index 0000000000..38f023a637 --- /dev/null +++ b/tests/PHPStan/Analyser/data/enum-reflection.php @@ -0,0 +1,52 @@ += 8.1 + +namespace EnumReflection; + +use ReflectionEnum; +use ReflectionEnumBackedCase; +use ReflectionEnumUnitCase; +use function PHPStan\Testing\assertType; + +enum Foo: int +{ + + case FOO = 1; + case BAR = 2; + + public function doFoo(): void + { + $r = new ReflectionEnum(self::class); + foreach ($r->getCases() as $case) { + assertType(ReflectionEnumBackedCase::class, $case); + } + + assertType(ReflectionEnumBackedCase::class, $r->getCase('FOO')); + } + +} + +enum Bar +{ + + case FOO; + case BAR; + + public function doFoo(): void + { + $r = new ReflectionEnum(self::class); + foreach ($r->getCases() as $case) { + assertType(ReflectionEnumUnitCase::class, $case); + } + assertType(ReflectionEnumUnitCase::class, $r->getCase('FOO')); + } + +} + +/** @param class-string $class */ +function testNarrowGetNameTypeAfterIsBacked(string $class) { + $r = new ReflectionEnum($class); + assertType('class-string', $r->getName()); + if ($r->isBacked()) { + assertType('class-string', $r->getName()); + } +} diff --git a/tests/PHPStan/Analyser/data/enum_exists.php b/tests/PHPStan/Analyser/data/enum_exists.php new file mode 100644 index 0000000000..33f1200924 --- /dev/null +++ b/tests/PHPStan/Analyser/data/enum_exists.php @@ -0,0 +1,28 @@ +', $enumFqcn); + return (new \ReflectionEnum($enumFqcn))->getCase($name)->getValue(); + } + assertType('string', $enumFqcn); + + return null; +} + +/** + * @param class-string $enumFqcn + */ +function getEnumValueFromClassString(string $enumFqcn, string $name): mixed { + if (enum_exists($enumFqcn)) { + assertType('class-string', $enumFqcn); + return (new \ReflectionEnum($enumFqcn))->getCase($name)->getValue(); + } + assertType('class-string', $enumFqcn); + + return null; +} diff --git a/tests/PHPStan/Analyser/data/enums.php b/tests/PHPStan/Analyser/data/enums.php index 5359bb3144..37490c1847 100644 --- a/tests/PHPStan/Analyser/data/enums.php +++ b/tests/PHPStan/Analyser/data/enums.php @@ -2,6 +2,7 @@ namespace EnumTypeAssertions; +use function in_array; use function PHPStan\Testing\assertType; enum Foo @@ -266,3 +267,84 @@ public function doBar() } } + +class InArrayEnum +{ + + /** @var list */ + private $list; + + public function doFoo(Foo $foo): void + { + if (in_array($foo, $this->list, true)) { + return; + } + + assertType(Foo::class, $foo); + } + +} + +class LooseComparisonWithEnums +{ + public function testEquality(Foo $foo, Bar $bar, Baz $baz, string $s, int $i, bool $b): void + { + assertType('true', $foo == $foo); + assertType('false', $foo == $bar); + assertType('false', $bar == $s); + assertType('false', $s == $bar); + assertType('false', $baz == $i); + assertType('false', $i == $baz); + + assertType('true', true == $foo); + assertType('true', $foo == true); + assertType('false', false == $baz); + assertType('false', $baz == false); + assertType('false', null == $baz); + assertType('false', $baz == null); + + assertType('true', Foo::ONE == true); + assertType('true', true == Foo::ONE); + assertType('false', Foo::ONE == false); + assertType('false', false == Foo::ONE); + assertType('false', null == Foo::ONE); + assertType('false', Foo::ONE == null); + assertType('true', $foo == Foo::ONE || Foo::TWO == $foo); + + assertType('bool', (rand() ? $bar : null) == $s); + assertType('bool', $s == (rand() ? $bar : null)); + assertType('bool', (rand() ? $baz : null) == $i); + assertType('bool', $i == (rand() ? $baz : null)); + assertType('bool', $foo == $b); + assertType('bool', $b == $foo); + } + + public function testNonEquality(Foo $foo, Bar $bar, Baz $baz, string $s, int $i, bool $b): void + { + assertType('false', $foo != $foo); + assertType('true', $foo != $bar); + assertType('true', $bar != $s); + assertType('true', $s != $bar); + assertType('true', $baz != $i); + assertType('true', $i != $baz); + + assertType('false', true != $foo); + assertType('false', $foo != true); + assertType('true', false != $baz); + assertType('true', $baz != false); + assertType('true', null != $baz); + assertType('true', $baz != null); + + assertType('false', Foo::ONE != true); + assertType('false', true != Foo::ONE); + assertType('true', Foo::ONE != false); + assertType('true', false != Foo::ONE); + assertType('true', null != Foo::ONE); + assertType('true', Foo::ONE != null); + + assertType('bool', (rand() ? $bar : null) != $s); + assertType('bool', $s != (rand() ? $bar : null)); + assertType('bool', (rand() ? $baz : null) != $i); + assertType('bool', $i != (rand() ? $baz : null)); + } +} diff --git a/tests/PHPStan/Analyser/data/expression-type-resolver-extension.php b/tests/PHPStan/Analyser/data/expression-type-resolver-extension.php new file mode 100644 index 0000000000..76b6b00389 --- /dev/null +++ b/tests/PHPStan/Analyser/data/expression-type-resolver-extension.php @@ -0,0 +1,21 @@ +methodReturningBoolNoMatterTheCallerUnlessReturnsString()); +assertType('bool', (new WhateverClass2)->methodReturningBoolNoMatterTheCallerUnlessReturnsString()); +assertType('string', (new WhateverClass3)->methodReturningBoolNoMatterTheCallerUnlessReturnsString()); diff --git a/tests/PHPStan/Analyser/data/extra-extra-int-types.php b/tests/PHPStan/Analyser/data/extra-extra-int-types.php new file mode 100644 index 0000000000..98c40a4326 --- /dev/null +++ b/tests/PHPStan/Analyser/data/extra-extra-int-types.php @@ -0,0 +1,23 @@ +', $nonPositiveInt); + assertType('int<0, max>', $nonNegativeInt); + } + +} diff --git a/tests/PHPStan/Analyser/data/extract.php b/tests/PHPStan/Analyser/data/extract.php new file mode 100644 index 0000000000..c57f2ef46d --- /dev/null +++ b/tests/PHPStan/Analyser/data/extract.php @@ -0,0 +1,68 @@ +get('position'); + assertVariableCertainty(TrinaryLogic::createYes(), $location); + + $location ?? ''; + assertVariableCertainty(TrinaryLogic::createYes(), $location); + } + +} + +function maybeTrueVarAssign():void { + if (rand(0,1)) { + $a = true; + } + $x = $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createYes(), $x); + assertType('1|true', $x); + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + assertType('true', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} + +function nullableVarAssign():void { + if (rand(0,1)) { + $a = true; + } else { + $a = null; + } + $x = $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createYes(), $x); + assertType('1|true', $x); + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType('true|null', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} + +function maybeNullableVarAssign():void { + if (rand(0,1)) { + $a = null; + } + $x = $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createYes(), $x); + assertType('1', $x); + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + assertType('null', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} + +function notExistsAssign():void { + $x = $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createYes(), $x); + assertType('1', $x); + assertVariableCertainty(TrinaryLogic::createNo(), $a); + assertType('*ERROR*', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} + +function nullableVarExpr():void { + if (rand(0,1)) { + $a = true; + } else { + $a = null; + } + $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + assertType('true|null', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} + +function maybeNullableVarExpr():void { + if (rand(0,1)) { + $a = null; + } + $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + assertType('null', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} + +function notExistsExpr():void { + $a ?? ($y=1) ?? 1; + + assertVariableCertainty(TrinaryLogic::createNo(), $a); + assertType('*ERROR*', $a); + assertVariableCertainty(TrinaryLogic::createMaybe(), $y); + assertType('1', $y); +} diff --git a/tests/PHPStan/Analyser/data/falsey-empty-certainty.php b/tests/PHPStan/Analyser/data/falsey-empty-certainty.php new file mode 100644 index 0000000000..ba24b22730 --- /dev/null +++ b/tests/PHPStan/Analyser/data/falsey-empty-certainty.php @@ -0,0 +1,98 @@ + null]; + if (rand() % 3) { + $a = ['bar' => 'hello']; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (empty($a['bar'])) { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyEmptyUncertainPropertyFetch(): void +{ + if (rand() % 2) { + $a = new \stdClass(); + if (rand() % 3) { + $a->x = 'hello'; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (empty($a->x)) { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function justEmpty(): void +{ + assertVariableCertainty(TrinaryLogic::createNo(), $foo); + if (!empty($foo)) { + assertVariableCertainty(TrinaryLogic::createNo(), $foo); + } else { + assertVariableCertainty(TrinaryLogic::createNo(), $foo); + } + assertVariableCertainty(TrinaryLogic::createNo(), $foo); +} + +function maybeEmpty(): void +{ + if (rand() % 2) { + $foo = 1; + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + if (!empty($foo)) { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); +} + +function maybeEmptyUnset(): void +{ + if (rand() % 2) { + $foo = 1; + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + if (!empty($foo)) { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + unset($foo); + assertVariableCertainty(TrinaryLogic::createNo(), $foo); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); +} + + +function parseVariableSymbolFromXmlNode(SimpleXMLElement $transactionXmlElement): string +{ + if ( + !empty($transactionXmlElement->invoice_number) + && preg_match('~^\d+/\d+\-0*(\d+)$~', (string) $transactionXmlElement->invoice_number, $matches) === 1 + ) { + assertVariableCertainty(TrinaryLogic::createYes(), $matches); + } +} diff --git a/tests/PHPStan/Analyser/data/falsey-isset-certainty.php b/tests/PHPStan/Analyser/data/falsey-isset-certainty.php new file mode 100644 index 0000000000..1fbb9547ac --- /dev/null +++ b/tests/PHPStan/Analyser/data/falsey-isset-certainty.php @@ -0,0 +1,282 @@ +bar = null; + if (rand() % 3) { + $a->bar = 'hello'; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + if (isset($a->bar)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function falseyIssetUncertainArrayDimFetchOnProperty(): void +{ + if (rand() % 2) { + $a = new \stdClass(); + $a->bar = null; + $a = ['bar' => null]; + if (rand() % 3) { + $a->bar = 'hello'; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a->bar)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyIssetUncertainPropertyFetch(): void +{ + if (rand() % 2) { + $a = new \stdClass(); + if (rand() % 3) { + $a->x = 'hello'; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a->x)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyIssetArrayDimFetch(): void +{ + $a = ['bar' => null]; + if (rand() % 3) { + $a = ['bar' => 'hello']; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + if (isset($a['bar'])) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function falseyIssetUncertainArrayDimFetch(): void +{ + if (rand() % 2) { + $a = ['bar' => null]; + if (rand() % 3) { + $a = ['bar' => 'hello']; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a['bar'])) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyIssetVariable(): void +{ + if (rand() % 2) { + $a = 'bar'; + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createNo(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function nullableVariable(): void +{ + $a = 'bar'; + if (rand() % 2) { + $a = null; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function nonNullableVariable(): void +{ + $a = 'bar'; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createNo(), $a); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function nonNullableVariableUnset(): void +{ + $a = 'bar'; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + unset($a); + assertVariableCertainty(TrinaryLogic::createNo(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createNo(), $a); + } + + assertVariableCertainty(TrinaryLogic::createNo(), $a); +} + +function falseyIssetNullableVariable(): void +{ + if (rand() % 2) { + $a = 'bar'; + if (rand() % 3) { + $a = null; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyMixedIssetVariable(): void +{ + if (rand() % 2) { + $a = getFoo(); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseySubtractedMixedIssetVariable(): void +{ + if (rand() % 2) { + $a = getFoo(); + if ($a === null) { + return; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + if (isset($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + } else { + assertVariableCertainty(TrinaryLogic::createNo(), $a); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyIssetWithAssignment(): void +{ + if (rand() % 2) { + $x = ['x' => 1]; + } + + if (isset($x[$z = getFoo()])) { + assertVariableCertainty(TrinaryLogic::createYes(), $z); + assertVariableCertainty(TrinaryLogic::createYes(), $x); + + } else { + assertVariableCertainty(TrinaryLogic::createYes(), $z); + assertVariableCertainty(TrinaryLogic::createMaybe(), $x); + } + + assertVariableCertainty(TrinaryLogic::createYes(), $z); + assertVariableCertainty(TrinaryLogic::createMaybe(), $x); +} + +function justIsset(): void +{ + if (isset($foo)) { + assertVariableCertainty(TrinaryLogic::createNo(), $foo); + } +} + +function maybeIsset(): void +{ + if (rand() % 2) { + $foo = 1; + } + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + if (isset($foo)) { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + assertType('1', $foo); + } +} + +function isStringNarrowsMaybeCertainty(int $i, string $s): void +{ + if (rand(0, 1)) { + $a = rand(0,1) ? $i : $s; + } + + if (is_string($a)) { + assertVariableCertainty(TrinaryLogic::createYes(), $a); + echo $a; + } +} + +function parseVariableSymbolFromXmlNode(SimpleXMLElement $transactionXmlElement): string +{ + if ( + isset($transactionXmlElement->invoice_number) + && preg_match('~^\d+/\d+\-0*(\d+)$~', (string) $transactionXmlElement->invoice_number, $matches) === 1 + ) { + assertVariableCertainty(TrinaryLogic::createYes(), $matches); + } +} diff --git a/tests/PHPStan/Analyser/data/falsey-ternary-certainty.php b/tests/PHPStan/Analyser/data/falsey-ternary-certainty.php new file mode 100644 index 0000000000..01045e25f9 --- /dev/null +++ b/tests/PHPStan/Analyser/data/falsey-ternary-certainty.php @@ -0,0 +1,226 @@ +bar = null; + if (rand() % 3) { + $a->bar = 'hello'; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + isset($a->bar)? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createYes(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function falseyTernaryUncertainArrayDimFetchOnProperty(): void +{ + if (rand() % 2) { + $a = new \stdClass(); + $a->bar = null; + $a = ['bar' => null]; + if (rand() % 3) { + $a->bar = 'hello'; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a->bar) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createMaybe(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyTernaryUncertainPropertyFetch(): void +{ + if (rand() % 2) { + $a = new \stdClass(); + if (rand() % 3) { + $a->x = 'hello'; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a->x) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createMaybe(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyTernaryArrayDimFetch(): void +{ + $a = ['bar' => null]; + if (rand() % 3) { + $a = ['bar' => 'hello']; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + isset($a['bar']) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createYes(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function falseyTernaryUncertainArrayDimFetch(): void +{ + if (rand() % 2) { + $a = ['bar' => null]; + if (rand() % 3) { + $a = ['bar' => 'hello']; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a['bar']) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createMaybe(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyTernaryVariable(): void +{ + if (rand() % 2) { + $a = 'bar'; + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createNo(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function nullableVariable(): void +{ + $a = 'bar'; + if (rand() % 2) { + $a = null; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + isset($a) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createYes(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function nonNullableVariable(): void +{ + $a = 'bar'; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + isset($a) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createNo(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function nonNullableVariableShort(): void +{ + $a = 'bar'; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); + isset($a) ?: + assertVariableCertainty(TrinaryLogic::createNo(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createYes(), $a); +} + +function falseyTernaryNullableVariable(): void +{ + if (rand() % 2) { + $a = 'bar'; + if (rand() % 3) { + $a = null; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createMaybe(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseyMixedTernaryVariable(): void +{ + if (rand() % 2) { + $a = getFoo(); + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createMaybe(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function falseySubtractedMixedTernaryVariable(): void +{ + if (rand() % 2) { + $a = getFoo(); + if ($a === null) { + return; + } + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); + isset($a) ? + assertVariableCertainty(TrinaryLogic::createYes(), $a) + : + assertVariableCertainty(TrinaryLogic::createNo(), $a) + ; + + assertVariableCertainty(TrinaryLogic::createMaybe(), $a); +} + +function parseVariableSymbolFromXmlNode(SimpleXMLElement $transactionXmlElement): string +{ + return isset($transactionXmlElement->invoice_number) + && preg_match('~^\d+/\d+\-0*(\d+)$~', (string) $transactionXmlElement->invoice_number, $matches) === 1 + ? assertVariableCertainty(TrinaryLogic::createYes(), $matches) + : ''; +} diff --git a/tests/PHPStan/Analyser/data/falsy-isset.php b/tests/PHPStan/Analyser/data/falsy-isset.php new file mode 100644 index 0000000000..bce229826a --- /dev/null +++ b/tests/PHPStan/Analyser/data/falsy-isset.php @@ -0,0 +1,99 @@ + $noteListLimit; + if ($showAllLink) { + assertType('int', $noteListLimit); + } +} diff --git a/tests/PHPStan/Analyser/data/filesystem-functions.php b/tests/PHPStan/Analyser/data/filesystem-functions.php index 988fbc8dc3..fc7614c63a 100644 --- a/tests/PHPStan/Analyser/data/filesystem-functions.php +++ b/tests/PHPStan/Analyser/data/filesystem-functions.php @@ -59,7 +59,7 @@ public function test3($fh): void public function test4(string $path): void { if (file_get_contents($path) === 'data') { - assertType('\'data\'', file_get_contents($path)); + assertType('string|false', file_get_contents($path)); file_put_contents($path, 'other'); assertType('string|false', file_get_contents($path)); } diff --git a/tests/PHPStan/Analyser/data/filter-input-array.php b/tests/PHPStan/Analyser/data/filter-input-array.php new file mode 100644 index 0000000000..706a300680 --- /dev/null +++ b/tests/PHPStan/Analyser/data/filter-input-array.php @@ -0,0 +1,79 @@ + FILTER_VALIDATE_INT, + 'flag' => FILTER_REQUIRE_SCALAR, + 'options' => ['min_range' => 1], + ]; + + // filter array with add_empty=default + assertType('array{int: int|false|null, positive_int: int<1, max>|false|null}', filter_input_array(INPUT_GET, [ + 'int' => FILTER_VALIDATE_INT, + 'positive_int' => $filter, + ])); + + // filter array with add_empty=true + assertType('array{int: int|false|null, positive_int: int<1, max>|false|null}', filter_input_array(INPUT_GET, [ + 'int' => FILTER_VALIDATE_INT, + 'positive_int' => $filter, + ], true)); + + // filter array with add_empty=false + assertType('array{int?: int|false, positive_int?: int<1, max>|false}', filter_input_array(INPUT_GET, [ + 'int' => FILTER_VALIDATE_INT, + 'positive_int' => $filter, + ], false)); + + // filter flag with add_empty=default + assertType('array', filter_input_array(INPUT_GET, FILTER_VALIDATE_INT)); + // filter flag with add_empty=true + assertType('array', filter_input_array(INPUT_GET, FILTER_VALIDATE_INT, true)); + // filter flag with add_empty=false + assertType('array', filter_input_array(INPUT_GET, FILTER_VALIDATE_INT, false)); + } + + /** + * @param array $arrayFilter + * @param FILTER_VALIDATE_* $intFilter + */ + function dynamicFilter(array $input, array $arrayFilter, int $intFilter): void + { + // filter array with add_empty=default + assertType('array', filter_input_array(INPUT_GET, $arrayFilter)); + // filter array with add_empty=true + assertType('array', filter_input_array(INPUT_GET, $arrayFilter, true)); + // filter array with add_empty=false + assertType('array', filter_input_array(INPUT_GET, $arrayFilter, false)); + + // filter flag with add_empty=default + assertType('array', filter_input_array(INPUT_GET, $intFilter)); + // filter flag with add_empty=true + assertType('array', filter_input_array(INPUT_GET, $intFilter, true)); + // filter flag with add_empty=false + assertType('array', filter_input_array(INPUT_GET, $intFilter, false)); + } + + /** + * @param INPUT_GET|INPUT_POST $union + */ + public function dynamicInputType($union, mixed $mixed): void + { + $filter = [ + 'filter' => FILTER_VALIDATE_INT, + 'flag' => FILTER_REQUIRE_SCALAR, + 'options' => ['min_range' => 1], + ]; + + assertType('array{foo: int<1, max>|false|null}', filter_input_array($union, ['foo' => $filter])); + assertType('array|false|null', filter_input_array($mixed, ['foo' => $filter])); + } + +} diff --git a/tests/PHPStan/Analyser/data/filter-input-php7.php b/tests/PHPStan/Analyser/data/filter-input-php7.php new file mode 100644 index 0000000000..b5ee1d393d --- /dev/null +++ b/tests/PHPStan/Analyser/data/filter-input-php7.php @@ -0,0 +1,16 @@ + FILTER_NULL_ON_FAILURE])); + assertType("'invalid'|int|null", filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['options' => ['default' => 'invalid']])); + assertType('array|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY])); + assertType('array|false', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('0|int<17, 19>|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['options' => ['default' => 0, 'min_range' => 17, 'max_range' => 19]])); + } + +} diff --git a/tests/PHPStan/Analyser/data/filter-var-array.php b/tests/PHPStan/Analyser/data/filter-var-array.php new file mode 100644 index 0000000000..52261b0e8a --- /dev/null +++ b/tests/PHPStan/Analyser/data/filter-var-array.php @@ -0,0 +1,340 @@ + '1', + 'invalid' => 'a', + ]; + + // filter array with add_empty=default + assertType('array{valid: 1, invalid: false, missing: null}', filter_var_array($input, [ + 'valid' => FILTER_VALIDATE_INT, + 'invalid' => FILTER_VALIDATE_INT, + 'missing' => FILTER_VALIDATE_INT, + ])); + + // filter array with add_empty=true + assertType('array{valid: 1, invalid: false, missing: null}', filter_var_array($input, [ + 'valid' => FILTER_VALIDATE_INT, + 'invalid' => FILTER_VALIDATE_INT, + 'missing' => FILTER_VALIDATE_INT, + ], true)); + + // filter array with add_empty=false + assertType('array{valid: 1, invalid: false}', filter_var_array($input, [ + 'valid' => FILTER_VALIDATE_INT, + 'invalid' => FILTER_VALIDATE_INT, + 'missing' => FILTER_VALIDATE_INT, + ], false)); + + // filter flag with add_empty=default + assertType('array{valid: 1, invalid: false}', filter_var_array($input, FILTER_VALIDATE_INT)); + // filter flag with add_empty=true + assertType('array{valid: 1, invalid: false}', filter_var_array($input, FILTER_VALIDATE_INT, true)); + // filter flag with add_empty=false + assertType('array{valid: 1, invalid: false}', filter_var_array($input, FILTER_VALIDATE_INT, false)); + + $filter = [ + 'filter' => FILTER_VALIDATE_INT, + 'flag' => FILTER_REQUIRE_SCALAR, + 'options' => ['min_range' => 1, 'max_range' => 10], + ]; + + // filter array with add_empty=default + assertType('array{valid: 1, invalid: false, missing: null}', filter_var_array($input, [ + 'valid' => $filter, + 'invalid' => $filter, + 'missing' => $filter, + ])); + + // filter array with add_empty=default + assertType('array{valid: 1, invalid: false, missing: null}', filter_var_array($input, [ + 'valid' => $filter, + 'invalid' => $filter, + 'missing' => $filter, + ], true)); + + // filter array with add_empty=default + assertType('array{valid: 1, invalid: false}', filter_var_array($input, [ + 'valid' => $filter, + 'invalid' => $filter, + 'missing' => $filter, + ], false)); +} + +function mixedInput(mixed $input): void +{ + // filter array with add_empty=default + assertType('array{id: int|false|null}', filter_var_array($input, [ + 'id' => FILTER_VALIDATE_INT, + ])); + + // filter array with add_empty=true + assertType('array{id: int|false|null}', filter_var_array($input, [ + 'id' => FILTER_VALIDATE_INT, + ], true)); + + // filter array with add_empty=false + assertType('array{id?: int|false}', filter_var_array($input, [ + 'id' => FILTER_VALIDATE_INT, + ], false)); + + // filter flag with add_empty=default + assertType('array', filter_var_array($input, FILTER_VALIDATE_INT)); + // filter flag with add_empty=true + assertType('array', filter_var_array($input, FILTER_VALIDATE_INT, true)); + // filter flag with add_empty=false + assertType('array', filter_var_array($input, FILTER_VALIDATE_INT, false)); + + $filter = [ + 'filter' => FILTER_VALIDATE_INT, + 'flag' => FILTER_REQUIRE_SCALAR, + 'options' => ['min_range' => 1, 'max_range' => 10], + ]; + + // filter array with add_empty=default + assertType('array{id: int<1, 10>|false|null}', filter_var_array($input, [ + 'id' => $filter, + ])); + + // filter array with add_empty=default + assertType('array{id: int<1, 10>|false|null}', filter_var_array($input, [ + 'id' => $filter, + ], true)); + + // filter array with add_empty=default + assertType('array{id?: int<1, 10>|false}', filter_var_array($input, [ + 'id' => $filter, + ], false)); +} + +function emptyArrayInput(): void +{ + // filter array with add_empty=default + assertType('array{valid: null, invalid: null, missing: null}', filter_var_array([], [ + 'valid' => FILTER_VALIDATE_INT, + 'invalid' => FILTER_VALIDATE_INT, + 'missing' => FILTER_VALIDATE_INT, + ])); + + // filter array with add_empty=true + assertType('array{valid: null, invalid: null, missing: null}', filter_var_array([], [ + 'valid' => FILTER_VALIDATE_INT, + 'invalid' => FILTER_VALIDATE_INT, + 'missing' => FILTER_VALIDATE_INT, + ], true)); + + // filter array with add_empty=false + assertType('array{}', filter_var_array([], [ + 'valid' => FILTER_VALIDATE_INT, + 'invalid' => FILTER_VALIDATE_INT, + 'missing' => FILTER_VALIDATE_INT, + ], false)); + + // filter flag with add_empty=default + assertType('array{}', filter_var_array([], FILTER_VALIDATE_INT)); + // filter flag with add_empty=true + assertType('array{}', filter_var_array([], FILTER_VALIDATE_INT, true)); + // filter flag with add_empty=false + assertType('array{}', filter_var_array([], FILTER_VALIDATE_INT, false)); + + $filter = [ + 'filter' => FILTER_VALIDATE_INT, + 'flag' => FILTER_REQUIRE_SCALAR, + 'options' => ['min_range' => 1, 'max_range' => 10], + ]; + + // filter array with add_empty=default + assertType('array{valid: null, invalid: null, missing: null}', filter_var_array([], [ + 'valid' => $filter, + 'invalid' => $filter, + 'missing' => $filter, + ])); + + // filter array with add_empty=default + assertType('array{valid: null, invalid: null, missing: null}', filter_var_array([], [ + 'valid' => $filter, + 'invalid' => $filter, + 'missing' => $filter, + ], true)); + + // filter array with add_empty=default + assertType('array{}', filter_var_array([], [ + 'valid' => $filter, + 'invalid' => $filter, + 'missing' => $filter, + ], false)); +} + +function superGlobalVariables(): void +{ + $filter = [ + 'filter' => FILTER_VALIDATE_INT, + 'flag' => FILTER_REQUIRE_SCALAR, + 'options' => ['min_range' => 1], + ]; + + // filter array with add_empty=default + assertType('array{int: int|false|null, positive_int: int<1, max>|false|null}', filter_var_array($_POST, [ + 'int' => FILTER_VALIDATE_INT, + 'positive_int' => $filter, + ])); + + // filter array with add_empty=true + assertType('array{int: int|false|null, positive_int: int<1, max>|false|null}', filter_var_array($_POST, [ + 'int' => FILTER_VALIDATE_INT, + 'positive_int' => $filter, + ], true)); + + // filter array with add_empty=false + assertType('array{int?: int|false, positive_int?: int<1, max>|false}', filter_var_array($_POST, [ + 'int' => FILTER_VALIDATE_INT, + 'positive_int' => $filter, + ], false)); + + // filter flag with add_empty=default + assertType('array', filter_var_array($_POST, FILTER_VALIDATE_INT)); + // filter flag with add_empty=true + assertType('array', filter_var_array($_POST, FILTER_VALIDATE_INT, true)); + // filter flag with add_empty=false + assertType('array', filter_var_array($_POST, FILTER_VALIDATE_INT, false)); +} + +/** + * @param list $input + */ +function typedList($input): void +{ + $filter = [ + 'filter' => FILTER_VALIDATE_INT, + 'flag' => FILTER_REQUIRE_SCALAR, + 'options' => ['min_range' => 1], + ]; + + // filter array with add_empty=default + assertType('array{int: int|null, positive_int: int<1, max>|false|null}', filter_var_array($input, [ + 'int' => FILTER_VALIDATE_INT, + 'positive_int' => $filter, + ])); + + // filter array with add_empty=true + assertType('array{int: int|null, positive_int: int<1, max>|false|null}', filter_var_array($input, [ + 'int' => FILTER_VALIDATE_INT, + 'positive_int' => $filter, + ], true)); + + // filter array with add_empty=false + assertType('array{int?: int, positive_int?: int<1, max>|false}', filter_var_array($input, [ + 'int' => FILTER_VALIDATE_INT, + 'positive_int' => $filter, + ], false)); + + // filter flag with add_empty=default + assertType('list', filter_var_array($input, FILTER_VALIDATE_INT)); + // filter flag with add_empty=true + assertType('list', filter_var_array($input, FILTER_VALIDATE_INT, true)); + // filter flag with add_empty=false + assertType('list', filter_var_array($input, FILTER_VALIDATE_INT, false)); +} + +/** + * @param array{exists: int, optional?: int, extra: int} $input + */ +function dynamicVariables(array $input): void +{ + // filter array with add_empty=default + assertType('array{exists: int, optional: int|null, missing: null}', filter_var_array($input, [ + 'exists' => FILTER_VALIDATE_INT, + 'optional' => FILTER_VALIDATE_INT, + 'missing' => FILTER_VALIDATE_INT, + ])); + + // filter array with add_empty=true + assertType('array{exists: int, optional: int|null, missing: null}', filter_var_array($input, [ + 'exists' => FILTER_VALIDATE_INT, + 'optional' => FILTER_VALIDATE_INT, + 'missing' => FILTER_VALIDATE_INT, + ], true)); + + // filter array with add_empty=false + assertType('array{exists: int, optional?: int}', filter_var_array($input, [ + 'exists' => FILTER_VALIDATE_INT, + 'optional' => FILTER_VALIDATE_INT, + 'missing' => FILTER_VALIDATE_INT, + ], false)); + + // filter flag with add_empty=default + assertType('array{exists: int, optional?: int, extra: int}', filter_var_array($input, FILTER_VALIDATE_INT)); + // filter flag with add_empty=true + assertType('array{exists: int, optional?: int, extra: int}', filter_var_array($input, FILTER_VALIDATE_INT, true)); + // filter flag with add_empty=false + assertType('array{exists: int, optional?: int, extra: int}', filter_var_array($input, FILTER_VALIDATE_INT, false)); + + $filter = [ + 'filter' => FILTER_VALIDATE_INT, + 'flag' => FILTER_REQUIRE_SCALAR, + 'options' => ['min_range' => 1, 'max_range' => 10], + ]; + + // filter array with add_empty=default + assertType('array{exists: int<1, 10>|false, optional: int<1, 10>|false|null, missing: null}', filter_var_array($input, [ + 'exists' => $filter, + 'optional' => $filter, + 'missing' => $filter, + ])); + + // filter array with add_empty=default + assertType('array{exists: int<1, 10>|false, optional: int<1, 10>|false|null, missing: null}', filter_var_array($input, [ + 'exists' => $filter, + 'optional' => $filter, + 'missing' => $filter, + ], true)); + + // filter array with add_empty=default + assertType('array{exists: int<1, 10>|false, optional?: int<1, 10>|false}', filter_var_array($input, [ + 'exists' => $filter, + 'optional' => $filter, + 'missing' => $filter, + ], false)); +} + +/** + * @param array{exists: int, optional?: int, extra: int} $input + * @param array $arrayFilter + * @param FILTER_VALIDATE_* $intFilter + */ +function dynamicFilter(array $input, array $arrayFilter, int $intFilter): void +{ + // filter array with add_empty=default + assertType('array|false|null', filter_var_array($input, $arrayFilter)); + // filter array with add_empty=true + assertType('array|false|null', filter_var_array($input, $arrayFilter, true)); + // filter array with add_empty=false + assertType('array|false|null', filter_var_array($input, $arrayFilter, false)); + + // filter flag with add_empty=default + assertType('array|false|null', filter_var_array($input, $intFilter)); + // filter flag with add_empty=true + assertType('array|false|null', filter_var_array($input, $intFilter, true)); + // filter flag with add_empty=false + assertType('array|false|null', filter_var_array($input, $intFilter, false)); + + // filter array with add_empty=default + assertType('array|false|null', filter_var_array([], $arrayFilter)); + // filter array with add_empty=true + assertType('array|false|null', filter_var_array([], $arrayFilter, true)); + // filter array with add_empty=false + assertType('array|false|null', filter_var_array([], $arrayFilter, false)); + + // filter flag with add_empty=default + assertType('array|false|null', filter_var_array([], $intFilter)); + // filter flag with add_empty=true + assertType('array|false|null', filter_var_array([], $intFilter, true)); + // filter flag with add_empty=false + assertType('array|false|null', filter_var_array([], $intFilter, false)); +} diff --git a/tests/PHPStan/Analyser/data/filter-var-dynamic-return-type-extension-regression.php b/tests/PHPStan/Analyser/data/filter-var-dynamic-return-type-extension-regression.php new file mode 100644 index 0000000000..69fc99f613 --- /dev/null +++ b/tests/PHPStan/Analyser/data/filter-var-dynamic-return-type-extension-regression.php @@ -0,0 +1,60 @@ +determineExactType(); + $type = $exactType ?? new MixedType(); + $otherTypes = $this->getOtherTypes(); + + assertType('array{default: PHPStan\Type\Type, range?: PHPStan\Type\Type}', $otherTypes); + if (isset($otherTypes['range'])) { + assertType('array{default: PHPStan\Type\Type, range: PHPStan\Type\Type}', $otherTypes); + if ($type instanceof ConstantScalarType) { + if ($otherTypes['range']->isSuperTypeOf($type)->no()) { + $type = $otherTypes['default']; + } + assertType('array{default: PHPStan\Type\Type, range: PHPStan\Type\Type}', $otherTypes); + unset($otherTypes['default']); + assertType('array{range: PHPStan\Type\Type}', $otherTypes); + } else { + $type = $otherTypes['range']; + assertType('array{default: PHPStan\Type\Type, range: PHPStan\Type\Type}', $otherTypes); + } + assertType('array{default?: PHPStan\Type\Type, range: PHPStan\Type\Type}', $otherTypes); + } + assertType('array{default?: PHPStan\Type\Type, range?: PHPStan\Type\Type}&non-empty-array', $otherTypes); + if ($exactType !== null) { + assertType('array{default?: PHPStan\Type\Type, range?: PHPStan\Type\Type}&non-empty-array', $otherTypes); + unset($otherTypes['default']); + assertType('array{range?: PHPStan\Type\Type}', $otherTypes); + } + assertType('array{default?: PHPStan\Type\Type, range?: PHPStan\Type\Type}', $otherTypes); + if (isset($otherTypes['default']) && $otherTypes['default']->isSuperTypeOf($type)->no()) { + $type = TypeCombinator::union($type, $otherTypes['default']); + } + } +} diff --git a/tests/PHPStan/Analyser/data/filter-var-returns-non-empty-string.php b/tests/PHPStan/Analyser/data/filter-var-returns-non-empty-string.php index 26965000cf..dc6620b0ca 100644 --- a/tests/PHPStan/Analyser/data/filter-var-returns-non-empty-string.php +++ b/tests/PHPStan/Analyser/data/filter-var-returns-non-empty-string.php @@ -16,7 +16,7 @@ public function run(string $str, int $int, int $positive_int, int $negative_int) assertType('non-empty-string', $str); $return = filter_var($str, FILTER_DEFAULT); - assertType('non-empty-string|false', $return); + assertType('non-empty-string', $return); $return = filter_var($str, FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW); assertType('string|false', $return); @@ -28,22 +28,22 @@ public function run(string $str, int $int, int $positive_int, int $negative_int) assertType('string|false', $return); $return = filter_var($str, FILTER_VALIDATE_EMAIL); - assertType('non-empty-string|false', $return); + assertType('non-falsy-string|false', $return); $return = filter_var($str, FILTER_VALIDATE_REGEXP); assertType('non-empty-string|false', $return); $return = filter_var($str, FILTER_VALIDATE_URL); - assertType('non-empty-string|false', $return); + assertType('non-falsy-string|false', $return); $return = filter_var($str, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE); - assertType('non-empty-string|null', $return); + assertType('non-falsy-string|null', $return); $return = filter_var($str, FILTER_VALIDATE_IP); - assertType('non-empty-string|false', $return); + assertType('non-falsy-string|false', $return); $return = filter_var($str, FILTER_VALIDATE_MAC); - assertType('non-empty-string|false', $return); + assertType('non-falsy-string|false', $return); $return = filter_var($str, FILTER_VALIDATE_DOMAIN); assertType('non-empty-string|false', $return); @@ -82,7 +82,10 @@ public function run(string $str, int $int, int $positive_int, int $negative_int) assertType('9', $return); $return = filter_var(1.0, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 9]]); - assertType('int<1, 9>|false', $return); + assertType('1', $return); + + $return = filter_var(11.0, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 9]]); + assertType('false', $return); $return = filter_var($str, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => $positive_int]]); assertType('int<1, max>|false', $return); @@ -95,10 +98,10 @@ public function run(string $str, int $int, int $positive_int, int $negative_int) $str2 = ''; $return = filter_var($str2, FILTER_DEFAULT); - assertType('string|false', $return); + assertType("''", $return); $return = filter_var($str2, FILTER_VALIDATE_URL); - assertType('string|false', $return); + assertType('non-falsy-string|false', $return); $return = filter_var('foo', FILTER_VALIDATE_INT); assertType('false', $return); diff --git a/tests/PHPStan/Analyser/data/filter-var.php b/tests/PHPStan/Analyser/data/filter-var.php new file mode 100644 index 0000000000..12f97e63b5 --- /dev/null +++ b/tests/PHPStan/Analyser/data/filter-var.php @@ -0,0 +1,164 @@ + $stringMixedMap + */ + public function doFoo($mixed, array $stringMixedMap): void + { + assertType('int|false', filter_var($mixed, FILTER_VALIDATE_INT)); + assertType('int|null', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_NULL_ON_FAILURE])); + + assertType('17', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_SCALAR])); + assertType('false', filter_var([17], FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_SCALAR])); + + assertType('false', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY])); + assertType('null', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('false', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY])); + assertType('null', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array|false', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY])); + assertType('array|null', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array|false', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY])); + assertType('array|null', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE])); + + assertType('array<17>', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY])); + assertType('array<17>', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY])); + assertType('array', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY])); + assertType('array', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY])); + assertType('array', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + + assertType('false', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY])); + assertType('null', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('false', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY])); + assertType('null', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array|false', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY])); + assertType('array|null', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array|false', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY])); + assertType('array|null', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + + assertType('0|int<17, 19>', filter_var($mixed, FILTER_VALIDATE_INT, ['options' => ['default' => 0, 'min_range' => 17, 'max_range' => 19]])); + + assertType('array', filter_var(false, FILTER_VALIDATE_BOOLEAN, FILTER_FORCE_ARRAY | FILTER_NULL_ON_FAILURE)); + } + + /** + * @param int<17, 19> $range1 + * @param int<1, 5> $range2 + * @param int<18, 19> $range3 + */ + public function intRanges(int $int, int $min, int $max, int $range1, int $range2, int $range3): void + { + assertType('int<17, 19>|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => 17, 'max_range' => 19]])); + assertType('false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => 19, 'max_range' => 17]])); + assertType('0|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => null, 'max_range' => null]])); + assertType('int<17, 19>|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => '17', 'max_range' => '19']])); + assertType('int|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => $min, 'max_range' => 19]])); + assertType('int<17, max>|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => 17, 'max_range' => $max]])); + assertType('int<17, 19>', filter_var($range1, FILTER_VALIDATE_INT, ['options' => ['min_range' => 17, 'max_range' => 19]])); + assertType('false', filter_var(9, FILTER_VALIDATE_INT, ['options' => ['min_range' => 17, 'max_range' => 19]])); + assertType('18', filter_var(18, FILTER_VALIDATE_INT, ['options' => ['min_range' => 17, 'max_range' => 19]])); + assertType('18', filter_var(18, FILTER_VALIDATE_INT, ['options' => ['min_range' => '17', 'max_range' => '19']])); + assertType('false', filter_var(-18, FILTER_VALIDATE_INT, ['options' => ['min_range' => null, 'max_range' => 19]])); + assertType('false', filter_var(18, FILTER_VALIDATE_INT, ['options' => ['min_range' => 17, 'max_range' => null]])); + assertType('false', filter_var($range2, FILTER_VALIDATE_INT, ['options' => ['min_range' => 17, 'max_range' => 19]])); + assertType('int<18, 19>', filter_var($range3, FILTER_VALIDATE_INT, ['options' => ['min_range' => 17, 'max_range' => 19]])); + assertType('int|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => $min, 'max_range' => $max]])); + assertType('int|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => $min]])); + assertType('int|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['max_range' => $max]])); + } + + /** @param resource $resource */ + public function invalidInput(array $arr, object $object, $resource): void + { + assertType('false', filter_var($arr)); + assertType('false', filter_var($object)); + assertType('false', filter_var($resource)); + assertType('null', filter_var(new stdClass(), FILTER_DEFAULT, FILTER_NULL_ON_FAILURE)); + assertType("'invalid'", filter_var(new stdClass(), FILTER_DEFAULT, ['options' => ['default' => 'invalid']])); + } + + public function intToInt(int $int, array $options): void + { + assertType('int', filter_var($int, FILTER_VALIDATE_INT)); + assertType('int|false', filter_var($int, FILTER_VALIDATE_INT, $options)); + assertType('int<0, max>|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]])); + } + + /** + * @param int<0, 9> $intRange + * @param non-empty-string $nonEmptyString + */ + public function scalars(bool $bool, float $float, int $int, string $string, int $intRange, string $nonEmptyString): void + { + assertType('bool', filter_var($bool, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); + assertType('true', filter_var(true, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); + assertType('false', filter_var(false, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); + assertType('bool|null', filter_var($float, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); + assertType('bool|null', filter_var(17.0, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // could be null + assertType('bool|null', filter_var(17.1, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // could be null + assertType('bool|null', filter_var($int, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); + assertType('bool|null', filter_var($intRange, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); + assertType('bool|null', filter_var(17, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // could be null + assertType('bool|null', filter_var($string, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); + assertType('bool|null', filter_var($nonEmptyString, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); + assertType('bool|null', filter_var('17', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // could be null + assertType('bool|null', filter_var('17.1', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); // could be null + assertType('null', filter_var(null, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)); + + assertType('float|false', filter_var($bool, FILTER_VALIDATE_FLOAT)); + assertType('1.0', filter_var(true, FILTER_VALIDATE_FLOAT)); + assertType('false', filter_var(false, FILTER_VALIDATE_FLOAT)); + assertType('float', filter_var($float, FILTER_VALIDATE_FLOAT)); + assertType('17.0', filter_var(17.0, FILTER_VALIDATE_FLOAT)); + assertType('17.1', filter_var(17.1, FILTER_VALIDATE_FLOAT)); + assertType('float', filter_var($int, FILTER_VALIDATE_FLOAT)); + assertType('float', filter_var($intRange, FILTER_VALIDATE_FLOAT)); + assertType('17.0', filter_var(17, FILTER_VALIDATE_FLOAT)); + assertType('float|false', filter_var($string, FILTER_VALIDATE_FLOAT)); + assertType('float|false', filter_var($nonEmptyString, FILTER_VALIDATE_FLOAT)); + assertType('float|false', filter_var('17', FILTER_VALIDATE_FLOAT)); // could be 17.0 + assertType('float|false', filter_var('17.1', FILTER_VALIDATE_FLOAT)); // could be 17.1 + assertType('false', filter_var(null, FILTER_VALIDATE_FLOAT)); + + assertType('int|false', filter_var($bool, FILTER_VALIDATE_INT)); + assertType('1', filter_var(true, FILTER_VALIDATE_INT)); + assertType('false', filter_var(false, FILTER_VALIDATE_INT)); + assertType('int|false', filter_var($float, FILTER_VALIDATE_INT)); + assertType('17', filter_var(17.0, FILTER_VALIDATE_INT)); + assertType('false', filter_var(17.1, FILTER_VALIDATE_INT)); + assertType('int', filter_var($int, FILTER_VALIDATE_INT)); + assertType('int<0, 9>', filter_var($intRange, FILTER_VALIDATE_INT)); + assertType('17', filter_var(17, FILTER_VALIDATE_INT)); + assertType('int|false', filter_var($string, FILTER_VALIDATE_INT)); + assertType('int|false', filter_var($nonEmptyString, FILTER_VALIDATE_INT)); + assertType('17', filter_var('17', FILTER_VALIDATE_INT)); + assertType('false', filter_var('17.1', FILTER_VALIDATE_INT)); + assertType('false', filter_var(null, FILTER_VALIDATE_INT)); + + assertType("''|'1'", filter_var($bool)); + assertType("'1'", filter_var(true)); + assertType("''", filter_var(false)); + assertType('numeric-string', filter_var($float)); + assertType("'17'", filter_var(17.0)); + assertType("'17.1'", filter_var(17.1)); + assertType('numeric-string', filter_var($int)); + assertType('numeric-string', filter_var($intRange)); + assertType("'17'", filter_var(17)); + assertType('string', filter_var($string)); + assertType('non-empty-string', filter_var($nonEmptyString)); + assertType("'17'", filter_var('17')); + assertType("'17.1'", filter_var('17.1')); + assertType("''", filter_var(null)); + } + +} diff --git a/tests/PHPStan/Analyser/data/finite-types.php b/tests/PHPStan/Analyser/data/finite-types.php new file mode 100644 index 0000000000..c3e7e719a4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/finite-types.php @@ -0,0 +1,33 @@ +doFoo(...); - assertType('int', $f(1)); - assertType('string', $f('foo')); + assertType('1', $f(1)); + assertType('\'foo\'', $f('foo')); $g = \Closure::fromCallable([$this, 'doFoo']); - assertType('int', $g(1)); - assertType('string', $g('foo')); + assertType('1', $g(1)); + assertType('\'foo\'', $g('foo')); } public function doBaz() diff --git a/tests/PHPStan/Analyser/data/foreach-partially-non-iterable.php b/tests/PHPStan/Analyser/data/foreach-partially-non-iterable.php new file mode 100644 index 0000000000..ad8e375e58 --- /dev/null +++ b/tests/PHPStan/Analyser/data/foreach-partially-non-iterable.php @@ -0,0 +1,35 @@ +|false $a + */ + public function doFoo($a): void + { + foreach ($a as $k => $v) { + assertType('string', $k); + assertType('int', $v); + } + } + +} + +class Bar +{ + + public function sayHello(\stdClass $s): void + { + $a = null; + foreach ($s as $k => $v) { + $a .= 'test'; + } + assertType('(literal-string&non-falsy-string)|null', $a); + } + +} diff --git a/tests/PHPStan/Analyser/data/foreach/foreach-iterable-with-complex-value-type.php b/tests/PHPStan/Analyser/data/foreach/foreach-iterable-with-complex-value-type.php index 1d5f8df694..926e501576 100644 --- a/tests/PHPStan/Analyser/data/foreach/foreach-iterable-with-complex-value-type.php +++ b/tests/PHPStan/Analyser/data/foreach/foreach-iterable-with-complex-value-type.php @@ -1,6 +1,6 @@ doFluentUnionIterable() as $fluentUnionIterableBaz) { + die; + } +} diff --git a/tests/PHPStan/Analyser/data/generic-callables.php b/tests/PHPStan/Analyser/data/generic-callables.php new file mode 100644 index 0000000000..94bb3238a2 --- /dev/null +++ b/tests/PHPStan/Analyser/data/generic-callables.php @@ -0,0 +1,80 @@ +(TClosureRet $val): (TClosureRet|TFuncRet) + */ +function testFuncClosureMixed(mixed $mixed) +{ +} + +/** + * @template TFuncRet of mixed + * @param TFuncRet $mixed + * + * @return callable(): TFuncRet + */ +function testFuncCallable(mixed $mixed): callable +{ +} + +/** + * @param Closure(TRet $val): TRet $callable + * @param non-empty-list(TRet $val): TRet> $callables + */ +function testClosure(Closure $callable, int $int, string $str, array $callables): void +{ + assertType('Closure(TRet): TRet', $callable); + assertType('int', $callable($int)); + assertType('string', $callable($str)); + assertType('string', $callables[0]($str)); + assertType('Closure(): 1', testFuncClosure(1)); +} + +function testClosureMixed(int $int, string $str): void +{ + $closure = testFuncClosureMixed($int); + assertType('Closure(TClosureRet): (int|TClosureRet)', $closure); + assertType('int|string', $closure($str)); +} + +/** + * @param callable(TRet $val): TRet $callable + */ +function testCallable(callable $callable, int $int, string $str): void +{ + assertType('callable(TRet): TRet', $callable); + assertType('int', $callable($int)); + assertType('string', $callable($str)); + assertType('callable(): 1', testFuncCallable(1)); +} + +/** + * @param Closure(TRetFirst $valone): (Closure(TRetSecond $valtwo): (TRetFirst|TRetSecond)) $closure + */ +function testNestedClosures(Closure $closure, string $str, int $int): void +{ + assertType('Closure(TRetFirst): (Closure(TRetSecond $valtwo): (TRetFirst|TRetSecond))', $closure); + $closure1 = $closure($str); + assertType('Closure(TRetSecond): (string|TRetSecond)', $closure1); + $result = $closure1($int); + assertType('int|string', $result); +} diff --git a/tests/PHPStan/Analyser/data/generic-enum-class-string.php b/tests/PHPStan/Analyser/data/generic-enum-class-string.php index 71e8ebd8dc..5c4262bb3c 100644 --- a/tests/PHPStan/Analyser/data/generic-enum-class-string.php +++ b/tests/PHPStan/Analyser/data/generic-enum-class-string.php @@ -8,7 +8,7 @@ function testEnumExists(string $str) { assertType('string', $str); if (enum_exists($str)) { - assertType('class-string', $str); + assertType('class-string', $str); } } diff --git a/tests/PHPStan/Analyser/data/generic-generalization.php b/tests/PHPStan/Analyser/data/generic-generalization.php index 34280cdd25..dcbde64d5b 100644 --- a/tests/PHPStan/Analyser/data/generic-generalization.php +++ b/tests/PHPStan/Analyser/data/generic-generalization.php @@ -32,17 +32,17 @@ function testUnbounded( string $numericString, string $nonEmptyString ): void { - assertType('string', unbounded('hello')); - assertType('string', unbounded('stdClass')); + assertType('\'hello\'', unbounded('hello')); + assertType('\'stdClass\'', unbounded('stdClass')); assertType('class-string', unbounded($classString)); assertType('class-string', unbounded($genericClassString)); - assertType('string', unbounded(rand(0,1) === 1 ? 'hello' : $classString)); + assertType("'hello'|class-string", unbounded(rand(0,1) === 1 ? 'hello' : $classString)); - assertType('array{foo: int}', unbounded($arrayShape)); + assertType('array{foo: 42}', unbounded($arrayShape)); - assertType('string', unbounded($numericString)); - assertType('string', unbounded($nonEmptyString)); + assertType('numeric-string', unbounded($numericString)); + assertType('non-empty-string', unbounded($nonEmptyString)); } /** diff --git a/tests/PHPStan/Analyser/data/generic-method-tags.php b/tests/PHPStan/Analyser/data/generic-method-tags.php new file mode 100644 index 0000000000..92fdfaef5c --- /dev/null +++ b/tests/PHPStan/Analyser/data/generic-method-tags.php @@ -0,0 +1,25 @@ +(TVal $param) + * @method TVal doAnotherThing(int $param) + */ +class Test +{ + public function __call(): mixed + { + } +} + +function test(int $int, string $string): void +{ + $test = new Test(); + + assertType('int', $test->doThing($int)); + assertType('string', $test->doThing($string)); + assertType(TVal::class, $test->doAnotherThing($int)); +} diff --git a/tests/PHPStan/Analyser/data/generic-unions.php b/tests/PHPStan/Analyser/data/generic-unions.php index 4fe7aac9f5..da3d1bac83 100644 --- a/tests/PHPStan/Analyser/data/generic-unions.php +++ b/tests/PHPStan/Analyser/data/generic-unions.php @@ -54,9 +54,9 @@ public function foo( assertType('string|null', $this->doBar($nullableString)); - assertType('int', $this->doBaz(1)); - assertType('string', $this->doBaz('foo')); - assertType('float', $this->doBaz(1.2)); + assertType('1', $this->doBaz(1)); + assertType('\'foo\'', $this->doBaz('foo')); + assertType('1.2', $this->doBaz(1.2)); assertType('string', $this->doBaz($stringOrInt)); } @@ -114,22 +114,22 @@ function getWithDefaultCallable($key, $default = null) return $default; } -assertType('int|null', getWithDefault(3)); -assertType('int|null', getWithDefaultCallable(3)); -assertType('int|string', getWithDefault(3, 'foo')); -assertType('int|string', getWithDefaultCallable(3, 'foo')); -assertType('int|string', getWithDefault(3, function () { +assertType('3|null', getWithDefault(3)); +assertType('3|null', getWithDefaultCallable(3)); +assertType('3|\'foo\'', getWithDefault(3, 'foo')); +assertType('3|\'foo\'', getWithDefaultCallable(3, 'foo')); +assertType('3|\'foo\'', getWithDefault(3, function () { return 'foo'; })); -assertType('int|string', getWithDefaultCallable(3, function () { +assertType('3|\'foo\'', getWithDefaultCallable(3, function () { return 'foo'; })); -assertType('GenericUnions\Foo|int', getWithDefault(3, function () { +assertType('3|GenericUnions\Foo', getWithDefault(3, function () { return new Foo; })); -assertType('GenericUnions\Foo|int', getWithDefaultCallable(3, function () { +assertType('3|GenericUnions\Foo', getWithDefaultCallable(3, function () { return new Foo; })); -assertType('GenericUnions\Foo|int', getWithDefault(3, new Foo)); -assertType('GenericUnions\Foo|int', getWithDefaultCallable(3, new Foo)); -assertType('int|string', getWithDefaultCallable(3, new InvokableClass)); +assertType('3|GenericUnions\Foo', getWithDefault(3, new Foo)); +assertType('3|GenericUnions\Foo', getWithDefaultCallable(3, new Foo)); +assertType('3|string', getWithDefaultCallable(3, new InvokableClass)); diff --git a/tests/PHPStan/Analyser/data/generics-do-not-generalize.php b/tests/PHPStan/Analyser/data/generics-do-not-generalize.php new file mode 100644 index 0000000000..d00b8b699a --- /dev/null +++ b/tests/PHPStan/Analyser/data/generics-do-not-generalize.php @@ -0,0 +1,148 @@ + + */ +function test2($param): Foo +{ + +} + +/** @template T */ +class Foo +{ + + /** @param T $p */ + public function __construct($p) + { + + } + +} + +function (): void { + assertType('array<1>', test(1)); + assertType('GenericsDoNotGeneralize\Foo', test2(1)); + assertType('GenericsDoNotGeneralize\Foo', new Foo(1)); +}; + +class Test +{ + public const CONST_A = 1; + public const CONST_B = 2; + + /** + * @return self::CONST_* + */ + public static function foo(): int + { + return self::CONST_A; + } +} + +/** + * Produces a new array of elements by mapping each element in collection through a transformation function (callback). + * Callback arguments will be element, index, collection + * + * @template K of array-key + * @template V + * @template V2 + * + * @param iterable $collection + * @param callable(V,K,iterable):V2 $callback + * + * @return ($collection is list ? list : array) + * + * @no-named-arguments + */ +function map($collection, callable $callback) +{ + $aggregation = []; + + foreach ($collection as $index => $element) { + $aggregation[$index] = $callback($element, $index, $collection); + } + + return $aggregation; +} + +function (): void { + $foo = Test::foo(); + + assertType('1|2', $foo); + + $bar = map([new Test()], static fn(Test $test) => $test::foo()); + + assertType('list<1|2>', $bar); +}; + +function (): void { + /** @var list $a */ + $a = doFoo(); + + assertType('ArrayIterator', new ArrayIterator($a)); +}; + +/** + * @template K of array-key + * @template V + * @param array $a + * @return ArrayIterator + */ +function createArrayIterator(array $a): ArrayIterator +{ + +} + +function (): void { + /** @var list $a */ + $a = doFoo(); + + assertType('ArrayIterator', createArrayIterator($a)); +}; + +/** @template T */ +class FooInvariant +{ + + /** @param T $p */ + public function __construct($p) + { + + } + +} + +/** @template-covariant T */ +class FooCovariant +{ + + /** @param T $p */ + public function __construct($p) + { + + } + +} + +function (): void { + assertType('GenericsDoNotGeneralize\\FooInvariant', new FooInvariant(1)); + assertType('GenericsDoNotGeneralize\\FooCovariant<1>', new FooCovariant(1)); +}; diff --git a/tests/PHPStan/Analyser/data/generics.php b/tests/PHPStan/Analyser/data/generics.php index 45e522e954..b7731b35b9 100644 --- a/tests/PHPStan/Analyser/data/generics.php +++ b/tests/PHPStan/Analyser/data/generics.php @@ -97,7 +97,7 @@ function testD($int, $float, $intFloat) assertType('DateTime|int', d($int, new \DateTime())); assertType('DateTime|float|int', d($intFloat, new \DateTime())); assertType('array{}|DateTime', d([], new \DateTime())); - assertType('array{blabla: string}|DateTime', d(['blabla' => 'barrrr'], new \DateTime())); + assertType('array{blabla: \'barrrr\'}|DateTime', d(['blabla' => 'barrrr'], new \DateTime())); } /** @@ -131,7 +131,7 @@ function f($a, $b) { $result = []; assertType('array', $a); - assertType('callable(A (function PHPStan\Generics\FunctionsAssertType\f(), argument)): B (function PHPStan\Generics\FunctionsAssertType\f(), argument)', $b); + assertType('callable(A): B', $b); foreach ($a as $k => $v) { assertType('A (function PHPStan\Generics\FunctionsAssertType\f(), argument)', $v); $newV = $b($v); @@ -150,7 +150,7 @@ function testF($arrayOfInt, $callableOrNull) assertType('Closure(int): numeric-string', function (int $a): string { return (string)$a; }); - assertType('array', f($arrayOfInt, function (int $a): string { + assertType('array', f($arrayOfInt, function (int $a): string { return (string)$a; })); assertType('Closure(mixed): string', function ($a): string { @@ -763,7 +763,7 @@ function testClasses() $factory = new Factory(new \DateTime(), new A(1)); assertType( - 'array{DateTime, PHPStan\\Generics\\FunctionsAssertType\\A, string, PHPStan\\Generics\\FunctionsAssertType\\A}', + 'array{DateTime, PHPStan\\Generics\\FunctionsAssertType\\A, \'\', PHPStan\\Generics\\FunctionsAssertType\\A}', $factory->create(new \DateTime(), '', new A(new \DateTime())) ); } @@ -1176,6 +1176,7 @@ class PrefixedTemplateWins2 * @template T of Foo * @phpstan-template T of Bar * @psalm-template T of Baz + * @phan-template T of Quux */ class PrefixedTemplateWins3 { @@ -1209,12 +1210,25 @@ class PrefixedTemplateWins5 } +/** + * @phan-template T of Foo + * @phpstan-template T of Bar + */ +class PrefixedTemplateWins6 +{ + + /** @var T */ + public $name; + +} + function testPrefixed( PrefixedTemplateWins $a, PrefixedTemplateWins2 $b, PrefixedTemplateWins3 $c, PrefixedTemplateWins4 $d, - PrefixedTemplateWins5 $e + PrefixedTemplateWins5 $e, + PrefixedTemplateWins6 $f ) { assertType('PHPStan\Generics\FunctionsAssertType\Bar', $a->name); @@ -1222,6 +1236,7 @@ function testPrefixed( assertType('PHPStan\Generics\FunctionsAssertType\Bar', $c->name); assertType('PHPStan\Generics\FunctionsAssertType\Bar', $d->name); assertType('PHPStan\Generics\FunctionsAssertType\Bar', $e->name); + assertType('PHPStan\Generics\FunctionsAssertType\Bar', $f->name); }; /** @@ -1405,7 +1420,7 @@ function (\Throwable $e): void { function (): void { $array = ['a' => 1, 'b' => 2]; - assertType('array{a: int, b: int}', a($array)); + assertType('array{a: 1, b: 2}', a($array)); }; @@ -1545,7 +1560,7 @@ function (): void { assertType('array{\'a\', \'b\', \'c\'}', arrayBound2(range('a', 'c'))); assertType('array', arrayBound2([1, 2, 3])); assertType('array{true, false, true}', arrayBound3([true, false, true])); - assertType('array{array{a: \'a\'}, array{b: \'b\'}, array{c: \'c\'}}', arrayBound4([['a' => 'a'], ['b' => 'b'], ['c' => 'c']])); + assertType("array{array{a: 'a'}, array{b: 'b'}, array{c: 'c'}}", arrayBound4([['a' => 'a'], ['b' => 'b'], ['c' => 'c']])); assertType('array', arrayBound5(range('a', 'c'))); }; diff --git a/tests/PHPStan/Analyser/data/get-class-static-class.php b/tests/PHPStan/Analyser/data/get-class-static-class.php new file mode 100644 index 0000000000..e6e378c808 --- /dev/null +++ b/tests/PHPStan/Analyser/data/get-class-static-class.php @@ -0,0 +1,28 @@ +doBar()); + assertNativeType('string', $this->doBar()); + + assertType('string', $this->doBaz()); + assertNativeType('mixed', $this->doBaz()); + + assertType('non-empty-string', $this->doLorem()); + assertNativeType('string', $this->doLorem()); + } + + public function doBar(): string + { + + } + + /** + * @return string + */ + public function doBaz() + { + + } + + /** + * @return non-empty-string + */ + public function doLorem(): string + { + + } + +} diff --git a/tests/PHPStan/Analyser/data/getopt.php b/tests/PHPStan/Analyser/data/getopt.php index 142c986f57..453667cd51 100644 --- a/tests/PHPStan/Analyser/data/getopt.php +++ b/tests/PHPStan/Analyser/data/getopt.php @@ -6,4 +6,4 @@ use function PHPStan\Testing\assertType; $opts = getopt("ab:c::", ["longopt1", "longopt2:", "longopt3::"]); -assertType('(array|string|false>|false)', $opts); +assertType('(array|string|false>|false)', $opts); diff --git a/tests/PHPStan/Analyser/data/gettype.php b/tests/PHPStan/Analyser/data/gettype.php new file mode 100644 index 0000000000..a5b0d751ec --- /dev/null +++ b/tests/PHPStan/Analyser/data/gettype.php @@ -0,0 +1,54 @@ +, g: int<0, 255>, b: int<0, 255>, a: int<0, 1>}', $imagickPixel->getColor()); + assertType('array{r: int<0, 255>, g: int<0, 255>, b: int<0, 255>, a: int<0, 1>}', $imagickPixel->getColor(0)); + assertType('array{r: float, g: float, b: float, a: float}', $imagickPixel->getColor(1)); + assertType('array{r: int<0, 255>, g: int<0, 255>, b: int<0, 255>, a: int<0, 255>}', $imagickPixel->getColor(2)); + assertType('array{}', $imagickPixel->getColor(3)); +}; diff --git a/tests/PHPStan/Analyser/data/immediately-called-function-without-implicit-throw.neon b/tests/PHPStan/Analyser/data/immediately-called-function-without-implicit-throw.neon new file mode 100644 index 0000000000..790db39dd6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/immediately-called-function-without-implicit-throw.neon @@ -0,0 +1,3 @@ +parameters: + exceptions: + implicitThrows: false diff --git a/tests/PHPStan/Analyser/data/immediately-called-function-without-implicit-throw.php b/tests/PHPStan/Analyser/data/immediately-called-function-without-implicit-throw.php new file mode 100644 index 0000000000..6b2c380923 --- /dev/null +++ b/tests/PHPStan/Analyser/data/immediately-called-function-without-implicit-throw.php @@ -0,0 +1,30 @@ +noThrow(...), []); + } finally { + assertVariableCertainty(TrinaryLogic::createYes(), $result); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/impure-connection-fns.php b/tests/PHPStan/Analyser/data/impure-connection-fns.php new file mode 100644 index 0000000000..314b3e7bfb --- /dev/null +++ b/tests/PHPStan/Analyser/data/impure-connection-fns.php @@ -0,0 +1,15 @@ +', connection_status()); + } +} diff --git a/tests/PHPStan/Analyser/data/impure-error-log.php b/tests/PHPStan/Analyser/data/impure-error-log.php new file mode 100644 index 0000000000..082112b83b --- /dev/null +++ b/tests/PHPStan/Analyser/data/impure-error-log.php @@ -0,0 +1,14 @@ +fooProp = rand(0, 1); } + /** + * @return $this + */ + public function returnsThis($arg) + { + $this->fooProp = rand(0, 1); + } + + /** + * @return $this + * @phpstan-impure + */ + public function returnsThisImpure($arg) + { + $this->fooProp = rand(0, 1); + } + public function ordinaryMethod(): int { return 1; @@ -51,6 +72,24 @@ public function doFoo(): void assertType('int', $this->fooProp); } + public function doFluent(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->returnsThis(new stdClass()); + assertType('int', $this->fooProp); + } + + public function doFluent2(): void + { + $this->fooProp = 1; + assertType('1', $this->fooProp); + + $this->phpDocReturnThis(); + assertType('int', $this->fooProp); + } + public function doBar(): void { $this->fooProp = 1; @@ -79,3 +118,86 @@ public function doLorem(): void } } + +class Person +{ + + public function getName(): ?string + { + } + +} + +class Bar +{ + + public function doFoo(): void + { + $f = new Foo(); + + $p = new Person(); + assert($p->getName() !== null); + assertType('string', $p->getName()); + $f->returnsThis($p); + assertType('string', $p->getName()); + } + + public function doFoo2(): void + { + $f = new Foo(); + + $p = new Person(); + assert($p->getName() !== null); + assertType('string', $p->getName()); + $f->returnsThisImpure($p); + assertType('string|null', $p->getName()); + } + +} + +class ToBeExtended +{ + + /** @phpstan-pure */ + public function pure(): int + { + + } + + /** @phpstan-impure */ + public function impure(): int + { + echo 'test'; + return 1; + } + +} + +class ExtendingClass extends ToBeExtended +{ + + /** + * @return int + */ + public function pure(): int + { + echo 'test'; + return 1; + } + + /** + * @return int + */ + public function impure(): int + { + return 1; + } + +} + +function (ExtendingClass $e): void { + assert($e->pure() === 1); + assertType('1', $e->pure()); + $e->impure(); + assertType('int', $e->pure()); +}; diff --git a/tests/PHPStan/Analyser/data/in-array-enum.php b/tests/PHPStan/Analyser/data/in-array-enum.php new file mode 100644 index 0000000000..66ae579980 --- /dev/null +++ b/tests/PHPStan/Analyser/data/in-array-enum.php @@ -0,0 +1,77 @@ += 8.1 + +declare(strict_types=1); + +namespace InArrayEnum; + +use function PHPStan\Testing\assertType; + +enum FooUnitEnum +{ + case A; + case B; +} + +class Foo +{ + + /** + * @param array $strings + * @param array $ints + */ + public function nonConstantValues(FooUnitEnum $a, array $strings, array $ints): void + { + assertType('false', in_array($a, $strings, true)); + assertType('false', in_array($a, $strings, false)); + assertType('false', in_array($a, $strings)); + + assertType('bool', in_array($a->name, $strings, true)); + assertType('bool', in_array($a->name, $strings, false)); + assertType('bool', in_array($a->name, $strings)); + + assertType('false', in_array($a->name, $ints, true)); + assertType('bool', in_array($a->name, $ints, false)); + assertType('bool', in_array($a->name, $ints)); + } + + public function looseCheckEnumSpecifyNeedle(mixed $v): void + { + if (in_array($v, FooUnitEnum::cases())) { + assertType('InArrayEnum\FooUnitEnum::A|InArrayEnum\FooUnitEnum::B', $v); + + if (in_array($v, ['A', null, FooUnitEnum::B])) { + assertType('InArrayEnum\FooUnitEnum::B', $v); + } + } + + } + + /** @param array $haystack */ + public function looseCheckEnumSpecifyHaystack(array $haystack): void + { + if (! in_array(FooUnitEnum::A, $haystack)) { + assertType('array', $haystack); + } + + if (! in_array(rand() ? FooUnitEnum::A : FooUnitEnum::B, $haystack, true)) { + assertType('array', $haystack); + } + + if (! in_array(rand() ? 5 : 6, $haystack, true)) { + assertType('array', $haystack); + } + + if (! in_array(rand() ? 5 : rand(), $haystack, true)) { + assertType('array', $haystack); + } + } + + /** @param array $haystack */ + public function skipUnsafeLooseComparison(?FooUnitEnum $v, array $haystack): void + { + if (in_array($v, $haystack, false)) { + assertType('InArrayEnum\FooUnitEnum|null', $v); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/in-array-haystack-subtract.php b/tests/PHPStan/Analyser/data/in-array-haystack-subtract.php new file mode 100644 index 0000000000..ee1757521a --- /dev/null +++ b/tests/PHPStan/Analyser/data/in-array-haystack-subtract.php @@ -0,0 +1,18 @@ + $haystack */ + public function specifyHaystack(array $haystack): void + { + if (! in_array(rand() ? 5 : 6, $haystack, true)) { + assertType('array', $haystack); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/in-array-non-empty.php b/tests/PHPStan/Analyser/data/in-array-non-empty.php index c8f39ad4b2..999dbc8ff3 100644 --- a/tests/PHPStan/Analyser/data/in-array-non-empty.php +++ b/tests/PHPStan/Analyser/data/in-array-non-empty.php @@ -14,7 +14,7 @@ class HelloWorld public function sayHello(array $array): void { if(in_array("thing", $array, true)){ - assertType('non-empty-array', $array); + assertType('non-empty-list', $array); } } diff --git a/tests/PHPStan/Analyser/data/in-array.php b/tests/PHPStan/Analyser/data/in-array.php index ca8246d0d1..7b0cf403da 100644 --- a/tests/PHPStan/Analyser/data/in-array.php +++ b/tests/PHPStan/Analyser/data/in-array.php @@ -2,6 +2,8 @@ namespace InArrayTypeSpecifyingExtension; +use function PHPStan\Testing\assertType; + class Foo { @@ -41,7 +43,19 @@ public function doFoo( return; } - die; + assertType('\'bar\'|\'foo\'', $s); + assertType('string', $mixed); + assertType('string', $r); + assertType('\'foo\'', $fooOrBarOrBaz); + } + + /** @param array $strings */ + public function doBar(int $i, array $strings): void + { + assertType('bool', in_array($i, $strings)); + assertType('bool', in_array($i, $strings, false)); + assertType('false', in_array($i, $strings, true)); + assertType('false', in_array(1, $strings, true)); } } diff --git a/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-template.php b/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-template.php index e340baba85..087d2893af 100644 --- a/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-template.php +++ b/tests/PHPStan/Analyser/data/inherit-phpdoc-merging-template.php @@ -32,7 +32,7 @@ public function doFoo($a, $b) public function doBar() { - assertType('array|int', $this->doFoo(1, 'hahaha')); + assertType('1|array<\'hahaha\'>', $this->doFoo(1, 'hahaha')); } } @@ -75,7 +75,7 @@ public function doFoo($a, $b) public function doBar() { - assertType('array|int', $this->doFoo(1, 'hahaha')); + assertType('1|array', $this->doFoo(1, 'hahaha')); } } @@ -92,7 +92,7 @@ public function doFoo($a, $b) public function doBar() { - assertType('array|int', $this->doFoo(1, 'hahaha')); + assertType('1|array<\'hahaha\'>', $this->doFoo(1, 'hahaha')); } } diff --git a/tests/PHPStan/Analyser/data/ini-get.php b/tests/PHPStan/Analyser/data/ini-get.php new file mode 100644 index 0000000000..6751b3cc16 --- /dev/null +++ b/tests/PHPStan/Analyser/data/ini-get.php @@ -0,0 +1,29 @@ +', $r1 + $j); assertType('int<-2, 9>', $r1 - $j); assertType('int<1, 30>', $r1 * $j); - assertType('float|int<0, 10>', $r1 / $j); + assertType('float|int<1, 10>', $r1 / $j); assertType('int', $rMin * $j); assertType('int<5, max>', $rMax * $j); assertType('int<2, 13>', $j + $r1); assertType('int<-9, 2>', $j - $r1); assertType('int<1, 30>', $j * $r1); - assertType('float|int<0, 3>', $j / $r1); + assertType('float|int<1, 3>', $j / $r1); assertType('int', $j * $rMin); assertType('int<5, max>', $j * $rMax); assertType('int<-19, -10>|int<2, 13>', $r1 + $z); assertType('int<-2, 9>|int<21, 30>', $r1 - $z); assertType('int<-200, -20>|int<1, 30>', $r1 * $z); - assertType('float|int<0, 10>', $r1 / $z); + assertType('float|int<1, 10>', $r1 / $z); assertType('int', $rMin * $z); assertType('int|int<5, max>', $rMax * $z); assertType('int<2, max>', $pi + 1); assertType('int<-1, max>', $pi - 2); assertType('int<2, max>', $pi * 2); - assertType('float|int<0, max>', $pi / 2); + assertType('float|int<1, max>', $pi / 2); assertType('int<2, max>', 1 + $pi); assertType('int', 2 - $pi); assertType('int<2, max>', 2 * $pi); - assertType('float|int<2, max>', 2 / $pi); + assertType('float|int<1, 2>', 2 / $pi); assertType('int<5, 14>', $r1 + 4); assertType('int<-3, 6>', $r1 - 4); assertType('int<4, 40>', $r1 * 4); - assertType('float|int<0, 2>', $r1 / 4); + assertType('float|int<1, 2>', $r1 / 4); assertType('int<9, max>', $rMax + 4); assertType('int<1, max>', $rMax - 4); assertType('int<20, max>', $rMax * 4); - assertType('float|int<1, max>', $rMax / 4); + assertType('float|int<2, max>', $rMax / 4); assertType('int<6, 20>', $r1 + $r2); assertType('int<-9, 5>', $r1 - $r2); assertType('int<5, 100>', $r1 * $r2); - assertType('float|int<0, 1>', $r1 / $r2); + assertType('float|int<1, 2>', $r1 / $r2); assertType('int<-99, 19>', $r1 - $r3); assertType('int', $r1 + $rMin); assertType('int<-4, max>', $r1 - $rMin); assertType('int', $r1 * $rMin); - assertType('float|int', $r1 / $rMin); + assertType('float|int<-10, -1>|int<1, 10>', $r1 / $rMin); assertType('int', $rMin + $r1); assertType('int', $rMin - $r1); assertType('int', $rMin * $r1); - assertType('float|int', $rMin / $r1); + assertType('float|int', $rMin / $r1); assertType('int<6, max>', $r1 + $rMax); assertType('int', $r1 - $rMax); assertType('int<5, max>', $r1 * $rMax); - assertType('float|int<0, max>', $r1 / $rMax); + assertType('float|int<1, 2>', $r1 / $rMax); assertType('int<6, max>', $rMax + $r1); assertType('int<-5, max>', $rMax - $r1); assertType('int<5, max>', $rMax * $r1); - assertType('float|int<5, max>', $rMax / $r1); + assertType('float|int<1, max>', $rMax / $r1); assertType('5|10|15|20|30', $x / $y); - assertType('float|int<0, max>', $rMax / $rMax); + assertType('float|int<1, max>', $rMax / $rMax); assertType('(float|int)', $rMin / $rMin); } @@ -307,8 +307,8 @@ public function maximaInversion($rMin, $rMax) { assertType('int<-5, max>', $rMin * -1); assertType('int', $rMax * -2); - assertType('float|int<0, max>', -1 / $rMin); - assertType('float|int', -2 / $rMax); + assertType('-1|1|float', -1 / $rMin); + assertType('float', -2 / $rMax); assertType('float|int<-5, max>', $rMin / -1); assertType('float|int', $rMax / -2); @@ -330,4 +330,29 @@ public function unaryMinus($r1, $r2, $rMin, $rMax, $rZero) { assertType('int<-50, 0>', -$rZero); } + /** + * @param int<-1, 2> $p + * @param int<-1, 2> $u + */ + public function sayHello($p, $u): void + { + assertType('int<-2, 4>', $p + $u); + assertType('int<-3, 3>', $p - $u); + assertType('int<-2, 4>', $p * $u); + assertType('float|int<-2, 2>', $p / $u); + } + + /** + * @param int<0, max> $positive + * @param int $negative + */ + public function zeroIssues($positive, $negative) + { + assertType('0', 0 * $positive); + assertType('int<0, max>', $positive * $positive); + assertType('0', 0 * $negative); + assertType('int<0, max>', $negative * $negative); + assertType('int', $negative * $positive); + } + } diff --git a/tests/PHPStan/Analyser/data/invalid-type-aliases.php b/tests/PHPStan/Analyser/data/invalid-type-aliases.php new file mode 100644 index 0000000000..5b91643af1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/invalid-type-aliases.php @@ -0,0 +1,22 @@ +returnsAlias()); + } + + /** @psalm-return MyObject */ + public function returnsAlias() + { + + } +} diff --git a/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-post-81.php b/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-post-81.php index 405b2a7a10..f1149f5f84 100644 --- a/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-post-81.php +++ b/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-post-81.php @@ -1,5 +1,7 @@ ', iterator_to_array($foo, false)); + assertType('list', iterator_to_array($foo, false)); } } diff --git a/tests/PHPStan/Analyser/data/json-validate.php b/tests/PHPStan/Analyser/data/json-validate.php new file mode 100644 index 0000000000..4687970f5d --- /dev/null +++ b/tests/PHPStan/Analyser/data/json-validate.php @@ -0,0 +1,19 @@ + $a * @return void */ - public function doFoo(array $a, string $key): void + public function doFoo(array $a, string $key, int $anotherKey): void { assertType('false', key_exists(2, $a)); assertType('bool', key_exists('foo', $a)); @@ -19,11 +19,94 @@ public function doFoo(array $a, string $key): void $a = ['foo' => 2, 3 => 'bar']; assertType('true', key_exists('foo', $a)); + assertType('true', key_exists(3, $a)); assertType('true', key_exists('3', $a)); assertType('false', key_exists(4, $a)); + if (key_exists($key, $a)) { + assertType("'3'|'foo'", $key); + } + if (key_exists($anotherKey, $a)) { + assertType('3', $anotherKey); + } + $empty = []; assertType('false', key_exists('foo', $empty)); assertType('false', key_exists($key, $empty)); } + + /** + * @param array $a + * @param array $b + * @param array $c + * @param array-key $key4 + * + * @return void + */ + public function doBar(array $a, array $b, array $c, int $key1, string $key2, int|string $key3, $key4, mixed $key5): void + { + if (key_exists($key1, $a)) { + assertType('int', $key1); + } + if (key_exists($key2, $a)) { + assertType('numeric-string', $key2); + } + if (key_exists($key3, $a)) { + assertType('int|numeric-string', $key3); + } + if (key_exists($key4, $a)) { + assertType('(int|numeric-string)', $key4); + } + if (key_exists($key5, $a)) { + assertType('int|numeric-string', $key5); + } + + if (key_exists($key1, $b)) { + assertType('*NEVER*', $key1); + } + if (key_exists($key2, $b)) { + assertType('string', $key2); + } + if (key_exists($key3, $b)) { + assertType('string', $key3); + } + if (key_exists($key4, $b)) { + assertType('string', $key4); + } + if (key_exists($key5, $b)) { + assertType('string', $key5); + } + + if (key_exists($key1, $c)) { + assertType('int', $key1); + } + if (key_exists($key2, $c)) { + assertType('string', $key2); + } + if (key_exists($key3, $c)) { + assertType('(int|string)', $key3); + } + if (key_exists($key4, $c)) { + assertType('(int|string)', $key4); + } + if (key_exists($key5, $c)) { + assertType('(int|string)', $key5); + } + + if (key_exists($key1, [3 => 'foo', 4 => 'bar'])) { + assertType('3|4', $key1); + } + if (key_exists($key2, [3 => 'foo', 4 => 'bar'])) { + assertType("'3'|'4'", $key2); + } + if (key_exists($key3, [3 => 'foo', 4 => 'bar'])) { + assertType("3|4|'3'|'4'", $key3); + } + if (key_exists($key4, [3 => 'foo', 4 => 'bar'])) { + assertType("(3|4|'3'|'4')", $key4); + } + if (key_exists($key5, [3 => 'foo', 4 => 'bar'])) { + assertType("3|4|'3'|'4'", $key5); + } + } } diff --git a/tests/PHPStan/Analyser/data/list-count.php b/tests/PHPStan/Analyser/data/list-count.php new file mode 100644 index 0000000000..75eedbd4e4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/list-count.php @@ -0,0 +1,24 @@ + $items + */ +function foo(array $items) { + assertType('list', $items); + if (count($items) === 3) { + assertType('array{int, int, int}', $items); + array_shift($items); + assertType('array{int, int}', $items); + } elseif (count($items) === 0) { + assertType('array{}', $items); + } elseif (count($items) === 5) { + assertType('array{int, int, int, int, int}', $items); + } else { + assertType('non-empty-list', $items); + } + assertType('list', $items); +} diff --git a/tests/PHPStan/Analyser/data/list-shapes.php b/tests/PHPStan/Analyser/data/list-shapes.php new file mode 100644 index 0000000000..62313ca8e7 --- /dev/null +++ b/tests/PHPStan/Analyser/data/list-shapes.php @@ -0,0 +1,26 @@ +', $list); + assertType('list', $list); } /** @param list $list */ public function directAssertionParamHint(array $list): void { - assertType('array', $list); + assertType('list', $list); } /** @param list $list */ public function directAssertionNullableParamHint(array $list = null): void { - assertType('array|null', $list); + assertType('list|null', $list); } /** @param list<\DateTime> $list */ public function directAssertionObjectParamHint($list): void { - assertType('array', $list); + assertType('list', $list); } public function withoutGenerics(): void @@ -37,7 +37,7 @@ public function withoutGenerics(): void $list[] = '1'; $list[] = true; $list[] = new \stdClass(); - assertType('non-empty-array', $list); + assertType('non-empty-list', $list); } @@ -48,7 +48,7 @@ public function withMixedType(): void $list[] = '1'; $list[] = true; $list[] = new \stdClass(); - assertType('non-empty-array', $list); + assertType('non-empty-list', $list); } public function withObjectType(): void @@ -56,7 +56,7 @@ public function withObjectType(): void /** @var list<\DateTime> $list */ $list = []; $list[] = new \DateTime(); - assertType('non-empty-array', $list); + assertType('non-empty-list', $list); } /** @return list */ @@ -66,7 +66,7 @@ public function withScalarGoodContent(): void $list = []; $list[] = '1'; $list[] = true; - assertType('non-empty-array', $list); + assertType('non-empty-list', $list); } public function withNumericKey(): void @@ -75,7 +75,7 @@ public function withNumericKey(): void $list = []; $list[] = '1'; $list['1'] = true; - assertType('non-empty-array&hasOffsetValue(1, true)', $list); + assertType('non-empty-array, mixed>&hasOffsetValue(1, true)', $list); } public function withFullListFunctionality(): void @@ -83,15 +83,27 @@ public function withFullListFunctionality(): void // These won't output errors for now but should when list type will be fully implemented /** @var list $list */ $list = []; + assertType('list', $list); $list[] = '1'; + assertType('non-empty-list', $list); $list[] = '2'; + assertType('non-empty-list', $list); unset($list[0]);//break list behaviour - assertType('array|int<1, max>, mixed>', $list); + assertType('array, mixed>', $list); /** @var list $list2 */ $list2 = []; + assertType('list', $list2); $list2[2] = '1';//Most likely to create a gap in indexes - assertType('non-empty-array&hasOffsetValue(2, \'1\')', $list2); + assertType('non-empty-array, mixed>&hasOffsetValue(2, \'1\')', $list2); + } + + /** @param list $list */ + public function testUnset(array $list): void + { + assertType('list', $list); + unset($list[2]); + assertType('array|int<3, max>, int>', $list); } } diff --git a/tests/PHPStan/Analyser/data/literal-string.php b/tests/PHPStan/Analyser/data/literal-string.php index 2f7bda2f01..c2e3cdfd63 100644 --- a/tests/PHPStan/Analyser/data/literal-string.php +++ b/tests/PHPStan/Analyser/data/literal-string.php @@ -57,7 +57,7 @@ public function increment($literalString, string $string) assertType('literal-string', $literalString); $string++; - assertType('string', $string); + assertType('(float|int|string)', $string); } } diff --git a/tests/PHPStan/Analyser/data/loose-const-comparison-php7.php b/tests/PHPStan/Analyser/data/loose-const-comparison-php7.php new file mode 100644 index 0000000000..9a0c544df1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/loose-const-comparison-php7.php @@ -0,0 +1,20 @@ + assertType('int', $value), + 'string' => assertType('string', $value), + }; + } + + public function doGettypeUnion(int|float|bool|string|object|array $value): void + { + $intOrString = 'integer'; + if (rand(0, 1)) { + $intOrString = 'string'; + } + match (gettype($value)) { + $intOrString => assertType('int|string', $value), + }; + } + +} + +final class FinalFoo +{ + +} + +final class FinalBar +{ + +} + +class TestGetClass +{ + + public function doMatch(FinalFoo|FinalBar $class): void + { + match (get_class($class)) { + FinalFoo::class => assertType(FinalFoo::class, $class), + FinalBar::class => assertType(FinalBar::class, $class), + }; + } + } diff --git a/tests/PHPStan/Analyser/data/mb-strlen-php72.php b/tests/PHPStan/Analyser/data/mb-strlen-php72.php index 17d8b708c5..4e9c20f4cd 100644 --- a/tests/PHPStan/Analyser/data/mb-strlen-php72.php +++ b/tests/PHPStan/Analyser/data/mb-strlen-php72.php @@ -1,6 +1,6 @@ -', mb_strlen($bool)); + assertType('int<1, max>', mb_strlen($i)); + assertType('int<0, max>', mb_strlen($s)); + assertType('int<1, max>', mb_strlen($nonEmpty)); + assertType('int<1, 2>', mb_strlen($constUnion)); + assertType('int<0, 4>', mb_strlen($constUnionMixed)); + assertType('3', mb_strlen(123)); + assertType('1', mb_strlen(true)); + assertType('0', mb_strlen(false)); + assertType('0', mb_strlen(null)); + assertType('1', mb_strlen(1.0)); + assertType('4', mb_strlen(1.23)); + assertType('int<1, max>', mb_strlen($float)); + assertType('int<1, max>', mb_strlen($intFloat)); + assertType('int<1, max>', mb_strlen($nonEmptyStringIntFloat)); + assertType('0', mb_strlen($emptyStringFalseNull)); + assertType('int<0, 1>', mb_strlen($emptyStringBoolNull)); + assertType('8', mb_strlen('паляниця', 'utf-8')); + assertType('11', mb_strlen('alias test🤔', 'utf8')); + assertType('*NEVER*', mb_strlen('', 'invalid encoding')); + assertType('int<5, 6>', mb_strlen('école', $utf8And8bit)); + assertType('5', mb_strlen('école', $utf8AndInvalidEncoding)); + assertType('1|3|5|6', mb_strlen('école', $unknownEncoding)); + assertType('2|4|5|8', mb_strlen('מזגן', $unknownEncoding)); + assertType('6|8|12|13|15|16|24', mb_strlen('いい天気ですね〜', $unknownEncoding)); + assertType('3', mb_strlen(123, $utf8AndInvalidEncoding)); + assertType('*NEVER*', mb_strlen('foo', $encodingsValidOnlyUntilPhp72)); + } + +} diff --git a/tests/PHPStan/Analyser/data/memcache-get.php b/tests/PHPStan/Analyser/data/memcache-get.php new file mode 100644 index 0000000000..735e85b545 --- /dev/null +++ b/tests/PHPStan/Analyser/data/memcache-get.php @@ -0,0 +1,14 @@ +get("key1")); + assertType('array|false', $memcache->get(array("key1", "key2", "key3"))); +}; diff --git a/tests/PHPStan/Analyser/data/method-phpDocs-inheritdoc-without-curly-braces.php b/tests/PHPStan/Analyser/data/method-phpDocs-inheritdoc-without-curly-braces.php index c0dff8a645..254d321d7f 100644 --- a/tests/PHPStan/Analyser/data/method-phpDocs-inheritdoc-without-curly-braces.php +++ b/tests/PHPStan/Analyser/data/method-phpDocs-inheritdoc-without-curly-braces.php @@ -5,7 +5,7 @@ use SomeNamespace\Amet as Dolor; use SomeNamespace\Consecteur; -class FooInheritDocChild extends Foo +class FooInheritDocChildWithoutCurly extends Foo { /** diff --git a/tests/PHPStan/Analyser/data/methodPhpDocs-phanPrefix.php b/tests/PHPStan/Analyser/data/methodPhpDocs-phanPrefix.php new file mode 100644 index 0000000000..5a44bfa784 --- /dev/null +++ b/tests/PHPStan/Analyser/data/methodPhpDocs-phanPrefix.php @@ -0,0 +1,174 @@ +doFluentUnionIterable() as $fluentUnionIterableBaz) { + die; + } + } + + /** + * @phan-return self[] + */ + public function doBar(): array + { + + } + + public function returnParent(): parent + { + + } + + /** + * @phan-return parent + */ + public function returnPhpDocParent() + { + + } + + /** + * @phan-return NULL[] + */ + public function returnNulls(): array + { + + } + + public function returnObject(): object + { + + } + + public function phpDocVoidMethod(): self + { + + } + + public function phpDocVoidMethodFromInterface(): self + { + + } + + public function phpDocVoidParentMethod(): self + { + + } + + public function phpDocWithoutCurlyBracesVoidParentMethod(): self + { + + } + + /** + * @phan-return string[] + */ + public function returnsStringArray(): array + { + + } + + private function privateMethodWithPhpDoc() + { + + } + +} diff --git a/tests/PHPStan/Analyser/data/minmax-arrays.php b/tests/PHPStan/Analyser/data/minmax-arrays.php index e84c924436..1a68d50b56 100644 --- a/tests/PHPStan/Analyser/data/minmax-arrays.php +++ b/tests/PHPStan/Analyser/data/minmax-arrays.php @@ -109,44 +109,19 @@ function dummy4(\DateTimeInterface $dateA, ?\DateTimeInterface $dateB): void assertType('DateTimeInterface|false', max(array_filter([$dateB]))); } -function dummy5(int $i, int $j): void -{ - assertType('array{0?: int|int<1, max>, 1?: int|int<1, max>}', array_filter([$i, $j])); - assertType('array{1: true}', array_filter([false, true])); -} - -function dummy6(string $s, string $t): void { - assertType('array{0?: non-falsy-string, 1?: non-falsy-string}', array_filter([$s, $t])); -} - class HelloWorld { - public function setRange(int $range): void - { - if ($range < 0) { - return; - } - assertType('int<0, 100>', min($range, 100)); - assertType('int<0, 100>', min(100, $range)); - } - - public function setRange2(int $range): void - { - if ($range > 100) { - return; - } - assertType('int<0, 100>', max($range, 0)); - assertType('int<0, 100>', max(0, $range)); - } - - public function boundRange(): void + public function unionType(): void { /** - * @var int<1, 6> $range + * @var array<0|1|2|3|4|5|6|7|8|9> */ - $range = getFoo(); + $numbers = getFoo(); + + assertType('0|1|2|3|4|5|6|7|8|9|false', min($numbers)); + assertType('0', min([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])); - assertType('int<1, 4>', min($range, 4)); - assertType('int<4, 6>', max(4, $range)); + assertType('0|1|2|3|4|5|6|7|8|9|false', max($numbers)); + assertType('9', max([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])); } } diff --git a/tests/PHPStan/Analyser/data/minmax-php8.php b/tests/PHPStan/Analyser/data/minmax-php8.php new file mode 100644 index 0000000000..3d738d4c38 --- /dev/null +++ b/tests/PHPStan/Analyser/data/minmax-php8.php @@ -0,0 +1,128 @@ + 0) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } + if (count($ints) >= 1) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } + if (count($ints) >= 2) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) <= 0) { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) < 1) { + assertType('*ERROR*', min($ints)); + assertType('*ERROR*', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } + if (count($ints) < 2) { + assertType('int', min($ints)); + assertType('int', max($ints)); + } else { + assertType('int', min($ints)); + assertType('int', max($ints)); + } +} + +/** + * @param int[] $ints + */ +function dummy3(array $ints): void +{ + assertType('int', min($ints)); + assertType('int', max($ints)); +} + + +function dummy4(\DateTimeInterface $dateA, ?\DateTimeInterface $dateB): void +{ + assertType('array{0: DateTimeInterface, 1?: DateTimeInterface}', array_filter([$dateA, $dateB])); + assertType('DateTimeInterface', min(array_filter([$dateA, $dateB]))); + assertType('DateTimeInterface', max(array_filter([$dateA, $dateB]))); + assertType('array{0?: DateTimeInterface}', array_filter([$dateB])); + assertType('DateTimeInterface', min(array_filter([$dateB]))); + assertType('DateTimeInterface', max(array_filter([$dateB]))); +} + + +class HelloWorld +{ + public function unionType(): void + { + /** + * @var array<0|1|2|3|4|5|6|7|8|9> + */ + $numbers = getFoo(); + + assertType('0|1|2|3|4|5|6|7|8|9', min($numbers)); + assertType('0', min([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])); + + assertType('0|1|2|3|4|5|6|7|8|9', max($numbers)); + assertType('9', max([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])); + } +} diff --git a/tests/PHPStan/Analyser/data/minmax.php b/tests/PHPStan/Analyser/data/minmax.php new file mode 100644 index 0000000000..d4cbb77c44 --- /dev/null +++ b/tests/PHPStan/Analyser/data/minmax.php @@ -0,0 +1,66 @@ +|int<1, max>, 1?: int|int<1, max>}', array_filter([$i, $j])); + assertType('array{1: true}', array_filter([false, true])); +} + +function dummy6(string $s, string $t): void { + assertType('array{0?: non-falsy-string, 1?: non-falsy-string}', array_filter([$s, $t])); +} + +class HelloWorld +{ + public function setRange(int $range): void + { + if ($range < 0) { + return; + } + assertType('int<0, 100>', min($range, 100)); + assertType('int<0, 100>', min(100, $range)); + } + + public function setRange2(int $range): void + { + if ($range > 100) { + return; + } + assertType('int<0, 100>', max($range, 0)); + assertType('int<0, 100>', max(0, $range)); + } + + public function boundRange(): void + { + /** + * @var int<1, 6> $range + */ + $range = getFoo(); + + assertType('int<1, 4>', min($range, 4)); + assertType('int<4, 6>', max(4, $range)); + } + + public function unionType(): void + { + /** + * @var array{0, 1, 2}|array{4, 5, 6} $numbers2 + */ + $numbers2 = getFoo(); + + assertType('0|4', min($numbers2)); + assertType('2|6', max($numbers2)); + } +} diff --git a/tests/PHPStan/Analyser/data/mixed-to-number.php b/tests/PHPStan/Analyser/data/mixed-to-number.php new file mode 100644 index 0000000000..97cc9d0842 --- /dev/null +++ b/tests/PHPStan/Analyser/data/mixed-to-number.php @@ -0,0 +1,47 @@ +|int<1, max>|non-falsy-string|true', $nonEmptyScalar); + assertType("0|0.0|''|'0'|false", $emptyScalar); + assertType("mixed~0|0.0|''|'0'|array{}|false|null", $nonEmptyMixed); + } + +} diff --git a/tests/PHPStan/Analyser/data/mysqli-affected-rows.php b/tests/PHPStan/Analyser/data/mysqli-affected-rows.php new file mode 100644 index 0000000000..6f227da9d6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/mysqli-affected-rows.php @@ -0,0 +1,15 @@ +query('UPDATE x SET y = 0;'); + assertType('int<-1, max>|numeric-string', $mysqli->affected_rows); + } +} diff --git a/tests/PHPStan/Analyser/data/mysqli-result-num-rows.php b/tests/PHPStan/Analyser/data/mysqli-result-num-rows.php new file mode 100644 index 0000000000..14aa92bd6b --- /dev/null +++ b/tests/PHPStan/Analyser/data/mysqli-result-num-rows.php @@ -0,0 +1,15 @@ +query('SELECT x FROM z;'); + assertType('int<0, max>|numeric-string', $mysqliResult->num_rows); + } +} diff --git a/tests/PHPStan/Analyser/data/mysqli-stmt-affected-rows-and-num-rows.php b/tests/PHPStan/Analyser/data/mysqli-stmt-affected-rows-and-num-rows.php new file mode 100644 index 0000000000..1ab625db4f --- /dev/null +++ b/tests/PHPStan/Analyser/data/mysqli-stmt-affected-rows-and-num-rows.php @@ -0,0 +1,20 @@ +prepare('SELECT x FROM z;'); + $stmt->execute(); + assertType('int<0, max>|numeric-string', $stmt->num_rows); + + $stmt = $mysqli->prepare('DELETE FROM z;'); + $stmt->execute(); + assertType('int<-1, max>|numeric-string', $stmt->affected_rows); + } +} diff --git a/tests/PHPStan/Analyser/data/mysqli_fetch_object.php b/tests/PHPStan/Analyser/data/mysqli_fetch_object.php new file mode 100644 index 0000000000..ceb0c6c78d --- /dev/null +++ b/tests/PHPStan/Analyser/data/mysqli_fetch_object.php @@ -0,0 +1,16 @@ +fetch_object()); + assertType('MysqliFetchObject\MyClass|false|null', $result->fetch_object(MyClass::class)); + + assertType('stdClass|false|null', mysqli_fetch_object($result)); + assertType('MysqliFetchObject\MyClass|false|null', mysqli_fetch_object($result, MyClass::class)); +} + +class MyClass {} + diff --git a/tests/PHPStan/Analyser/data/native-expressions.php b/tests/PHPStan/Analyser/data/native-expressions.php new file mode 100644 index 0000000000..72f5a47e7e --- /dev/null +++ b/tests/PHPStan/Analyser/data/native-expressions.php @@ -0,0 +1,56 @@ +|non-empty-string', $a); + assertNativeType('int|string', $a); + if (is_string($a)) { + assertType('non-empty-string', $a); + assertNativeType('string', $a); + } +} + +class Foo{ + public function __construct( + /** @var non-empty-array */ + private array $array + ){ + assertType('non-empty-array', $this->array); + assertNativeType('array', $this->array); + if(count($array) === 0){ + throw new \InvalidArgumentException(); + } + } + + /** + * @param array{a: 'b'} $a + * @return void + */ + public function doUnset(array $a){ + assertType("array{a: 'b'}", $a); + assertNativeType('array', $a); + unset($a['a']); + assertType("array{}", $a); + assertNativeType("array", $a); + } +} + diff --git a/tests/PHPStan/Analyser/data/native-types-first-class-callables.php b/tests/PHPStan/Analyser/data/native-types-first-class-callables.php new file mode 100644 index 0000000000..35b693f916 --- /dev/null +++ b/tests/PHPStan/Analyser/data/native-types-first-class-callables.php @@ -0,0 +1,87 @@ += 8.1 + +namespace NativeTypesFirstClassCallables; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** @return non-empty-string */ + public function doFoo(): string + { + + } + + /** @return non-empty-string */ + public static function doBar(): string + { + + } + +} + +/** @return non-empty-string */ +function doFooFunction(): string +{ + +} + +class Test +{ + + public function doFoo(): void + { + $foo = new Foo(); + $f = $foo->doFoo(...); + assertType('non-empty-string', $f()); + assertNativeType('string', $f()); + assertType('non-empty-string', ($foo->doFoo(...))()); + assertNativeType('string', ($foo->doFoo(...))()); + + $g = Foo::doBar(...); + assertType('non-empty-string', $g()); + assertNativeType('string', $g()); + + $h = doFooFunction(...); + assertType('non-empty-string', $h()); + assertNativeType('string', $h()); + + $i = $h(...); + assertType('non-empty-string', $i()); + assertNativeType('string', $i()); + + $j = [Foo::class, 'doBar'](...); + assertType('non-empty-string', $j()); + assertNativeType('string', $j()); + } + +} + +class Nullsafe +{ + + /** @var int */ + private $untyped; + + private int $typed; + + /** @return non-empty-string */ + public function doFoo(): string + { + + } + + public function doBar(?self $self): void + { + assertType('non-empty-string|null', $self?->doFoo()); + assertNativeType('string|null', $self?->doFoo()); + + assertType('int|null', $self?->untyped); + assertNativeType('mixed', $self?->untyped); + assertType('int|null', $self?->typed); + assertNativeType('int|null', $self?->typed); + } + +} diff --git a/tests/PHPStan/Analyser/data/native-types-ftp-connect-resource.php b/tests/PHPStan/Analyser/data/native-types-ftp-connect-resource.php new file mode 100644 index 0000000000..0952898880 --- /dev/null +++ b/tests/PHPStan/Analyser/data/native-types-ftp-connect-resource.php @@ -0,0 +1,18 @@ += 7.4 namespace NativeTypes; @@ -170,6 +170,70 @@ public function doIfElse(\DateTimeInterface $date): void } } + public function declareStrictTypes(array $array): void + { + /** @var array $array */ + assertType('array', $array); + assertNativeType('array', $array); + + declare(strict_types=1); + assertType('array', $array); + assertNativeType('array', $array); + } + + public function arrowFunction(array $array): void + { + /** @var array $array */ + assertType('array', $array); + assertNativeType('array', $array); + + (fn () => assertNativeType('array', $array))(); + } + + public function closuresUsingCallMethod(array $array, object $object): void + { + /** @var \stdClass $object */ + assertType('$this(NativeTypes\Foo)', $this); + assertNativeType('$this(NativeTypes\Foo)', $this); + + /** @var array $array */ + assertType('array', $array); + assertNativeType('array', $array); + + (function () use ($array) { + assertType('stdClass', $this); + assertNativeType('object', $this); + + assertType('array', $array); + assertNativeType('array', $array); + })->call($object); + + assertType('$this(NativeTypes\Foo)', $this); + assertNativeType('$this(NativeTypes\Foo)', $this); + } + + public function closureBind(array $array, object $object): void + { + /** @var \stdClass $object */ + assertType('$this(NativeTypes\Foo)', $this); + assertNativeType('$this(NativeTypes\Foo)', $this); + + /** @var array $array */ + assertType('array', $array); + assertNativeType('array', $array); + + \Closure::bind(function () use ($array) { + assertType('stdClass', $this); + assertNativeType('object', $this); + + assertType('array', $array); + assertNativeType('array', $array); + }, $object); + + assertType('$this(NativeTypes\Foo)', $this); + assertNativeType('$this(NativeTypes\Foo)', $this); + } + } /** @@ -202,3 +266,131 @@ function fooFunction( assertType('string', $nonNullableString); assertNativeType('string', $nonNullableString); } + +function phpDocDoesNotInfluenceExistingNativeType(): void +{ + $array = []; + + assertType('array{}', $array); + assertNativeType('array{}', $array); + + /** @var array $array */ + assertType('array', $array); + assertNativeType('array{}', $array); +} + +class NativeStaticCall +{ + + public function doFoo() + { + assertType('non-empty-string', self::doBar()); + assertNativeType('string', self::doBar()); + + $s = new self(); + assertType('non-empty-string', $s::doBar()); + assertNativeType('string', $s::doBar()); + } + + /** @return non-empty-string */ + public static function doBar(): string + { + + } + +} + +class TypedProperties +{ + + /** @var int */ + private $untyped; + + private int $typed; + + /** @var int */ + private static $untypedStatic; + + private static int $typedStatic; + + public function doFoo(): void + { + assertType('int', $this->untyped); + assertNativeType('mixed', $this->untyped); + assertType('int', $this->typed); + assertNativeType('int', $this->typed); + assertType('int', self::$untypedStatic); + assertNativeType('mixed', self::$untypedStatic); + assertType('int', self::$typedStatic); + assertNativeType('int', self::$typedStatic); + } + +} + +/** @return non-empty-string */ +function funcWithANativeReturnType(): string +{ + +} + +class TestFuncWithANativeReturnType +{ + + public function doFoo(): void + { + assertType('non-empty-string', funcWithANativeReturnType()); + assertNativeType('string', funcWithANativeReturnType()); + + $f = function (): string { + return funcWithANativeReturnType(); + }; + + assertType('non-empty-string', $f()); + assertNativeType('string', $f()); + + assertType('non-empty-string', (function (): string { + return funcWithANativeReturnType(); + })()); + assertNativeType('string', (function (): string { + return funcWithANativeReturnType(); + })()); + + $g = fn () => funcWithANativeReturnType(); + + assertType('non-empty-string', $g()); + assertNativeType('string', $g()); + + assertType('non-empty-string', (fn () => funcWithANativeReturnType())()); + assertNativeType('string', (fn () => funcWithANativeReturnType())()); + } + +} + +class TestPhp8Stubs +{ + + public function doFoo(): void + { + $a = array_replace([1, 2, 3], [4, 5, 6]); + assertType('non-empty-array<0|1|2, 1|2|3|4|5|6>', $a); + assertNativeType('array', $a); + } + +} + +class PositiveInt +{ + + /** + * @param positive-int $i + * @return void + */ + public function doFoo(int $i): void + { + assertType('true', $i > 0); + assertType('false', $i <= 0); + assertNativeType('bool', $i > 0); + assertNativeType('bool', $i <= 0); + } + +} diff --git a/tests/PHPStan/Analyser/data/never.php b/tests/PHPStan/Analyser/data/never.php index d09728b2f3..d57a16e5cb 100644 --- a/tests/PHPStan/Analyser/data/never.php +++ b/tests/PHPStan/Analyser/data/never.php @@ -14,7 +14,7 @@ public function doFoo(): never public function doBar() { - assertType('*NEVER*', $this->doFoo()); + assertType('never', $this->doFoo()); } public function doBaz(?int $i) diff --git a/tests/PHPStan/Analyser/data/no-named-arguments.php b/tests/PHPStan/Analyser/data/no-named-arguments.php index 6b9e6507cb..d16a28a5dd 100644 --- a/tests/PHPStan/Analyser/data/no-named-arguments.php +++ b/tests/PHPStan/Analyser/data/no-named-arguments.php @@ -2,6 +2,7 @@ namespace NoNamedArguments; +use function PHPStan\Testing\assertNativeType; use function PHPStan\Testing\assertType; /** @@ -9,7 +10,8 @@ */ function noNamedArgumentsInFunction(float ...$args) { - assertType('array', $args); + assertType('list', $args); + assertNativeType('list', $args); } class Baz extends Foo implements Bar @@ -19,17 +21,20 @@ class Baz extends Foo implements Bar */ public function noNamedArgumentsInMethod(float ...$args) { - assertType('array', $args); + assertType('list', $args); + assertNativeType('list', $args); } public function noNamedArgumentsInParent(float ...$args) { - assertType('array', $args); + assertType('list', $args); + assertNativeType('list', $args); } public function noNamedArgumentsInInterface(float ...$args) { - assertType('array', $args); + assertType('list', $args); + assertNativeType('list', $args); } } diff --git a/tests/PHPStan/Analyser/data/non-empty-array.php b/tests/PHPStan/Analyser/data/non-empty-array.php index 2656b5ff72..a7cdc6540a 100644 --- a/tests/PHPStan/Analyser/data/non-empty-array.php +++ b/tests/PHPStan/Analyser/data/non-empty-array.php @@ -26,10 +26,10 @@ public function doFoo( ): void { assertType('non-empty-array', $array); - assertType('non-empty-array', $list); + assertType('non-empty-list', $list); assertType('non-empty-array', $arrayOfStrings); - assertType('non-empty-array', $listOfStd); - assertType('non-empty-array', $listOfStd2); + assertType('non-empty-list', $listOfStd); + assertType('non-empty-list', $listOfStd2); assertType('array', $invalidList); assertType('mixed', $invalidList2); } diff --git a/tests/PHPStan/Analyser/data/non-empty-string-substr-pre-80.php b/tests/PHPStan/Analyser/data/non-empty-string-substr-pre-80.php index b0d8f23d80..7300733e12 100644 --- a/tests/PHPStan/Analyser/data/non-empty-string-substr-pre-80.php +++ b/tests/PHPStan/Analyser/data/non-empty-string-substr-pre-80.php @@ -1,6 +1,6 @@ ', explode($s, 'foo')); + assertType('non-empty-list', explode($s, 'foo')); } /** diff --git a/tests/PHPStan/Analyser/data/non-falsy-string.php b/tests/PHPStan/Analyser/data/non-falsy-string.php index c78bb0daa8..8654b0ef38 100644 --- a/tests/PHPStan/Analyser/data/non-falsy-string.php +++ b/tests/PHPStan/Analyser/data/non-falsy-string.php @@ -10,7 +10,7 @@ class Foo { * @param truthy-string $truthyString */ public function bar($nonFalseyString, $truthyString) { - assertType('int|int<1, max>', (int) $nonFalseyString); + assertType('int', (int) $nonFalseyString); // truthy-string is an alias for non-falsy-string assertType('non-falsy-string', $truthyString); } diff --git a/tests/PHPStan/Analyser/data/nullsafe-vs-scalar.php b/tests/PHPStan/Analyser/data/nullsafe-vs-scalar.php new file mode 100644 index 0000000000..ba26923120 --- /dev/null +++ b/tests/PHPStan/Analyser/data/nullsafe-vs-scalar.php @@ -0,0 +1,34 @@ +getTimestamp() > 0 ? $date->format('j') : ''; + echo $date?->getTimestamp() > 0 ? $date->format('j') : ''; + assertType('DateTimeImmutable|null', $date); + } + + public function bbb(?\DateTimeImmutable $date): void + { + echo 0 < $date?->getTimestamp() ? $date->format('j') : ''; + echo 0 < $date?->getTimestamp() ? $date->format('j') : ''; + assertType('DateTimeImmutable|null', $date); + } + + /** @param mixed $date */ + public function ccc($date): void + { + if ($date?->getTimestamp() > 0) { + assertType('mixed~null', $date); + } + + echo $date?->getTimestamp() > 0 ? $date->format('j') : ''; + echo $date?->getTimestamp() > 0 ? $date->format('j') : ''; + assertType('mixed', $date); + } +} diff --git a/tests/PHPStan/Analyser/data/nullsafe.php b/tests/PHPStan/Analyser/data/nullsafe.php index fcb27c2ebd..ed4b00481a 100644 --- a/tests/PHPStan/Analyser/data/nullsafe.php +++ b/tests/PHPStan/Analyser/data/nullsafe.php @@ -99,4 +99,11 @@ public function doDolor(?self $self) assertType('Nullsafe\Foo|null', $self?->nullableSelf); } + public function doNull(): void + { + $null = null; + assertType('null', $null?->foo); + assertType('null', $null?->doFoo()); + } + } diff --git a/tests/PHPStan/Analyser/data/object-shape.php b/tests/PHPStan/Analyser/data/object-shape.php new file mode 100644 index 0000000000..89d0f80654 --- /dev/null +++ b/tests/PHPStan/Analyser/data/object-shape.php @@ -0,0 +1,204 @@ +foo); + assertType('int', $o->bar); + assertType('*ERROR*', $o->baz); + } + + /** + * @param object{foo: self, bar: int, baz?: string} $o + */ + public function doFoo2(object $o): void + { + assertType('object{foo: ObjectShape\Foo, bar: int, baz?: string}', $o); + } + + public function doBaz(): void + { + assertType('object{}&stdClass', (object) []); + + $a = ['bar' => 2]; + if (rand(0, 1)) { + $a['foo'] = 1; + } + + assertType('object{bar: 2, foo?: 1}&stdClass', (object) $a); + } + + /** + * @template T + * @param object{foo: int, bar: T} $o + * @return T + */ + public function generics(object $o) + { + + } + + public function testGenerics() + { + $o = (object) ['foo' => 1, 'bar' => new \Exception()]; + assertType('object{foo: 1, bar: Exception}&stdClass', $o); + assertType('1', $o->foo); + assertType('Exception', $o->bar); + + assertType('Exception', $this->generics($o)); + } + + /** + * @return object{foo: static} + */ + public function returnObjectShapeWithStatic(): object + { + + } + + public function testObjectShapeWithStatic() + { + assertType('object{foo: static(ObjectShape\Foo)}', $this->returnObjectShapeWithStatic()); + } + +} + +class FooChild extends Foo +{ + +} + +class Bar +{ + + public function doFoo(Foo $foo) + { + assertType('object{foo: ObjectShape\Foo}', $foo->returnObjectShapeWithStatic()); + } + + public function doFoo2(FooChild $foo) + { + assertType('object{foo: ObjectShape\FooChild}', $foo->returnObjectShapeWithStatic()); + } + +} + +class OptionalProperty +{ + + /** + * @param object{foo: string, bar?: int} $o + * @return void + */ + public function doFoo(object $o): void + { + assertType('object{foo: string, bar?: int}', $o); + if (isset($o->foo)) { + assertType('object{foo: string, bar?: int}', $o); + } + + assertType('object{foo: string, bar?: int}', $o); + if (isset($o->bar)) { + assertType('object{foo: string, bar: int}', $o); + } + + assertType('object{foo: string, bar?: int}', $o); + } + + /** + * @param object{foo: string, bar?: int} $o + * @return void + */ + public function doBar(object $o): void + { + assertType('object{foo: string, bar?: int}', $o); + if (property_exists($o, 'foo')) { + assertType('object{foo: string, bar?: int}', $o); + } + + assertType('object{foo: string, bar?: int}', $o); + if (property_exists($o, 'bar')) { + assertType('object{foo: string, bar: int}', $o); + } + + assertType('object{foo: string, bar?: int}', $o); + } + +} + +class MethodExistsCheck +{ + + /** + * @param object{foo: string, bar?: int} $o + */ + public function doFoo(object $o): void + { + if (method_exists($o, 'doFoo')) { + assertType('object{foo: string, bar?: int}&hasMethod(doFoo)', $o); + } else { + assertType('object{foo: string, bar?: int}', $o); + } + + assertType('object{foo: string, bar?: int}', $o); + } + +} + +class ObjectWithProperty +{ + + public function doFoo(object $o): void + { + if (property_exists($o, 'foo')) { + assertType('object&hasProperty(foo)', $o); + } else { + assertType('object', $o); + } + assertType('object', $o); + + if (isset($o->foo)) { + assertType('object&hasProperty(foo)', $o); + } else { + assertType('object', $o); + } + assertType('object', $o); + } + +} + +class TestTemplate +{ + + /** + * @template T of object{foo: int} + * @param T $o + * @return T + */ + public function doBar(object $o): object + { + return $o; + } + + /** + * @param object{foo: positive-int} $o + * @return void + */ + public function doFoo(object $o): void + { + assertType('object{foo: int<1, max>}', $this->doBar($o)); + } + +} diff --git a/tests/PHPStan/Analyser/data/param-closure-this-stubs.php b/tests/PHPStan/Analyser/data/param-closure-this-stubs.php new file mode 100644 index 0000000000..61b0ab9b59 --- /dev/null +++ b/tests/PHPStan/Analyser/data/param-closure-this-stubs.php @@ -0,0 +1,60 @@ +transactional(function () { + assertType(EntityManagerParamClosureThis::class, $this); + }); + } + + public function doFoo2(): void + { + \MyFunctionClosureThis\doFoo(function () { + assertType(\MyFunctionClosureThis\Foo::class, $this); + }); + } + + public function doFoo3(array $a): void + { + uksort($a, function () { + assertType(\stdClass::class, $this); + }); + } + + /** + * @param \Ds\Deque $deque + */ + public function doFoo4(\Ds\Deque $deque): void + { + $deque->filter(function () { + assertType('Ds\Deque', $this); + }); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/param-closure-this-stubs.stub b/tests/PHPStan/Analyser/data/param-closure-this-stubs.stub new file mode 100644 index 0000000000..ec35c140a2 --- /dev/null +++ b/tests/PHPStan/Analyser/data/param-closure-this-stubs.stub @@ -0,0 +1,54 @@ + + * @param-closure-this $this $callback + */ + public function filter(callable $callback = null): Deque + { + } + } +} diff --git a/tests/PHPStan/Analyser/data/param-closure-this.php b/tests/PHPStan/Analyser/data/param-closure-this.php new file mode 100644 index 0000000000..1801bbef2b --- /dev/null +++ b/tests/PHPStan/Analyser/data/param-closure-this.php @@ -0,0 +1,318 @@ += 7.4 + +namespace ParamClosureThis; + +use function PHPStan\Testing\assertType; +use function sprintf; + +interface Some +{ + + public function voidMethod(): void; + +} + +class Foo +{ + + public ?string $prop = null; + + /** + * @param-closure-this Some $cb + */ + public function paramClosureClass(callable $cb) + { + + } + + /** + * @param-closure-this self $cb + */ + public function paramClosureSelf(callable $cb) + { + + } + + /** + * @param-closure-this Some $cb + * @param-immediately-invoked-callable $cb + */ + public function paramClosureClassImmediatelyCalled(callable $cb) + { + + } + + /** + * @param-closure-this static $cb + */ + public function paramClosureStatic(callable $cb) + { + + } + + /** + * @param-closure-this ($i is 1 ? Foo : Some) $cb + */ + public function paramClosureConditional(int $i, callable $cb) + { + + } + + /** + * @template T of object + * @param class-string $class + * @param-closure-this T $cb + */ + public function paramClosureGenerics(string $class, callable $cb): void + { + + } + + public function voidMethod(): void + { + + } + + public function doFoo(): void + { + assertType(sprintf('$this(%s)', self::class), $this); + $this->paramClosureClass(function () { + assertType(Some::class, $this); + }); + assertType(sprintf('$this(%s)', self::class), $this); + $this->paramClosureClass(static function () { + assertType('*ERROR*', $this); + }); + assertType(sprintf('$this(%s)', self::class), $this); + + $this->paramClosureSelf(function () use (&$a) { + assertType(Foo::class, $this); + }); + $this->paramClosureConditional(1, function () { + assertType(Foo::class, $this); + }); + $this->paramClosureConditional(2, function () { + assertType(Some::class, $this); + }); + $this->paramClosureGenerics(\stdClass::class, function () { + assertType(\stdClass::class, $this); + }); + } + + public function doFoo2(): void + { + assertType(sprintf('$this(%s)', self::class), $this); + $this->paramClosureClass(fn () => assertType(Some::class, $this)); + assertType(sprintf('$this(%s)', self::class), $this); + $this->paramClosureClass(static fn () => assertType('*ERROR*', $this)); + assertType(sprintf('$this(%s)', self::class), $this); + } + + public function doFoo3(): void + { + $a = 1; + assertType(sprintf('$this(%s)', self::class), $this); + $this->paramClosureClass(function () use (&$a) { + assertType(Some::class, $this); + }); + assertType(sprintf('$this(%s)', self::class), $this); + $this->paramClosureClass(static function () use (&$a) { + assertType('*ERROR*', $this); + }); + assertType(sprintf('$this(%s)', self::class), $this); + } + + public function interplayWithProcessImmediatelyCalledCallable(): void + { + assert($this->prop !== null); + assertType('string', $this->prop); + $this->paramClosureClassImmediatelyCalled(function () { + // $this is Some, not Foo + $this->voidMethod(); + }); + + // keep the narrowed type + assertType('string', $this->prop); + } + + public function interplayWithProcessImmediatelyCalledCallable2(): void + { + $s = new self(); + assert($s->prop !== null); + assertType('string', $s->prop); + $this->paramClosureClassImmediatelyCalled(function () use ($s) { + // $this is Some, not Foo + $this->voidMethod(); + + // but still invalidate $s + $s->voidMethod(); + }); + assertType('string|null', $s->prop); + } + +} + +function (Foo $f): void { + assertType('*ERROR*', $this); + $f->paramClosureClass(function () { + assertType(Some::class, $this); + }); + assertType('*ERROR*', $this); + $f->paramClosureClass(static function () { + assertType('*ERROR*', $this); + }); + assertType('*ERROR*', $this); +}; + +function (Foo $f): void { + $a = 1; + assertType('*ERROR*', $this); + $f->paramClosureClass(function () use (&$a) { + assertType(Some::class, $this); + }); + assertType('*ERROR*', $this); + $f->paramClosureClass(static function () use (&$a) { + assertType('*ERROR*', $this); + }); + assertType('*ERROR*', $this); +}; + +function (Foo $f): void { + assertType('*ERROR*', $this); + $f->paramClosureClass(fn () => assertType(Some::class, $this)); + assertType('*ERROR*', $this); + $f->paramClosureClass(static fn () => assertType('*ERROR*', $this)); + assertType('*ERROR*', $this); +}; + +class Bar extends Foo +{ + + public function testClosureStatic(): void + { + assertType('$this(ParamClosureThis\Bar)', $this); + $this->paramClosureStatic(function () { + assertType('static(ParamClosureThis\Bar)', $this); + }); + assertType('$this(ParamClosureThis\Bar)', $this); + } + +} + +function (Bar $b): void { + $b->paramClosureStatic(function () { + assertType(Bar::class, $this); + }); +}; + +class ImplicitInheritance extends Foo +{ + + public function paramClosureClass(callable $cb) + { + + } + + public function paramClosureSelf(callable $cb) + { + + } + + public function paramClosureStatic(callable $cb) + { + + } + + public function paramClosureConditional(int $j, callable $ca) + { + // renamed parameter names + } + + public function doFoo(): void + { + $this->paramClosureClass(function () { + assertType(Some::class, $this); + }); + $this->paramClosureClass(static function () { + assertType('*ERROR*', $this); + }); + $this->paramClosureSelf(function () use (&$a) { + assertType(Foo::class, $this); + }); + $this->paramClosureStatic(function () use (&$a) { + assertType('static(ParamClosureThis\ImplicitInheritance)', $this); + }); + $this->paramClosureConditional(1, function () { + assertType(Foo::class, $this); + }); + $this->paramClosureConditional(2, function () { + assertType(Some::class, $this); + }); + $this->paramClosureGenerics(\stdClass::class, function () { + assertType(\stdClass::class, $this); + }); + } + +} + +class ImplicitInheritanceMoreComplicated extends Foo +{ + + /** + * @param callable $cb + */ + public function paramClosureClass(callable $cb) + { + + } + + /** + * @param callable $cb + */ + public function paramClosureSelf(callable $cb) + { + + } + + /** + * @param callable $cb + */ + public function paramClosureStatic(callable $cb) + { + + } + + /** + * @param callable $ca + */ + public function paramClosureConditional(int $j, callable $ca) + { + // renamed parameter names + } + + public function doFoo(): void + { + $this->paramClosureClass(function () { + assertType(Some::class, $this); + }); + $this->paramClosureClass(static function () { + assertType('*ERROR*', $this); + }); + $this->paramClosureSelf(function () use (&$a) { + assertType(Foo::class, $this); + }); + $this->paramClosureStatic(function () use (&$a) { + assertType('static(ParamClosureThis\ImplicitInheritanceMoreComplicated)', $this); + }); + $this->paramClosureConditional(1, function () { + assertType(Foo::class, $this); + }); + $this->paramClosureConditional(2, function () { + assertType(Some::class, $this); + }); + $this->paramClosureGenerics(\stdClass::class, function () { + assertType(\stdClass::class, $this); + }); + } + +} diff --git a/tests/PHPStan/Analyser/data/param-out-default.php b/tests/PHPStan/Analyser/data/param-out-default.php new file mode 100644 index 0000000000..5d83d593a2 --- /dev/null +++ b/tests/PHPStan/Analyser/data/param-out-default.php @@ -0,0 +1,36 @@ + : array) $out + */ + public function doFoo(&$out, $flags = 1): void + { + + } + + public function doBar(): void + { + $this->doFoo($a); + assertType('array', $a); + + $this->doFoo($b, 1); + assertType('array', $b); + + $this->doFoo($c, 2); + assertType('array', $c); + } + + public function sayHello(string $row): void + { + preg_match_all('#// error:(.+)#', $row, $matches); + assertType('array>', $matches); + } + +} diff --git a/tests/PHPStan/Analyser/data/param-out.php b/tests/PHPStan/Analyser/data/param-out.php new file mode 100644 index 0000000000..88cd9bf14d --- /dev/null +++ b/tests/PHPStan/Analyser/data/param-out.php @@ -0,0 +1,503 @@ + + */ +class ExtendsFooBar extends FooBar { + /** + * @param-out string $s + */ + function subMethod(?string &$s): void + { + } + + /** + * @param-out string $s + */ + function overriddenMethod(?string &$s): void + { + } + + function overriddenButinheritedPhpDocMethod(?string &$s): void + { + } + + public function renamedParams(int $x, int &$y) { + parent::renamedParams($x, $y); + } + + /** + * @param-out array $b + */ + public function paramOutOverridden(int $a, int &$b) { + } + +} + +class OutFromStub { + function stringOut(string &$string): void + { + } +} + +/** + * @param-out bool $s + */ +function takesNullableBool(?bool &$s) : void { + $s = true; +} + +/** + * @param-out int $var + */ +function variadicFoo(&...$var): void +{ + $var[0] = 2; + $var[1] = 2; +} + +/** + * @param-out string $s + * @param-out int $var + */ +function variadicFoo2(?string &$s, &...$var): void +{ + $s = ''; + $var[0] = 2; + $var[1] = 2; +} + +function foo1(?string $s): void { + assertType('string|null', $s); + addFoo($s); + assertType('string', $s); +} + +function foo2($mixed): void { + assertType('mixed', $mixed); + addFoo($mixed); + assertType('string', $mixed); +} + +/** + * @param FooBar $fooBar + * @return void + */ +function foo3($mixed, $fooBar): void { + assertType('mixed', $mixed); + $fooBar->genericClassFoo($mixed); + assertType('int', $mixed); +} + +function foo6(): void { + $b = false; + takesNullableBool($b); + + assertType('bool', $b); +} + +function foo7(): void { + variadicFoo( $a, $b); + assertType('int', $a); + assertType('int', $b); + + variadicFoo2($s, $a, $b); + assertType('string', $s); + assertType('int', $a); + assertType('int', $b); +} + +function foo8(string $s): void { + sodium_memzero($s); + assertType('null', $s); +} + +function foo9(?string $s): void { + $c = new OutFromStub(); + $c->stringOut($s); + assertType('string', $s); +} + +function foo10(?string $s): void { + $c = new ExtendsFooBar(); + $c->baseMethod($s); + assertType('string', $s); +} + +function foo11(?string $s): void { + $c = new ExtendsFooBar(); + $c->subMethod($s); + assertType('string', $s); +} + +function foo12(?string $s): void { + $c = new ExtendsFooBar(); + $c->overriddenMethod($s); + assertType('string', $s); +} + +function foo13(?string $s): void { + $c = new ExtendsFooBar(); + $c->overriddenButinheritedPhpDocMethod($s); + assertType('string', $s); +} + +/** + * @param array $a + * @param non-empty-array $nonEmptyArray + */ +function foo14(array $a, $nonEmptyArray): void { + \shuffle($a); + assertType('list', $a); + \shuffle($nonEmptyArray); + assertType('non-empty-list', $nonEmptyArray); +} + +function fooCompare (int $a, int $b): int { + return $a > $b ? 1 : -1; +} + +function foo15() { + $manifest = [1, 2, 3]; + uasort( + $manifest, + "fooCompare" + ); + assertType('array{1, 2, 3}', $manifest); +} + +function fooSpaceship (string $a, string $b): int { + return $a <=> $b; +} + +function foo16() { + $array = [1, 2]; + uksort( + $array, + "fooSpaceship" + ); + assertType('array{1, 2}', $array); +} + +function fooShuffle() { + $array = ["foo" => 123, "bar" => 456]; + shuffle($array); + assertType('non-empty-array<0|1, 123|456>&list', $array); + + $emptyArray = []; + shuffle($emptyArray); + assertType('array{}', $emptyArray); +} + +function fooSort() { + $array = ["foo" => 123, "bar" => 456]; + sort($array); + assertType('non-empty-list<123|456>', $array); + assertType('true', array_is_list($array)); + + $emptyArray = []; + sort($emptyArray); + assertType('array{}', $emptyArray); +} + +function fooFscanf($r): void +{ + fscanf($r, "%d:%d:%d", $hours, $minutes, $seconds); + assertType('float|int|string|null', $hours); + assertType('float|int|string|null', $minutes); + assertType('float|int|string|null', $seconds); + + $n = fscanf($r, "%s %s", $p1, $p2); + assertType('float|int|string|null', $p1); + assertType('float|int|string|null', $p2); +} + +function fooScanf(): void +{ + sscanf("10:05:03", "%d:%d:%d", $hours, $minutes, $seconds); + assertType('float|int|string|null', $hours); + assertType('float|int|string|null', $minutes); + assertType('float|int|string|null', $seconds); + + $n = sscanf("42 psalm road", "%s %s", $p1, $p2); + assertType('int|null', $n); // could be 'int' + assertType('float|int|string|null', $p1); + assertType('float|int|string|null', $p2); +} + +function fooMatch(string $input): void { + preg_match_all('/@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/', $input, $matches, PREG_PATTERN_ORDER); + assertType('array>', $matches); + + preg_match_all('/@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/', $input, $matches, PREG_SET_ORDER); + assertType('list>', $matches); + + preg_match('/@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/', $input, $matches, PREG_UNMATCHED_AS_NULL); + assertType("array", $matches); +} + +function fooParams(ExtendsFooBar $subX, float $x1, float $y1) +{ + $subX->renamedParams($x1, $y1); + + assertType('float', $x1); + assertType('string', $y1); // overridden via reference of base-class, by param order (renamed params) +} + +function fooParams2(ExtendsFooBar $subX, float $x1, float $y1) { + $subX->paramOutOverridden($x1, $y1); + + assertType('float', $x1); + assertType('array', $y1); // overridden phpdoc-param-out-type in subclass +} + +function fooDateTime(\SplFileObject $splFileObject, ?string $wouldBlock) { + // php-src native method overridden via stub + $splFileObject->flock(1, $wouldBlock); + + assertType('string', $wouldBlock); +} + +function testMatch() { + preg_match('#.*#', 'foo', $matches); + assertType('array', $matches); +} + +function testParseStr() { + $str="first=value&arr[]=foo+bar&arr[]=baz"; + parse_str($str, $output); + + /* + echo $output['first'];//value + echo $output['arr'][0];//foo bar + echo $output['arr'][1];//baz + */ + + \PHPStan\Testing\assertType('array', $output); +} + +function fooSimilar() { + $similar = similar_text('foo', 'bar', $percent); + assertType('int', $similar); + assertType('float', $percent); +} + +function fooExec() { + exec("my cmd", $output, $exitCode); + + assertType('list', $output); + assertType('int', $exitCode); +} + +function fooSystem() { + system("my cmd", $exitCode); + + assertType('int', $exitCode); +} + +function fooPassthru() { + passthru("my cmd", $exitCode); + + assertType('int', $exitCode); +} + +class X { + /** + * @param-out array $ref + */ + public function __construct(string &$ref) { + $ref = []; + } +} + +class SubX extends X { + /** + * @param-out float $ref + */ + public function __construct(string $a, string &$ref) { + parent::__construct($ref); + } +} + +function fooConstruct(string $s) { + $x = new X($s); + assertType('array', $s); +} + +function fooSubConstruct(string $s) { + $x = new SubX('', $s); + assertType('float', $s); +} + +function fooFlock(int $f): void +{ + $fp=fopen('/tmp/lock.txt', 'r+'); + flock($fp, $f, $wouldBlock); + assertType('0|1', $wouldBlock); +} + +function fooFsockopen() { + $fp=fsockopen("udp://127.0.0.1",13, $errno, $errstr); + assertType('int', $errno); + assertType('string', $errstr); +} + +function fooHeadersSent() { + headers_sent($filename, $linenum); + assertType('int', $linenum); + assertType('string', $filename); +} + +function fooMbParseStr() { + mb_parse_str("foo=bar", $output); + assertType('array', $output); + + mb_parse_str('email=mail@example.org&city=town&x=1&y[g]=3&f=1.23', $output); + assertType('array', $output); +} + +function fooPreg() +{ + $string = 'April 15, 2003'; + $pattern = '/(\w+) (\d+), (\d+)/i'; + $replacement = '${1}1,$3'; + preg_replace($pattern, $replacement, $string, -1, $c); + assertType('int<0, max>', $c); + + preg_replace_callback($pattern, function ($matches) { + return strtolower($matches[0]); + }, $string, -1, $c); + assertType('int<0, max>', $c); + + preg_filter($pattern, $replacement, $string, -1, $c); + assertType('int<0, max>', $c); +} + +function fooReplace() { + $vowels = array("a", "e", "i", "o", "u", "A", "E", "I", "O", "U"); + str_replace($vowels, "", "World", $count); + assertType('int', $count); + + $vowels = array("a", "e", "i", "o", "u", "A", "E", "I", "O", "U"); + str_ireplace($vowels, "", "World", $count); + assertType('int', $count); +} + +function fooIsCallable($x, bool $b) +{ + is_callable($x, $b, $name); + assertType('callable-string', $name); +} + +function noParamOut(string &$s): void +{ + +} + +function noParamOutVariadic(string &...$s): void +{ + +} + +function ($s): void { + assertType('mixed', $s); + noParamOut($s); + assertType('string', $s); +}; + +function ($s, $t): void { + assertType('mixed', $s); + assertType('mixed', $t); + noParamOutVariadic($s, $t); + assertType('string', $s); + assertType('string', $t); +}; + +class NoParamOutClass +{ + + function doFoo(string &$s): void + { + + } + + function doFooVariadic(string &...$s): void + { + + } + +} + +function ($s): void { + assertType('mixed', $s); + $c = new NoParamOutClass(); + $c->doFoo($s); + assertType('string', $s); +}; + +function ($s, $t): void { + assertType('mixed', $s); + assertType('mixed', $t); + $c = new NoParamOutClass(); + $c->doFooVariadic($s, $t); + assertType('string', $s); + assertType('string', $t); +}; diff --git a/tests/PHPStan/Analyser/data/pathConstants.php b/tests/PHPStan/Analyser/data/pathConstants.php new file mode 100644 index 0000000000..3bf091958f --- /dev/null +++ b/tests/PHPStan/Analyser/data/pathConstants.php @@ -0,0 +1,6 @@ +loadHTML($actual); + } else { + $loaded = $document->loadXML($actual); + } + + foreach (libxml_get_errors() as $error) { + $message .= "\n" . $error->message; + } + + if ($loaded === false || ($strict && $message !== '')) { + assertType('string', $message); + assertNativeType('string', $message); + if ($filename !== '') { + assertType('string', $message); + assertNativeType('string', $message); + throw new Exception( + sprintf( + 'Could not load "%s".%s', + $filename, + $message !== '' ? "\n" . $message : '' + ) + ); + } + + assertType('string', $message); + assertNativeType('string', $message); + + if ($message === '') { + $message = 'Could not load XML for unknown reason'; + } + + assertType('non-empty-string', $message); + assertNativeType('non-empty-string', $message); + + throw new Exception($message); + } + + return $document; + } +} diff --git a/tests/PHPStan/Analyser/data/pow.php b/tests/PHPStan/Analyser/data/pow.php index 960b88fad3..82f03b1bae 100644 --- a/tests/PHPStan/Analyser/data/pow.php +++ b/tests/PHPStan/Analyser/data/pow.php @@ -6,16 +6,174 @@ function ($a, $b): void { assertType('(float|int)', pow($a, $b)); + assertType('(float|int)', $a ** $b); }; function (int $a, int $b): void { assertType('(float|int)', pow($a, $b)); + assertType('(float|int)', $a ** $b); }; function (\GMP $a, \GMP $b): void { assertType('GMP', pow($a, $b)); + assertType('GMP', $a ** $b); }; function (\stdClass $a, \GMP $b): void { assertType('GMP|stdClass', pow($a, $b)); + assertType('GMP|stdClass', $a ** $b); + + assertType('GMP|stdClass', pow($b, $a)); + assertType('GMP|stdClass', $b ** $a); +}; + +function (): void { + $range = rand(1, 3); + assertType('int<1, 3>', $range); + + assertType('int<1, 9>', pow($range, 2)); + assertType('int<1, 9>', $range ** 2); + + assertType('int<2, 8>', pow(2, $range)); + assertType('int<2, 8>', 2 ** $range); +}; + +function (): void { + $range = rand(2, 3); + $x = 2; + if (rand(0, 1)) { + $x = 3; + } else if (rand(0, 10)) { + $x = 4; + } + + assertType('int<4, 27>|int<16, 81>', pow($range, $x)); + assertType('int<4, 27>|int<16, 81>', $range ** $x); + + assertType('int<4, 27>|int<16, 64>', pow($x, $range)); + assertType('int<4, 27>|int<16, 64>', $x ** $range); + + assertType('int<4, 27>', pow($range, $range)); + assertType('int<4, 27>', $range ** $range); +}; + +/** + * @param positive-int $positiveInt + * @param int $range2 + * @param int<-6, -4>|int<-2, -1> $unionRange1 + * @param int<4, 6>|int<1, 2> $unionRange2 + */ +function foo($positiveInt, $range2, $unionRange1, $unionRange2): void { + $range = rand(2, 3); + + assertType('int<2, max>', pow($range, $positiveInt)); + assertType('int<2, max>', $range ** $positiveInt); + + assertType('int', pow($range, $range2)); + assertType('int', $range ** $range2); + + assertType('(float|int)', pow($range, PHP_INT_MAX)); + assertType('(float|int)', $range ** PHP_INT_MAX); + + assertType('(float|int)', pow($range2, $positiveInt)); + assertType('(float|int)', $range2 ** $positiveInt); + + assertType('(float|int)', pow($positiveInt, $range2)); + assertType('(float|int)', $positiveInt ** $range2); + + assertType('int<-6, 16>|int<1296, 4096>', pow($unionRange1, $unionRange2)); + assertType('int<-6, 16>|int<1296, 4096>', $unionRange1 ** $unionRange2); + + assertType('int<2, 4>|int<16, 64>', pow(2, $unionRange2)); + assertType('int<2, 4>|int<16, 64>', 2 ** $unionRange2); + + assertType('int<2, 4>|int<16, 64>', pow("2", $unionRange2)); + assertType('int<2, 4>|int<16, 64>', "2" ** $unionRange2); + + assertType('1', pow(true, $unionRange2)); + assertType('1', true ** $unionRange2); + + assertType('0|1', pow(null, $unionRange2)); + assertType('0|1', null ** $unionRange2); +} + +/** + * @param numeric-string $numericS + */ +function doFoo(int $intA, int $intB, string $s, bool $bool, $numericS, float $float, array $arr): void { + assertType('(float|int)', pow($intA, $intB)); + assertType('(float|int)', $intA ** $intB); + + assertType('(float|int)', pow($intA, $numericS)); + assertType('(float|int)', $intA ** $numericS); + assertType('(float|int)', $numericS ** $numericS); + assertType('(float|int)', pow($intA, "123")); + assertType('(float|int)', $intA ** "123"); + assertType('int', pow($intA, 1)); + assertType('int', $intA ** '1'); + + assertType('(float|int)', pow($intA, $s)); + assertType('(float|int)', $intA ** $s); + + assertType('(float|int)', pow($intA, $bool)); // could be int + assertType('(float|int)', $intA ** $bool); // could be int + assertType('int', pow($intA, true)); + assertType('int', $intA ** true); + + assertType('*ERROR*', pow($bool, $arr)); + assertType('*ERROR*', pow($bool, [])); + + assertType('0|1', pow(null, "123")); + assertType('0|1', pow(null, $intA)); + assertType('1', "123" ** null); + assertType('1', $intA ** null); + assertType('1.0', $float ** null); + + assertType('*ERROR*', "123" ** $arr); + assertType('*ERROR*', "123" ** []); + + assertType('625', pow('5', '4')); + assertType('625', '5' ** '4'); + + assertType('(float|int)', pow($intA, $bool)); // could be float + assertType('(float|int)', $intA ** $bool); // could be float + assertType('*ERROR*', $intA ** $arr); + assertType('*ERROR*', $intA ** []); + + assertType('1', pow($intA, 0)); + assertType('1', $intA ** '0'); + assertType('1', $intA ** false); + assertType('int', $intA ** true); + + assertType('1.0', pow($float, 0)); + assertType('1.0', $float ** '0'); + assertType('1.0', $float ** false); + assertType('float', pow($float, 1)); + assertType('float', $float ** '1'); + assertType('*ERROR*', $float ** $arr); + assertType('*ERROR*', $float ** []); + + assertType('1.0', pow(1.1, 0)); + assertType('1.0', 1.1 ** '0'); + assertType('1.0', 1.1 ** false); + assertType('*ERROR*', 1.1 ** $arr); + assertType('*ERROR*', 1.1 ** []); + + assertType('NAN', pow(-1,5.5)); + + assertType('1', pow($s, 0)); + assertType('1', $s ** '0'); + assertType('1', $s ** false); + assertType('(float|int)', pow($s, 1)); + assertType('(float|int)', $s ** '1'); + assertType('*ERROR*', $s ** $arr); + assertType('*ERROR*', $s ** []); + + assertType('1', pow($bool, 0)); + assertType('1', $bool ** '0'); + assertType('1', $bool ** false); + assertType('(float|int)', pow($bool, 1)); + assertType('(float|int)', $bool ** '1'); + assertType('*ERROR*', $bool ** $arr); + assertType('*ERROR*', $bool ** []); }; diff --git a/tests/PHPStan/Analyser/data/pr-2030.php b/tests/PHPStan/Analyser/data/pr-2030.php new file mode 100644 index 0000000000..f1366d0512 --- /dev/null +++ b/tests/PHPStan/Analyser/data/pr-2030.php @@ -0,0 +1,39 @@ + $data + * @return array + */ + public function someMethod(array $data): array + { + foreach ($data[self::FIELD_NOTES][self::SUBFIELD_NOTE] ?? [] as $index => $noteData) { + $noteTitle = $noteData[self::FIELD_TITLE] ?? null; + $noteSource = $noteData[self::FIELD_SOURCE] ?? null; + $noteBody = $noteData[self::FIELD_BODY] ?? null; + + if ($noteBody === null || trim($noteBody) === '') { + $data[self::FIELD_NOTES] = self::EMPTY_NOTE_BODY; + } + } + + if (isset($data[self::FIELD_NOTES][self::SUBFIELD_NOTE])) {} + + return $data; + } + +} diff --git a/tests/PHPStan/Analyser/data/preg_filter.php b/tests/PHPStan/Analyser/data/preg_filter.php index 8eb5826df0..02824d3c9c 100644 --- a/tests/PHPStan/Analyser/data/preg_filter.php +++ b/tests/PHPStan/Analyser/data/preg_filter.php @@ -24,16 +24,16 @@ function doFoo1() { function doFoo2() { $subject = 123; - assertType('array|string|null', preg_filter('/\d/', '$0', $subject)); + assertType('list|string|null', preg_filter('/\d/', '$0', $subject)); $subject = 123.123; - assertType('array|string|null', preg_filter('/\d/', '$0', $subject)); + assertType('list|string|null', preg_filter('/\d/', '$0', $subject)); } public function dooFoo3(string $pattern, string $replace) { - assertType('array|string|null', preg_filter($pattern, $replace)); - assertType('array|string|null', preg_filter($pattern)); - assertType('array|string|null', preg_filter()); + assertType('list|string|null', preg_filter($pattern, $replace)); + assertType('list|string|null', preg_filter($pattern)); + assertType('list|string|null', preg_filter()); } function bug664() { diff --git a/tests/PHPStan/Analyser/data/preg_match_php7.php b/tests/PHPStan/Analyser/data/preg_match_php7.php index 2e435cbf87..306a77e80a 100644 --- a/tests/PHPStan/Analyser/data/preg_match_php7.php +++ b/tests/PHPStan/Analyser/data/preg_match_php7.php @@ -1,6 +1,6 @@ |false', preg_split('/-/', '1-2-3')); - assertType('array|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY)); - assertType('array}>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE)); - assertType('array}>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + assertType('list|false', preg_split('/-/', '1-2-3')); + assertType('list|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY)); + assertType('list}>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE)); + assertType('list}>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); } /** @@ -22,10 +24,10 @@ public function doFoo() */ public static function splitWithOffset($pattern, $subject, $limit = -1, $flags = 0) { - assertType('array}>|false', preg_split($pattern, $subject, $limit, $flags | PREG_SPLIT_OFFSET_CAPTURE)); - assertType('array}>|false', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags)); + assertType('list}>|false', preg_split($pattern, $subject, $limit, $flags | PREG_SPLIT_OFFSET_CAPTURE)); + assertType('list}>|false', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags)); - assertType('array}>|false', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags | PREG_SPLIT_NO_EMPTY)); + assertType('list}>|false', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags | PREG_SPLIT_NO_EMPTY)); } /** @@ -40,6 +42,6 @@ public static function dynamicFlags($pattern, $subject, $limit = -1) { $flags |= PREG_SPLIT_NO_EMPTY; } - assertType('array}>|false', preg_split($pattern, $subject, $limit, $flags)); + assertType('list}>|false', preg_split($pattern, $subject, $limit, $flags)); } } diff --git a/tests/PHPStan/Analyser/data/preserve-large-constant-array.php b/tests/PHPStan/Analyser/data/preserve-large-constant-array.php new file mode 100644 index 0000000000..a66425e3dd --- /dev/null +++ b/tests/PHPStan/Analyser/data/preserve-large-constant-array.php @@ -0,0 +1,67 @@ +value); + } + /** @param \Closure(T|null): T $callback */ + public function onResolve2(\Closure $callback) : void{ + $r = $callback($this->value); + assertType('TValue (class ProcessCalledMethodInfiniteLoop\\Promise, argument)', $r); + + $callback($this->value); + } +} +class HelloWorld +{ + /** + * @template TValue + * @param \Generator, TValue|null, void> $async + */ + public function next(\Generator $async) : void{ + $async->next(); + if(!$async->valid()) return; + $promise = $async->current(); + $promise->onResolve(function($value) use ($async) : void{ + $async->send($value); + $this->next($async); + }); + } +} diff --git a/tests/PHPStan/Analyser/data/promoted-properties-types.php b/tests/PHPStan/Analyser/data/promoted-properties-types.php index 7581c6dbe2..20c9aa11d1 100644 --- a/tests/PHPStan/Analyser/data/promoted-properties-types.php +++ b/tests/PHPStan/Analyser/data/promoted-properties-types.php @@ -103,3 +103,16 @@ function (Baz $baz): void { assertType('array', $baz->anotherPhpDocArray); assertType('stdClass', $baz->templateProperty); }; + +class PromotedPropertyNotNullable +{ + + public function __construct( + public int $intProp = null, + ) {} + +} + +function (PromotedPropertyNotNullable $p) { + assertType('int', $p->intProp); +}; diff --git a/tests/PHPStan/Analyser/data/psalm-prefix-unresolvable.php b/tests/PHPStan/Analyser/data/psalm-prefix-unresolvable.php index 036f9b996d..a2ae0bf2b7 100644 --- a/tests/PHPStan/Analyser/data/psalm-prefix-unresolvable.php +++ b/tests/PHPStan/Analyser/data/psalm-prefix-unresolvable.php @@ -18,7 +18,7 @@ public function doFoo() public function doBar(): void { - assertType('array', $this->doFoo()); + assertType('list', $this->doFoo()); } /** diff --git a/tests/PHPStan/Analyser/data/pure-callable.php b/tests/PHPStan/Analyser/data/pure-callable.php new file mode 100644 index 0000000000..39ef172288 --- /dev/null +++ b/tests/PHPStan/Analyser/data/pure-callable.php @@ -0,0 +1,18 @@ += 0); + \assert($max >= 0); assertType('int<0, max>', random_int(0, $max)); }; diff --git a/tests/PHPStan/Analyser/data/range-int-range.php b/tests/PHPStan/Analyser/data/range-int-range.php new file mode 100644 index 0000000000..f1846aad71 --- /dev/null +++ b/tests/PHPStan/Analyser/data/range-int-range.php @@ -0,0 +1,61 @@ + $a + * @param int<0,max> $b + */ + public function zeroToMax( + int $a, + int $b + ): void + { + assertType('list>', range($a, $b)); + } + + /** + * @param int<2,10> $a + * @param int<5,20> $b + */ + public function twoToTwenty( + int $a, + int $b + ): void + { + assertType('list>', range($a, $b)); + } + + /** + * @param int<10,30> $a + * @param int<5,20> $b + */ + public function fifteenTo5( + int $a, + int $b + ): void + { + assertType('list>', range($a, $b)); + } + + public function knownRange( + ): void + { + $a = 5; + $b = 10; + assertType('array{5, 6, 7, 8, 9, 10}', range($a, $b)); + } + + public function knownLargeRange( + ): void + { + $a = 5; + $b = 100; + assertType('non-empty-list>', range($a, $b)); + } +} diff --git a/tests/PHPStan/Analyser/data/range-numeric-string.php b/tests/PHPStan/Analyser/data/range-numeric-string.php index faddec206b..bae424e559 100644 --- a/tests/PHPStan/Analyser/data/range-numeric-string.php +++ b/tests/PHPStan/Analyser/data/range-numeric-string.php @@ -16,7 +16,7 @@ public function doFoo( string $b ): void { - assertType('array', range($a, $b)); + assertType('list', range($a, $b)); } } diff --git a/tests/PHPStan/Analyser/data/reflection-type.php b/tests/PHPStan/Analyser/data/reflection-type.php new file mode 100644 index 0000000000..d390747fe6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/reflection-type.php @@ -0,0 +1,15 @@ +getType()); + assertType('ReflectionType|null', $reflectionFunctionAbstract->getReturnType()); + assertType('ReflectionType|null', $reflectionParameter->getType()); +} diff --git a/tests/PHPStan/Analyser/data/reflectionclass-issue-5511-php8.php b/tests/PHPStan/Analyser/data/reflectionclass-issue-5511-php8.php index b657010861..41dbe632a6 100644 --- a/tests/PHPStan/Analyser/data/reflectionclass-issue-5511-php8.php +++ b/tests/PHPStan/Analyser/data/reflectionclass-issue-5511-php8.php @@ -40,7 +40,7 @@ function testGetAttributes( assertType('array>', $classGCN); assertType('array>', $classCN); assertType('array>', $classStr); - assertType('array>', $classNonsense); + assertType('array>', $classNonsense); $methodAll = $reflectionMethod->getAttributes(); $methodAbc = $reflectionMethod->getAttributes(Abc::class); diff --git a/tests/PHPStan/Analyser/data/rule-error-builder.php b/tests/PHPStan/Analyser/data/rule-error-builder.php new file mode 100644 index 0000000000..4f4b905ab3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/rule-error-builder.php @@ -0,0 +1,28 @@ +', $builder); + assertType('PHPStan\Rules\RuleError', $builder->build()); + + $builder->identifier('test'); + assertType('PHPStan\Rules\RuleErrorBuilder', $builder); + assertType('PHPStan\Rules\IdentifierRuleError', $builder->build()); + + assertType('PHPStan\Rules\IdentifierRuleError', RuleErrorBuilder::message('test')->identifier('test')->build()); + + $builder->tip('test'); + assertType('PHPStan\Rules\RuleErrorBuilder', $builder); + assertType('PHPStan\Rules\IdentifierRuleError&PHPStan\Rules\TipRuleError', $builder->build()); + } + +} diff --git a/tests/PHPStan/Analyser/data/self-out.php b/tests/PHPStan/Analyser/data/self-out.php new file mode 100644 index 0000000000..d4de8dbf84 --- /dev/null +++ b/tests/PHPStan/Analyser/data/self-out.php @@ -0,0 +1,97 @@ + + */ + private array $data; + /** + * @param T $data + */ + public function __construct($data) { + $this->data = [$data]; + } + /** + * @template NewT + * + * @param NewT $data + * + * @phpstan-self-out self + * + * @return void + */ + public function addData($data) { + /** @var self $this */ + $this->data []= $data; + } + /** + * @template NewT + * + * @param NewT $data + * + * @phpstan-self-out self + * + * @return void + */ + public function setData($data) { + /** @var self $this */ + $this->data = [$data]; + } + /** + * @return ($this is a ? void : never) + */ + public function test(): void { + } +} + +/** + * @template T + * @extends a + */ +class b extends a { + /** + * @param T $data + */ + public function __construct($data) { + parent::__construct($data); + } +} + +function () { + $i = new a(123); + // OK - $i is a<123> + assertType('SelfOut\\a', $i); + assertType('null', $i->test()); + + $i->addData(321); + // OK - $i is a<123|321> + assertType('SelfOut\\a', $i); + assertType('null', $i->test()); + + $i->setData("test"); + // IfThisIsMismatch - Class is not a as required + assertType('SelfOut\\a<\'test\'>', $i); + assertType('never', $i->test()); +}; + +function () { + $i = new b(123); + assertType('SelfOut\\b', $i); + + $i->addData(321); + assertType('SelfOut\\a', $i); + + $i->addData(random_bytes(3)); + assertType('SelfOut\\a', $i); + + $i->setData(true); + assertType('SelfOut\\a', $i); +}; diff --git a/tests/PHPStan/Analyser/data/set-type-type-specifying.php b/tests/PHPStan/Analyser/data/set-type-type-specifying.php new file mode 100644 index 0000000000..11869c0623 --- /dev/null +++ b/tests/PHPStan/Analyser/data/set-type-type-specifying.php @@ -0,0 +1,673 @@ + 'bar']; + settype($x, 'string'); + assertType('*ERROR*', $x); + + // array to int + $x = []; + settype($x, 'int'); + assertType('0', $x); + + $x = ['foo']; + settype($x, 'int'); + assertType('1', $x); + + $x = ['foo' => 'bar']; + settype($x, 'int'); + assertType('1', $x); + + // array to integer + $x = []; + settype($x, 'integer'); + assertType('0', $x); + + $x = ['foo']; + settype($x, 'integer'); + assertType('1', $x); + + $x = ['foo' => 'bar']; + settype($x, 'integer'); + assertType('1', $x); + + // array to float + $x = []; + settype($x, 'float'); + assertType('0.0', $x); + + $x = ['foo']; + settype($x, 'float'); + assertType('1.0', $x); + + $x = ['foo' => 'bar']; + settype($x, 'float'); + assertType('1.0', $x); + + // array to double + $x = []; + settype($x, 'double'); + assertType('0.0', $x); + + $x = ['foo']; + settype($x, 'double'); + assertType('1.0', $x); + + $x = ['foo' => 'bar']; + settype($x, 'double'); + assertType('1.0', $x); + + // array to bool + $x = []; + settype($x, 'bool'); + assertType('false', $x); + + $x = ['foo']; + settype($x, 'bool'); + assertType('true', $x); + + $x = ['foo' => 'bar']; + settype($x, 'bool'); + assertType('true', $x); + + // array to boolean + $x = []; + settype($x, 'boolean'); + assertType('false', $x); + + $x = ['foo']; + settype($x, 'boolean'); + assertType('true', $x); + + $x = ['foo' => 'bar']; + settype($x, 'boolean'); + assertType('true', $x); + + // array to array + $x = []; + settype($x, 'array'); + assertType('array{}', $x); + + $x = ['foo']; + settype($x, 'array'); + assertType("array{'foo'}", $x); + + $x = ['foo' => 'bar']; + settype($x, 'array'); + assertType("array{foo: 'bar'}", $x); + + // array to object + $x = []; + settype($x, 'object'); + assertType('stdClass', $x); + + $x = ['foo']; + settype($x, 'object'); + assertType("stdClass", $x); + + $x = ['foo' => 'bar']; + settype($x, 'object'); + assertType("stdClass", $x); + + // array to null + $x = []; + settype($x, 'null'); + assertType('null', $x); + + $x = ['foo']; + settype($x, 'null'); + assertType('null', $x); + + $x = ['foo' => 'bar']; + settype($x, 'null'); + assertType('null', $x); + + // object to string + $x = new stdClass(); + settype($x, 'string'); + assertType('*ERROR*', $x); + + // object to int + $x = new stdClass(); + settype($x, 'int'); + assertType('*ERROR*', $x); + + // object to integer + $x = new stdClass(); + settype($x, 'integer'); + assertType('*ERROR*', $x); + + // object to float + $x = new stdClass(); + settype($x, 'float'); + assertType('*ERROR*', $x); + + // object to double + $x = new stdClass(); + settype($x, 'double'); + assertType('*ERROR*', $x); + + // object to bool + $x = new stdClass(); + settype($x, 'bool'); + assertType('true', $x); + + // object to boolean + $x = new stdClass(); + settype($x, 'boolean'); + assertType('true', $x); + + // object to array + $x = new stdClass(); + settype($x, 'array'); + assertType('array', $x); + + // object to object + $x = new stdClass(); + settype($x, 'object'); + assertType('stdClass', $x); + + // object to null + $x = new stdClass(); + settype($x, 'null'); + assertType('null', $x); + + // null to string + $x = null; + settype($x, 'string'); + assertType("''", $x); + + // null to int + $x = null; + settype($x, 'int'); + assertType('0', $x); + + // null to integer + $x = null; + settype($x, 'integer'); + assertType('0', $x); + + // null to float + $x = null; + settype($x, 'float'); + assertType('0.0', $x); + + // null to double + $x = null; + settype($x, 'double'); + assertType('0.0', $x); + + // null to bool + $x = null; + settype($x, 'bool'); + assertType('false', $x); + + // null to boolean + $x = null; + settype($x, 'boolean'); + assertType('false', $x); + + // null to array + $x = null; + settype($x, 'array'); + assertType('array{}', $x); + + // null to object + $x = null; + settype($x, 'object'); + assertType('stdClass', $x); + + // null to null + $x = null; + settype($x, 'null'); + assertType('null', $x); + + // Mixed to non-constant. + settype($value, $castTo); + assertType("array|bool|float|int|stdClass|string|null", $value); +} diff --git a/tests/PHPStan/Analyser/data/shuffle.php b/tests/PHPStan/Analyser/data/shuffle.php index 2b3ce76a00..c2bf853776 100644 --- a/tests/PHPStan/Analyser/data/shuffle.php +++ b/tests/PHPStan/Analyser/data/shuffle.php @@ -2,57 +2,140 @@ namespace Shuffle; +use function PHPStan\Testing\assertNativeType; use function PHPStan\Testing\assertType; class Foo { - public function normalArrays(array $arr): void + public function normalArrays1(array $arr): void { /** @var mixed[] $arr */ shuffle($arr); - assertType('array', $arr); - assertType('array', array_keys($arr)); - assertType('array', array_values($arr)); + assertType('list', $arr); + assertNativeType('list', $arr); + assertType('list>', array_keys($arr)); + assertType('list', array_values($arr)); + } + public function normalArrays2(array $arr): void + { /** @var non-empty-array $arr */ shuffle($arr); - assertType('non-empty-array', $arr); - assertType('non-empty-array', array_keys($arr)); - assertType('non-empty-array', array_values($arr)); + assertType('non-empty-list', $arr); + assertNativeType('list', $arr); + assertType('non-empty-list>', array_keys($arr)); + assertType('non-empty-list', array_values($arr)); + } + + public function normalArrays3(array $arr): void + { + /** @var array $arr */ + if (array_key_exists('foo', $arr)) { + shuffle($arr); + assertType('non-empty-list', $arr); + assertNativeType('non-empty-list', $arr); + assertType('non-empty-list>', array_keys($arr)); + assertType('non-empty-list', array_values($arr)); + } + } + + public function normalArrays4(array $arr): void + { + /** @var array $arr */ + if (array_key_exists('foo', $arr) && $arr['foo'] === 'bar') { + shuffle($arr); + assertType('non-empty-list', $arr); + assertNativeType('non-empty-list', $arr); + assertType('non-empty-list>', array_keys($arr)); + assertType('non-empty-list', array_values($arr)); + } } - public function constantArrays(array $arr): void + public function constantArrays1(array $arr): void { - /** @var array{} $arr */ + $arr = []; shuffle($arr); assertType('array{}', $arr); + assertNativeType('array{}', $arr); assertType('array{}', array_keys($arr)); assertType('array{}', array_values($arr)); + } + public function constantArrays2(array $arr): void + { /** @var array{0?: 1, 1?: 2, 2?: 3} $arr */ shuffle($arr); - assertType('array<0|1|2, 1|2|3>', $arr); - assertType('array', array_keys($arr)); - assertType('array', array_values($arr)); + assertType('array<0|1|2, 1|2|3>&list', $arr); + assertNativeType('list', $arr); + assertType('list<0|1|2>', array_keys($arr)); + assertType('list<1|2|3>', array_values($arr)); + } - /** @var array{1, 2, 3} $arr */ + public function constantArrays3(array $arr): void + { + $arr = [1, 2, 3]; shuffle($arr); - assertType('non-empty-array<0|1|2, 1|2|3>', $arr); - assertType('non-empty-array', array_keys($arr)); - assertType('non-empty-array', array_values($arr)); + assertType('non-empty-array<0|1|2, 1|2|3>&list', $arr); + assertNativeType('non-empty-array<0|1|2, 1|2|3>&list', $arr); + assertType('non-empty-list<0|1|2>', array_keys($arr)); + assertType('non-empty-list<1|2|3>', array_values($arr)); + } - /** @var array{a: 1, b: 2, c: 3} $arr */ + public function constantArrays4(array $arr): void + { + $arr = ['a' => 1, 'b' => 2, 'c' => 3]; shuffle($arr); - assertType('non-empty-array<0|1|2, 1|2|3>', $arr); - assertType('non-empty-array', array_keys($arr)); - assertType('non-empty-array', array_values($arr)); + assertType('non-empty-array<0|1|2, 1|2|3>&list', $arr); + assertNativeType('non-empty-array<0|1|2, 1|2|3>&list', $arr); + assertType('non-empty-list<0|1|2>', array_keys($arr)); + assertType('non-empty-list<1|2|3>', array_values($arr)); + } - /** @var array{0: 1, 3: 2, 42: 3} $arr */ + public function constantArrays5(array $arr): void + { + $arr = [0 => 1, 3 => 2, 42 => 3]; shuffle($arr); - assertType('non-empty-array<0|1|2, 1|2|3>', $arr); - assertType('non-empty-array', array_keys($arr)); - assertType('non-empty-array', array_values($arr)); + assertType('non-empty-array<0|1|2, 1|2|3>&list', $arr); + assertNativeType('non-empty-array<0|1|2, 1|2|3>&list', $arr); + assertType('non-empty-list<0|1|2>', array_keys($arr)); + assertType('non-empty-list<1|2|3>', array_values($arr)); + } + + public function constantArrays6(array $arr): void + { + /** @var array{foo?: 1, bar: 2, }|array{baz: 3, foobar?: 4} $arr */ + shuffle($arr); + assertType('non-empty-array<0|1, 1|2|3|4>&list', $arr); + assertNativeType('list', $arr); + assertType('non-empty-list<0|1>', array_keys($arr)); + assertType('non-empty-list<1|2|3|4>', array_values($arr)); + } + + public function mixed($arr): void + { + shuffle($arr); + assertType('list', $arr); + assertNativeType('list', $arr); + assertType('list>', array_keys($arr)); + assertType('list', array_values($arr)); + } + + public function subtractedArray($arr): void + { + if (is_array($arr)) { + shuffle($arr); + assertType('list', $arr); + assertNativeType('list', $arr); + assertType('list>', array_keys($arr)); + assertType('list', array_values($arr)); + } else { + shuffle($arr); + assertType('*ERROR*', $arr); + assertNativeType('*ERROR*', $arr); + assertType('list', array_keys($arr)); + assertType('list', array_values($arr)); + } } } diff --git a/tests/PHPStan/Analyser/data/sizeof-php8.php b/tests/PHPStan/Analyser/data/sizeof-php8.php new file mode 100644 index 0000000000..0af3b4062c --- /dev/null +++ b/tests/PHPStan/Analyser/data/sizeof-php8.php @@ -0,0 +1,63 @@ + 1, + 'five' => 5, + 'three' => 3, + ]; + + $arr1 = $arr; + sort($arr1); + assertType('non-empty-list<1|3|4|5>', $arr1); + assertNativeType('non-empty-list<1|3|4|5>', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('non-empty-list<1|3|4|5>', $arr2); + assertNativeType('non-empty-list<1|3|4|5>', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('non-empty-list<1|3|4|5>', $arr3); + assertNativeType('non-empty-list<1|3|4|5>', $arr3); + } + + public function constantArrayOptionalKey(): void + { + $arr = [ + 'one' => 1, + 'five' => 5, + ]; + if (rand(0, 1)) { + $arr['two'] = 2; + } + + $arr1 = $arr; + sort($arr1); + assertType('non-empty-list<1|2|5>', $arr1); + assertNativeType('non-empty-list<1|2|5>', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('non-empty-list<1|2|5>', $arr2); + assertNativeType('non-empty-list<1|2|5>', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('non-empty-list<1|2|5>', $arr3); + assertNativeType('non-empty-list<1|2|5>', $arr3); + } + + public function constantArrayUnion(): void + { + $arr = rand(0, 1) ? [ + 'one' => 1, + 'five' => 5, + ] : [ + 'two' => 2, + ]; + + $arr1 = $arr; + sort($arr1); + assertType('non-empty-list<1|2|5>', $arr1); + assertNativeType('non-empty-list<1|2|5>', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('non-empty-list<1|2|5>', $arr2); + assertNativeType('non-empty-list<1|2|5>', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('non-empty-list<1|2|5>', $arr3); + assertNativeType('non-empty-list<1|2|5>', $arr3); + } + + /** @param array $arr */ + public function normalArray(array $arr): void + { + $arr1 = $arr; + sort($arr1); + assertType('list', $arr1); + assertNativeType('list', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('list', $arr2); + assertNativeType('list', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('list', $arr3); + assertNativeType('list', $arr3); + } + + public function mixed($arr): void + { + $arr1 = $arr; + sort($arr1); + assertType('mixed', $arr1); + assertNativeType('mixed', $arr1); + + $arr2 = $arr; + rsort($arr2); + assertType('mixed', $arr2); + assertNativeType('mixed', $arr2); + + $arr3 = $arr; + usort($arr3, fn(int $a, int $b) => $a <=> $b); + assertType('mixed', $arr3); + assertNativeType('mixed', $arr3); + } + + public function notArray(): void + { + $arr = 'foo'; + sort($arr); + assertType("'foo'", $arr); + } +} + +class Bar +{ + + /** + * @template T + * @param T&list $array + * @return list + */ + public function doFoo(array $array) + { + assertType('list&T (method Sort\Bar::doFoo(), argument)', $array); + usort($array, function (array $a, array $b) { + return $a['a'] <=> $b['a']; + }); + + assertType('list&T (method Sort\Bar::doFoo(), argument)', $array); + + return $array; + } + +} diff --git a/tests/PHPStan/Analyser/data/specified-types-closure-edge.php b/tests/PHPStan/Analyser/data/specified-types-closure-edge.php new file mode 100644 index 0000000000..f4e396bb79 --- /dev/null +++ b/tests/PHPStan/Analyser/data/specified-types-closure-edge.php @@ -0,0 +1,26 @@ + 'b'])); + assertType('string', strtr($s, ['f' => 'b', 'o' => 'a'])); + + assertType('string', strtr($s, $s, $nonEmptyString)); + assertType('string', strtr($s, $nonEmptyString, $nonEmptyString)); + assertType('string', strtr($s, $nonFalseyString, $nonFalseyString)); + + assertType('non-empty-string', strtr($nonEmptyString, $s, $nonEmptyString)); + assertType('non-empty-string', strtr($nonEmptyString, $nonEmptyString, $nonEmptyString)); + assertType('non-empty-string', strtr($nonEmptyString, $nonFalseyString, $nonFalseyString)); + + assertType('non-empty-string', strtr($nonFalseyString, $s, $nonEmptyString)); + assertType('non-falsy-string', strtr($nonFalseyString, $nonEmptyString, $nonFalseyString)); + assertType('non-falsy-string', strtr($nonFalseyString, $nonFalseyString, $nonFalseyString)); +} diff --git a/tests/PHPStan/Analyser/data/strval.php b/tests/PHPStan/Analyser/data/strval.php index e3b9a1fd42..870aafd6b8 100644 --- a/tests/PHPStan/Analyser/data/strval.php +++ b/tests/PHPStan/Analyser/data/strval.php @@ -23,6 +23,9 @@ function strvalTest(string $string, string $class): void assertType('class-string', strval($class)); assertType('string', strval(new \Exception())); assertType('*ERROR*', strval(new \stdClass())); + assertType('*ERROR*', strval([])); + assertType('*ERROR*', strval(function() {})); + assertType('string', strval(fopen('php://memory', 'r'))); } function intvalTest(string $string): void @@ -40,6 +43,9 @@ function intvalTest(string $string): void assertType('int', intval(rand() * 0.1)); assertType('0', intval([])); assertType('1', intval([null])); + assertType('int', intval(new \stdClass())); + assertType('int', intval(function() {})); + assertType('int', intval(fopen('php://memory', 'r'))); } function floatvalTest(string $string): void @@ -57,6 +63,9 @@ function floatvalTest(string $string): void assertType('float', floatval(rand() * 0.1)); assertType('0.0', floatval([])); assertType('1.0', floatval([null])); + assertType('float', floatval(new \stdClass())); + assertType('float', floatval(function() {})); + assertType('float', floatval(fopen('php://memory', 'r'))); } function boolvalTest(string $string): void @@ -75,6 +84,8 @@ function boolvalTest(string $string): void assertType('false', boolval([])); assertType('true', boolval([null])); assertType('true', boolval(new \stdClass())); + assertType('true', boolval(function() {})); + assertType('bool', boolval(fopen('php://memory', 'r'))); } function arrayTest(array $a): void diff --git a/tests/PHPStan/Analyser/data/template-null-bound.php b/tests/PHPStan/Analyser/data/template-null-bound.php index 66f1346914..3456f02a09 100644 --- a/tests/PHPStan/Analyser/data/template-null-bound.php +++ b/tests/PHPStan/Analyser/data/template-null-bound.php @@ -21,6 +21,6 @@ public function doFoo(?int $p): ?int function (Foo $f, ?int $i): void { assertType('null', $f->doFoo(null)); - assertType('int', $f->doFoo(1)); + assertType('1', $f->doFoo(1)); assertType('int|null', $f->doFoo($i)); }; diff --git a/tests/PHPStan/Analyser/data/throw-expr.php b/tests/PHPStan/Analyser/data/throw-expr.php index 581e8b1d3e..2893fe4ee7 100644 --- a/tests/PHPStan/Analyser/data/throw-expr.php +++ b/tests/PHPStan/Analyser/data/throw-expr.php @@ -15,7 +15,7 @@ public function doFoo(bool $b): void public function doBar(): void { - assertType('*NEVER*', throw new \Exception()); + assertType('never', throw new \Exception()); } } diff --git a/tests/PHPStan/Analyser/data/trait-type-alias.php b/tests/PHPStan/Analyser/data/trait-type-alias.php new file mode 100644 index 0000000000..b83fe210fa --- /dev/null +++ b/tests/PHPStan/Analyser/data/trait-type-alias.php @@ -0,0 +1,54 @@ +', $parameter); + assertType('array', $parameter); } /** @@ -134,7 +134,7 @@ public function invalidImports($parameter1, $parameter2, $parameter3) */ public function conflictingAlias($parameter) { - assertType('*NEVER*', $parameter); + assertType('never', $parameter); } public function __get(string $name) @@ -151,7 +151,7 @@ public function testIntAlias($int) } assertType('int|string', (new Foo)->globalAliasProperty); - assertType('callable(string): string|false', (new Foo)->localAliasProperty); + assertType('callable(string): (string|false)', (new Foo)->localAliasProperty); assertType('Countable&Traversable', (new Foo)->importedAliasProperty); assertType('Countable&Traversable', (new Foo)->reexportedAliasProperty); assertType('TypeAliasesDataset\SubScope\Foo', (new Foo)->scopedAliasProperty); diff --git a/tests/PHPStan/Analyser/data/var-in-and-out-of-function.php b/tests/PHPStan/Analyser/data/var-in-and-out-of-function.php new file mode 100644 index 0000000000..6550d139b7 --- /dev/null +++ b/tests/PHPStan/Analyser/data/var-in-and-out-of-function.php @@ -0,0 +1,25 @@ +', strlen($constUnionMixed)); assertType('3', strlen(123)); assertType('1', strlen(true)); diff --git a/tests/PHPStan/Analyser/expression-type-resolver-extension.neon b/tests/PHPStan/Analyser/expression-type-resolver-extension.neon new file mode 100644 index 0000000000..de0f92640b --- /dev/null +++ b/tests/PHPStan/Analyser/expression-type-resolver-extension.neon @@ -0,0 +1,7 @@ +# config for ExpressionTypeResolverExtensionTest +services: + - + class: ExpressionTypeResolverExtension\MethodCallReturnsBoolExpressionTypeResolverExtension + tags: + - phpstan.broker.expressionTypeResolverExtension + diff --git a/tests/PHPStan/Analyser/looseConstComparisonPhp7.neon b/tests/PHPStan/Analyser/looseConstComparisonPhp7.neon new file mode 100644 index 0000000000..b41c80f2a6 --- /dev/null +++ b/tests/PHPStan/Analyser/looseConstComparisonPhp7.neon @@ -0,0 +1,2 @@ +parameters: + phpVersion: 70400 # PHP 7.4 diff --git a/tests/PHPStan/Analyser/looseConstComparisonPhp8.neon b/tests/PHPStan/Analyser/looseConstComparisonPhp8.neon new file mode 100644 index 0000000000..0ed5d49f80 --- /dev/null +++ b/tests/PHPStan/Analyser/looseConstComparisonPhp8.neon @@ -0,0 +1,2 @@ +parameters: + phpVersion: 80000 # PHP 8.0 diff --git a/tests/PHPStan/Analyser/param-closure-this-stubs.neon b/tests/PHPStan/Analyser/param-closure-this-stubs.neon new file mode 100644 index 0000000000..bbfb15154d --- /dev/null +++ b/tests/PHPStan/Analyser/param-closure-this-stubs.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - data/param-closure-this-stubs.stub diff --git a/tests/PHPStan/Analyser/param-out.neon b/tests/PHPStan/Analyser/param-out.neon new file mode 100644 index 0000000000..8d3fe24304 --- /dev/null +++ b/tests/PHPStan/Analyser/param-out.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - param-out.stub diff --git a/tests/PHPStan/Analyser/param-out.stub b/tests/PHPStan/Analyser/param-out.stub new file mode 100644 index 0000000000..297e1620fb --- /dev/null +++ b/tests/PHPStan/Analyser/param-out.stub @@ -0,0 +1,21 @@ +foo(); + $this->x = 5; + $this->y = 5; + $this->z = 5; + } +} diff --git a/tests/PHPStan/Analyser/traits/uninitializedProperty/FooTrait.php b/tests/PHPStan/Analyser/traits/uninitializedProperty/FooTrait.php new file mode 100644 index 0000000000..d216f94304 --- /dev/null +++ b/tests/PHPStan/Analyser/traits/uninitializedProperty/FooTrait.php @@ -0,0 +1,19 @@ += 8.1 + +namespace TraitsUnititializedProperty; + +trait FooTrait +{ + protected readonly int $x; + + /** @readonly */ + protected int $y; + protected int $z; + + public function foo(): void + { + echo $this->x; + echo $this->y; + echo $this->z; + } +} diff --git a/tests/PHPStan/Analyser/usePathConstantsAsConstantString.neon b/tests/PHPStan/Analyser/usePathConstantsAsConstantString.neon new file mode 100644 index 0000000000..12225ce6e6 --- /dev/null +++ b/tests/PHPStan/Analyser/usePathConstantsAsConstantString.neon @@ -0,0 +1,2 @@ +parameters: + usePathConstantsAsConstantString: true diff --git a/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php b/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php index 256d969238..a7cb8997d8 100644 --- a/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php +++ b/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php @@ -77,6 +77,7 @@ private function runPath(string $path, int $expectedStatusCode): string ), false, null, + null, ); $analysisResult = $analyserApplication->analyse( [$path], @@ -93,7 +94,6 @@ private function runPath(string $path, int $expectedStatusCode): string unlink($memoryLimitFile); } $statusCode = $errorFormatter->formatErrors($analysisResult, $symfonyOutput); - $this->assertSame($expectedStatusCode, $statusCode); rewind($output->getStream()); @@ -102,6 +102,8 @@ private function runPath(string $path, int $expectedStatusCode): string throw new ShouldNotHappenException(); } + $this->assertSame($expectedStatusCode, $statusCode, $contents); + return $contents; } diff --git a/tests/PHPStan/Command/AnalyseCommandTest.php b/tests/PHPStan/Command/AnalyseCommandTest.php index 68abcd2875..7a548a5d54 100644 --- a/tests/PHPStan/Command/AnalyseCommandTest.php +++ b/tests/PHPStan/Command/AnalyseCommandTest.php @@ -50,12 +50,24 @@ public function testInvalidAutoloadFile(): void public function testValidAutoloadFile(): void { + $originalDir = getcwd(); + if ($originalDir === false) { + throw new ShouldNotHappenException(); + } + $autoloadFile = __DIR__ . DIRECTORY_SEPARATOR . 'data/autoload-file.php'; - $output = $this->runCommand(0, ['--autoload-file' => $autoloadFile]); - $this->assertStringContainsString('[OK] No errors', $output); - $this->assertStringNotContainsString(sprintf('Autoload file "%s" not found.' . PHP_EOL, $autoloadFile), $output); - $this->assertSame('magic value', SOME_CONSTANT_IN_AUTOLOAD_FILE); + chdir(__DIR__); + + try { + $output = $this->runCommand(0, ['--autoload-file' => $autoloadFile]); + $this->assertStringContainsString('[OK] No errors', $output); + $this->assertStringNotContainsString(sprintf('Autoload file "%s" not found.' . PHP_EOL, $autoloadFile), $output); + $this->assertSame('magic value', SOME_CONSTANT_IN_AUTOLOAD_FILE); + } catch (Throwable $e) { + chdir($originalDir); + throw $e; + } } /** @@ -96,9 +108,10 @@ private function runCommand(int $expectedStatusCode, array $parameters = []): st $commandTester->execute([ 'paths' => [__DIR__ . DIRECTORY_SEPARATOR . 'test'], - ] + $parameters); + '--debug' => true, + ] + $parameters, ['debug' => true]); - $this->assertSame($expectedStatusCode, $commandTester->getStatusCode()); + $this->assertSame($expectedStatusCode, $commandTester->getStatusCode(), $commandTester->getDisplay()); return $commandTester->getDisplay(); } diff --git a/tests/PHPStan/Command/AnalysisResultTest.php b/tests/PHPStan/Command/AnalysisResultTest.php index 8ec17f2a8e..2ea3344a9b 100644 --- a/tests/PHPStan/Command/AnalysisResultTest.php +++ b/tests/PHPStan/Command/AnalysisResultTest.php @@ -43,6 +43,9 @@ public function testErrorsAreSortedByFileNameAndLine(): void false, null, true, + 0, + false, + [], ))->getFileSpecificErrors(), ); } diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php index ea66e724ba..01fd649bf0 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php @@ -148,6 +148,9 @@ public function testFormatErrorMessagesRegexEscape(): void false, null, true, + 0, + false, + [], ); $formatter->formatErrors( $result, @@ -185,6 +188,9 @@ public function testEscapeDiNeon(): void false, null, true, + 0, + false, + [], ); $formatter->formatErrors( @@ -249,6 +255,9 @@ public function testOutputOrdering(array $errors): void false, null, true, + 0, + false, + [], ); $formatter->formatErrors( @@ -406,6 +415,9 @@ public function testEndOfFileNewlines( false, null, true, + 0, + false, + [], ); $resource = fopen('php://memory', 'w', false); diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php new file mode 100644 index 0000000000..b49c717f22 --- /dev/null +++ b/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php @@ -0,0 +1,174 @@ + '#^Bar$#', + 'count' => 1, + 'path' => __DIR__ . '/../Foo.php', +]; +\$ignoreErrors[] = [ + 'message' => '#^Foo$#', + 'count' => 2, + 'path' => __DIR__ . '/Foo.php', +]; + +return ['parameters' => ['ignoreErrors' => \$ignoreErrors]]; +", + ]; + + yield [ + [ + new Error( + 'Foo', + __DIR__ . '/Foo.php', + 5, + ), + new Error( + 'Foo', + __DIR__ . '/Foo.php', + 5, + ), + (new Error( + 'Foo with identifier', + __DIR__ . '/Foo.php', + 5, + ))->withIdentifier('argument.type'), + (new Error( + 'Foo with identifier', + __DIR__ . '/Foo.php', + 6, + ))->withIdentifier('argument.type'), + ], + " '#^Foo$#', + 'count' => 2, + 'path' => __DIR__ . '/Foo.php', +]; +\$ignoreErrors[] = [ + // identifier: argument.type + 'message' => '#^Foo with identifier$#', + 'count' => 2, + 'path' => __DIR__ . '/Foo.php', +]; + +return ['parameters' => ['ignoreErrors' => \$ignoreErrors]]; +", + ]; + + yield [ + [ + new Error( + 'Foo', + __DIR__ . '/Foo.php', + 5, + ), + new Error( + 'Foo', + __DIR__ . '/Foo.php', + 5, + ), + (new Error( + 'Foo with same message, different identifier', + __DIR__ . '/Foo.php', + 5, + ))->withIdentifier('argument.type'), + (new Error( + 'Foo with same message, different identifier', + __DIR__ . '/Foo.php', + 6, + ))->withIdentifier('argument.byRef'), + (new Error( + 'Foo with another message', + __DIR__ . '/Foo.php', + 5, + ))->withIdentifier('argument.type'), + ], + " '#^Foo$#', + 'count' => 2, + 'path' => __DIR__ . '/Foo.php', +]; +\$ignoreErrors[] = [ + // identifier: argument.type + 'message' => '#^Foo with another message$#', + 'count' => 1, + 'path' => __DIR__ . '/Foo.php', +]; +\$ignoreErrors[] = [ + // identifiers: argument.byRef, argument.type + 'message' => '#^Foo with same message, different identifier$#', + 'count' => 2, + 'path' => __DIR__ . '/Foo.php', +]; + +return ['parameters' => ['ignoreErrors' => \$ignoreErrors]]; +", + ]; + } + + /** + * @dataProvider dataFormatErrors + * @param list $errors + */ + public function testFormatErrors(array $errors, string $expectedOutput): void + { + $formatter = new BaselinePhpErrorFormatter(new ParentDirectoryRelativePathHelper(__DIR__)); + $formatter->formatErrors( + new AnalysisResult( + $errors, + [], + [], + [], + [], + false, + null, + true, + 0, + true, + [], + ), + $this->getOutput(), + ); + + $this->assertSame($expectedOutput, $this->getOutputContent()); + } + +} diff --git a/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php index 04d4996963..0b8593d203 100644 --- a/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php @@ -154,6 +154,9 @@ public function testTraitPath(): void false, null, true, + 0, + false, + [], ), $this->getOutput()); $this->assertXmlStringEqualsXmlString(' @@ -162,4 +165,35 @@ public function testTraitPath(): void ', $this->getOutputContent()); } + public function testIdentifier(): void + { + $formatter = new CheckstyleErrorFormatter(new SimpleRelativePathHelper(__DIR__)); + $error = (new Error( + 'Foo', + __DIR__ . '/FooTrait.php', + 5, + true, + __DIR__ . '/Foo.php', + null, + ))->withIdentifier('argument.type'); + $formatter->formatErrors(new AnalysisResult( + [$error], + [], + [], + [], + [], + false, + null, + true, + 0, + true, + [], + ), $this->getOutput()); + $this->assertXmlStringEqualsXmlString(' + + + +', $this->getOutputContent()); + } + } diff --git a/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php index 17a687af4c..c936bf48cc 100644 --- a/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php @@ -2,6 +2,9 @@ namespace PHPStan\Command\ErrorFormatter; +use Nette\Utils\Json; +use PHPStan\Analyser\Error; +use PHPStan\Command\AnalysisResult; use PHPStan\Testing\ErrorFormatterTestCase; use function sprintf; @@ -235,4 +238,26 @@ public function testFormatErrors( $this->assertJsonStringEqualsJsonString($expected, $this->getOutputContent(), sprintf('%s: JSON do not match', $message)); } + public function dataFormatTip(): iterable + { + yield ['tip', 'tip']; + yield ['%configurationFile%', '%configurationFile%']; + yield ['this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', 'this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.']; + } + + /** + * @dataProvider dataFormatTip + */ + public function testFormatTip(string $tip, string $expectedTip): void + { + $formatter = new JsonErrorFormatter(false); + $formatter->formatErrors(new AnalysisResult([ + new Error('Foo', '/foo/bar.php', 1, true, null, null, $tip), + ], [], [], [], [], false, null, true, 0, false, []), $this->getOutput()); + + $content = $this->getOutputContent(); + $json = Json::decode($content, Json::FORCE_ARRAY); + $this->assertSame($expectedTip, $json['files']['/foo/bar.php']['messages'][0]['tip']); + } + } diff --git a/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php index f83f2de850..64facd9af9 100644 --- a/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php @@ -8,6 +8,7 @@ use PHPStan\File\NullRelativePathHelper; use PHPStan\File\SimpleRelativePathHelper; use PHPStan\Testing\ErrorFormatterTestCase; +use function getenv; use function putenv; use function sprintf; use const PHP_VERSION_ID; @@ -23,27 +24,30 @@ protected function setUp(): void protected function tearDown(): void { putenv('COLUMNS'); + putenv('TERM_PROGRAM'); } public function dataFormatterOutputProvider(): iterable { yield [ - 'No errors', - 0, - 0, - 0, - ' + 'message' => 'No errors', + 'exitCode' => 0, + 'numFileErrors' => 0, + 'numGenericErrors' => 0, + 'extraEnvVars' => [], + 'expected' => ' [OK] No errors ', ]; yield [ - 'One file error', - 1, - 1, - 0, - ' ------ ------------------------------------------------------------------- + 'message' => 'One file error', + 'exitCode' => 1, + 'numFileErrors' => 1, + 'numGenericErrors' => 0, + 'extraEnvVars' => [], + 'expected' => ' ------ ------------------------------------------------------------------- Line folder with unicode 😃/file name with "spaces" and unicode 😃.php ------ ------------------------------------------------------------------- 4 Foo @@ -56,11 +60,12 @@ public function dataFormatterOutputProvider(): iterable ]; yield [ - 'One generic error', - 1, - 0, - 1, - ' -- --------------------- + 'message' => 'One generic error', + 'exitCode' => 1, + 'numFileErrors' => 0, + 'numGenericErrors' => 1, + 'extraEnvVars' => [], + 'expected' => ' -- --------------------- Error -- --------------------- first generic error @@ -73,11 +78,12 @@ public function dataFormatterOutputProvider(): iterable ]; yield [ - 'Multiple file errors', - 1, - 4, - 0, - ' ------ ------------------------------------------------------------------- + 'message' => 'Multiple file errors', + 'exitCode' => 1, + 'numFileErrors' => 4, + 'numGenericErrors' => 0, + 'extraEnvVars' => [], + 'expected' => ' ------ ------------------------------------------------------------------- Line folder with unicode 😃/file name with "spaces" and unicode 😃.php ------ ------------------------------------------------------------------- 2 Bar @@ -100,11 +106,12 @@ public function dataFormatterOutputProvider(): iterable ]; yield [ - 'Multiple generic errors', - 1, - 0, - 2, - ' -- ---------------------- + 'message' => 'Multiple generic errors', + 'exitCode' => 1, + 'numFileErrors' => 0, + 'numGenericErrors' => 2, + 'extraEnvVars' => [], + 'expected' => ' -- ---------------------- Error -- ---------------------- first generic error @@ -118,11 +125,12 @@ public function dataFormatterOutputProvider(): iterable ]; yield [ - 'Multiple file, multiple generic errors', - 1, - 4, - 2, - ' ------ ------------------------------------------------------------------- + 'message' => 'Multiple file, multiple generic errors', + 'exitCode' => 1, + 'numFileErrors' => 4, + 'numGenericErrors' => 2, + 'extraEnvVars' => [], + 'expected' => ' ------ ------------------------------------------------------------------- Line folder with unicode 😃/file name with "spaces" and unicode 😃.php ------ ------------------------------------------------------------------- 2 Bar @@ -148,19 +156,38 @@ public function dataFormatterOutputProvider(): iterable [ERROR] Found 6 errors +', + ]; + + yield [ + 'message' => 'One file error, called via Visual Studio Code', + 'exitCode' => 1, + 'numFileErrors' => 1, + 'numGenericErrors' => 0, + 'extraEnvVars' => ['TERM_PROGRAM=vscode'], + 'expected' => ' ------ ------------------------------------------------------------------- + Line folder with unicode 😃/file name with "spaces" and unicode 😃.php + ------ ------------------------------------------------------------------- + :4 Foo + ------ ------------------------------------------------------------------- + + + [ERROR] Found 1 error + ', ]; } /** * @dataProvider dataFormatterOutputProvider - * + * @param array $extraEnvVars */ public function testFormatErrors( string $message, int $exitCode, int $numFileErrors, int $numGenericErrors, + array $extraEnvVars, string $expected, ): void { @@ -169,6 +196,11 @@ public function testFormatErrors( } $formatter = $this->createErrorFormatter(null); + // NOTE: extra env vars need to be cleared in tearDown() + foreach ($extraEnvVars as $envVar) { + putenv($envVar); + } + $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), $this->getOutput(), @@ -181,20 +213,33 @@ public function testEditorUrlWithTrait(): void { $formatter = $this->createErrorFormatter('editor://%file%/%line%'); $error = new Error('Test', 'Foo.php (in context of trait)', 12, true, 'Foo.php', 'Bar.php'); - $formatter->formatErrors(new AnalysisResult([$error], [], [], [], [], false, null, true), $this->getOutput()); + $formatter->formatErrors(new AnalysisResult([$error], [], [], [], [], false, null, true, 0, false, []), $this->getOutput()); $this->assertStringContainsString('Bar.php', $this->getOutputContent()); } public function testEditorUrlWithRelativePath(): void { + if (getenv('TERMINAL_EMULATOR') === 'JetBrains-JediTerm') { + $this->markTestSkipped('PhpStorm console does not support links in console.'); + } + $formatter = $this->createErrorFormatter('editor://custom/path/%relFile%/%line%'); $error = new Error('Test', 'Foo.php', 12, true, self::DIRECTORY_PATH . '/rel/Foo.php'); - $formatter->formatErrors(new AnalysisResult([$error], [], [], [], [], false, null, true), $this->getOutput(true)); + $formatter->formatErrors(new AnalysisResult([$error], [], [], [], [], false, null, true, 0, false, []), $this->getOutput(true)); $this->assertStringContainsString('editor://custom/path/rel/Foo.php', $this->getOutputContent(true)); } + public function testEditorUrlWithCustomTitle(): void + { + $formatter = $this->createErrorFormatter('editor://any', '%relFile%:%line%'); + $error = new Error('Test', 'Foo.php', 12, true, self::DIRECTORY_PATH . '/rel/Foo.php'); + $formatter->formatErrors(new AnalysisResult([$error], [], [], [], [], false, null, true, 0, false, []), $this->getOutput(true)); + + $this->assertStringContainsString('rel/Foo.php:12', $this->getOutputContent(true)); + } + public function testBug6727(): void { putenv('COLUMNS=30'); @@ -215,13 +260,16 @@ public function testBug6727(): void false, null, true, + 0, + false, + [], ), $this->getOutput(), ); self::expectNotToPerformAssertions(); } - private function createErrorFormatter(?string $editorUrl): TableErrorFormatter + private function createErrorFormatter(?string $editorUrl, ?string $editorUrlTitle = null): TableErrorFormatter { $relativePathHelper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), self::DIRECTORY_PATH, [], '/'); @@ -234,6 +282,7 @@ private function createErrorFormatter(?string $editorUrl): TableErrorFormatter ), false, $editorUrl, + $editorUrlTitle, ); } diff --git a/tests/PHPStan/Command/test/empty.php b/tests/PHPStan/Command/test/empty.php new file mode 100644 index 0000000000..3c6b265174 --- /dev/null +++ b/tests/PHPStan/Command/test/empty.php @@ -0,0 +1 @@ +skipIfNotOnWindows(); - $fileExcluder = new FileExcluder($this->getFileHelper(), new EmptyStubFilesProvider(), $analyseExcludes); + $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes); $this->assertSame($isExcluded, $fileExcluder->isExcludedFromAnalysing($filePath)); } @@ -128,7 +127,7 @@ public function testFilesAreExcludedFromAnalysingOnUnix( { $this->skipIfNotOnUnix(); - $fileExcluder = new FileExcluder($this->getFileHelper(), new EmptyStubFilesProvider(), $analyseExcludes); + $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes); $this->assertSame($isExcluded, $fileExcluder->isExcludedFromAnalysing($filePath)); } diff --git a/tests/PHPStan/File/FileHelperTest.php b/tests/PHPStan/File/FileHelperTest.php index 5cc0941fd7..409936a8d1 100644 --- a/tests/PHPStan/File/FileHelperTest.php +++ b/tests/PHPStan/File/FileHelperTest.php @@ -20,6 +20,8 @@ public function dataAbsolutizePathOnWindows(): array ['users', 'C:\abcd\users'], ['../lib', 'C:\abcd\../lib'], ['./lib', 'C:\abcd\./lib'], + ['vFs-v1.0://a\b', 'vFs-v1.0://a\b'], + ['./x://a\b', 'C:\abcd\./x://a\b'], ]; } @@ -47,6 +49,8 @@ public function dataAbsolutizePathOnLinuxOrMac(): array ['../lib', '/abcd/../lib'], ['./lib', '/abcd/./lib'], ['phar:///home/users/', 'phar:///home/users/'], + ['vFs-v1.0://a/b', 'vFs-v1.0://a/b'], + ['./x://a/b', '/abcd/./x://a/b'], ]; } @@ -73,6 +77,7 @@ public function dataNormalizePathOnWindows(): array ['/home/users/./phpstan', '\home\users\phpstan'], ['/home/users/../../phpstan/', '\phpstan'], ['./phpstan/', 'phpstan'], + ['vFs-v1.0://a/b', 'vfs-v1.0://a\b'], ]; } @@ -98,6 +103,7 @@ public function dataNormalizePathOnLinuxOrMac(): array ['/home/users/./phpstan', '/home/users/phpstan'], ['/home/users/../../phpstan/', '/phpstan'], ['./phpstan/', 'phpstan'], + ['vFs-v1.0://a/b', 'vfs-v1.0://a/b'], ['phar:///usr/local/bin/phpstan.phar/tmp/cache/../..', 'phar:///usr/local/bin/phpstan.phar'], ['phar:///usr/local/bin/phpstan.phar/tmp/cache/../../..', '/usr/local/bin'], ]; diff --git a/tests/PHPStan/File/RelativePathHelperTest.php b/tests/PHPStan/File/RelativePathHelperTest.php index 5dcb549b8d..5d5b3eca41 100644 --- a/tests/PHPStan/File/RelativePathHelperTest.php +++ b/tests/PHPStan/File/RelativePathHelperTest.php @@ -142,6 +142,40 @@ public function dataGetRelativePath(): array '/usr/app/src/analyzed.php', '/usr/app/src/analyzed.php', ], + [ + '/Users/ondrej/Downloads/phpstan-wtf/normal.php', + [ + '/Users/ondrej/Downloads/phpstan-wtf/normal.php/src', + ], + '/Users/ondrej/Downloads/phpstan-wtf/normal.php/src/index.php', + 'index.php', + ], + [ + '/Users/ondrej/Downloads/phpstan-wtf/normal', + [ + '/Users/ondrej/Downloads/phpstan-wtf/normal/src', + ], + '/Users/ondrej/Downloads/phpstan-wtf/normal/src/index.php', + 'index.php', + ], + [ + '/Users/ondrej/Downloads/phpstan-wtf/normal.php', + [ + '/Users/ondrej/Downloads/phpstan-wtf/normal.php/src', + '/Users/ondrej/Downloads/phpstan-wtf/normal.php/tests', + ], + '/Users/ondrej/Downloads/phpstan-wtf/normal.php/src/index.php', + 'src/index.php', + ], + [ + '/Users/ondrej/Downloads/phpstan-wtf/normal', + [ + '/Users/ondrej/Downloads/phpstan-wtf/normal/src', + '/Users/ondrej/Downloads/phpstan-wtf/normal/tests', + ], + '/Users/ondrej/Downloads/phpstan-wtf/normal/src/index.php', + 'src/index.php', + ], ]; } diff --git a/tests/PHPStan/Fixture/ManyCasesTestEnum.php b/tests/PHPStan/Fixture/ManyCasesTestEnum.php new file mode 100644 index 0000000000..6575f39e69 --- /dev/null +++ b/tests/PHPStan/Fixture/ManyCasesTestEnum.php @@ -0,0 +1,15 @@ += 8.1 + +namespace PHPStan\Fixture; + +enum ManyCasesTestEnum +{ + + case A; + case B; + case C; + case D; + case E; + case F; + +} diff --git a/tests/PHPStan/Generics/GenericsIntegrationTest.php b/tests/PHPStan/Generics/GenericsIntegrationTest.php index 27162c64a0..6f25d37cd0 100644 --- a/tests/PHPStan/Generics/GenericsIntegrationTest.php +++ b/tests/PHPStan/Generics/GenericsIntegrationTest.php @@ -19,6 +19,7 @@ public function dataTopics(): array ['varyingAcceptor'], ['classes'], ['variance'], + ['typeProjections'], ['bug2574'], ['bug2577'], ['bug2620'], diff --git a/tests/PHPStan/Generics/data/classes-4.json b/tests/PHPStan/Generics/data/classes-4.json new file mode 100644 index 0000000000..3b7dd01977 --- /dev/null +++ b/tests/PHPStan/Generics/data/classes-4.json @@ -0,0 +1,7 @@ +[ + { + "message": "Call to new PHPStan\\Generics\\Classes\\SomeRule() on a separate line has no effect.", + "line": 283, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Generics/data/typeProjections-0.json b/tests/PHPStan/Generics/data/typeProjections-0.json new file mode 100644 index 0000000000..548c221a62 --- /dev/null +++ b/tests/PHPStan/Generics/data/typeProjections-0.json @@ -0,0 +1,37 @@ +[ + { + "message": "Dumped type: PHPStan\\Generics\\TypeProjections\\B", + "line": 38, + "ignorable": false + }, + { + "message": "Dumped type: mixed", + "line": 48, + "ignorable": false + }, + { + "message": "Dumped type: PHPStan\\Generics\\TypeProjections\\A", + "line": 56, + "ignorable": false + }, + { + "message": "Dumped type: mixed", + "line": 65, + "ignorable": false + }, + { + "message": "Dumped type: PHPStan\\Generics\\TypeProjections\\B", + "line": 91, + "ignorable": false + }, + { + "message": "Dumped type: mixed", + "line": 105, + "ignorable": false + }, + { + "message": "Dumped type: mixed", + "line": 119, + "ignorable": false + } +] diff --git a/tests/PHPStan/Generics/data/typeProjections-5.json b/tests/PHPStan/Generics/data/typeProjections-5.json new file mode 100644 index 0000000000..ada4b861f0 --- /dev/null +++ b/tests/PHPStan/Generics/data/typeProjections-5.json @@ -0,0 +1,52 @@ +[ + { + "message": "Parameter #1 $item of method PHPStan\\Generics\\TypeProjections\\Box::pack() expects never, PHPStan\\Generics\\TypeProjections\\B given.", + "line": 37, + "ignorable": true + }, + { + "message": "Parameter #1 $item of method PHPStan\\Generics\\TypeProjections\\Box::pack() expects PHPStan\\Generics\\TypeProjections\\B, PHPStan\\Generics\\TypeProjections\\A given.", + "line": 46, + "ignorable": true + }, + { + "message": "Parameter #1 $item of method PHPStan\\Generics\\TypeProjections\\Box<*>::pack() expects never, PHPStan\\Generics\\TypeProjections\\A given.", + "line": 64, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped::mapOut() expects callable(): never, Closure(): PHPStan\\Generics\\TypeProjections\\A given.", + "line": 94, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped::mapOut() expects callable(): never, Closure(): PHPStan\\Generics\\TypeProjections\\B given.", + "line": 95, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped::mapOut() expects callable(): never, Closure(): PHPStan\\Generics\\TypeProjections\\C given.", + "line": 96, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped::mapOut() expects callable(): PHPStan\\Generics\\TypeProjections\\B, Closure(): PHPStan\\Generics\\TypeProjections\\A given.", + "line": 108, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped<*>::mapOut() expects callable(): never, Closure(): PHPStan\\Generics\\TypeProjections\\A given.", + "line": 122, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped<*>::mapOut() expects callable(): never, Closure(): PHPStan\\Generics\\TypeProjections\\B given.", + "line": 123, + "ignorable": true + }, + { + "message": "Parameter #1 $mapper of method PHPStan\\Generics\\TypeProjections\\Mapped<*>::mapOut() expects callable(): never, Closure(): PHPStan\\Generics\\TypeProjections\\C given.", + "line": 124, + "ignorable": true + } +] diff --git a/tests/PHPStan/Generics/data/typeProjections.php b/tests/PHPStan/Generics/data/typeProjections.php new file mode 100644 index 0000000000..8cfe1f92db --- /dev/null +++ b/tests/PHPStan/Generics/data/typeProjections.php @@ -0,0 +1,125 @@ += 7.4 + +namespace PHPStan\Generics\TypeProjections; + +use function PHPStan\dumpType; + +class A {} +class B extends A {} +class C extends B {} + +/** + * @template T + */ +interface Box +{ + /** @param T $item */ + public function pack(mixed $item): void; + + /** @return T */ + public function unpack(): mixed; +} + +/** + * @template T of A + */ +interface BoundedBox +{ + /** @return T */ + public function unpack(): mixed; +} + +/** + * @param Box $box + */ +function testCovariant(Box $box, B $b): void +{ + $box->pack($b); + dumpType($box->unpack()); +} + +/** + * @param Box $box + */ +function testContravariant(Box $box, A $a, B $b): void +{ + $box->pack($a); + $box->pack($b); + dumpType($box->unpack()); +} + +/** + * @param BoundedBox $box + */ +function testContravariantWithBound(BoundedBox $box): void +{ + dumpType($box->unpack()); +} + +/** + * @param Box<*> $box + */ +function testStar(Box $box, A $a): void +{ + $box->pack($a); + dumpType($box->unpack()); +} + + +/** + * @template T + */ +interface Mapped +{ + /** + * @param callable(T): void $mapper + */ + public function mapIn(callable $mapper): void; + + /** + * @param callable(): T $mapper + */ + public function mapOut(callable $mapper): void; +} + +/** + * @param Mapped $mapped + */ +function testCovariantMapped(Mapped $mapped): void +{ + $mapped->mapIn(function ($foo) { + dumpType($foo); + }); + + $mapped->mapOut(fn() => new A); + $mapped->mapOut(fn() => new B); + $mapped->mapOut(fn() => new C); +} + +/** + * @param Mapped $mapped + */ +function testContravariantMapped(Mapped $mapped): void +{ + $mapped->mapIn(function ($foo) { + dumpType($foo); + }); + + $mapped->mapOut(fn() => new A); + $mapped->mapOut(fn() => new B); + $mapped->mapOut(fn() => new C); +} + +/** + * @param Mapped<*> $mapped + */ +function testStarMapped(Mapped $mapped): void +{ + $mapped->mapIn(function ($foo) { + dumpType($foo); + }); + + $mapped->mapOut(fn() => new A); + $mapped->mapOut(fn() => new B); + $mapped->mapOut(fn() => new C); +} diff --git a/tests/PHPStan/Generics/data/variance-2.json b/tests/PHPStan/Generics/data/variance-2.json index 888e38af9c..e0db4f89f6 100644 --- a/tests/PHPStan/Generics/data/variance-2.json +++ b/tests/PHPStan/Generics/data/variance-2.json @@ -60,8 +60,13 @@ "ignorable": true }, { - "message": "Template type T is declared as covariant, but occurs in invariant position in parameter v of method PHPStan\\Generics\\Variance\\ConstructorAndStatic::__construct().", - "line": 142, + "message": "Template type T is declared as covariant, but occurs in contravariant position in parameter t of method PHPStan\\Generics\\Variance\\ConstructorAndStatic::create().", + "line": 153, + "ignorable": true + }, + { + "message": "Template type T is declared as covariant, but occurs in contravariant position in parameter w of method PHPStan\\Generics\\Variance\\ConstructorAndStatic::create().", + "line": 153, "ignorable": true }, { @@ -69,4 +74,4 @@ "line": 153, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php b/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php index 5a7b686525..df0dd443c2 100644 --- a/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php +++ b/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php @@ -5,7 +5,7 @@ use PHPStan\Testing\LevelsTestCase; /** - * @group exec + * @group levels */ class InferPrivatePropertyTypeFromConstructorIntegrationTest extends LevelsTestCase { diff --git a/tests/PHPStan/Levels/LevelsCheckAlwaysTrueIntegrationTest.php b/tests/PHPStan/Levels/LevelsCheckAlwaysTrueIntegrationTest.php deleted file mode 100644 index ec322d6b09..0000000000 --- a/tests/PHPStan/Levels/LevelsCheckAlwaysTrueIntegrationTest.php +++ /dev/null @@ -1,40 +0,0 @@ -= 80300) { + $topics[] = ['constantAccesses83']; + } + + return $topics; } public function getDataPath(): string diff --git a/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php b/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php index a247012c5e..854d9ded9e 100644 --- a/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php +++ b/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php @@ -5,7 +5,7 @@ use PHPStan\Testing\LevelsTestCase; /** - * @group exec + * @group levels */ class NamedArgumentsIntegrationTest extends LevelsTestCase { diff --git a/tests/PHPStan/Levels/StubValidatorIntegrationTest.php b/tests/PHPStan/Levels/StubValidatorIntegrationTest.php index 1c4dcf3142..f4138d043f 100644 --- a/tests/PHPStan/Levels/StubValidatorIntegrationTest.php +++ b/tests/PHPStan/Levels/StubValidatorIntegrationTest.php @@ -5,7 +5,7 @@ use PHPStan\Testing\LevelsTestCase; /** - * @group exec + * @group levels */ class StubValidatorIntegrationTest extends LevelsTestCase { diff --git a/tests/PHPStan/Levels/StubsIntegrationTest.php b/tests/PHPStan/Levels/StubsIntegrationTest.php index 11ad056599..dfec2e90f5 100644 --- a/tests/PHPStan/Levels/StubsIntegrationTest.php +++ b/tests/PHPStan/Levels/StubsIntegrationTest.php @@ -5,7 +5,7 @@ use PHPStan\Testing\LevelsTestCase; /** - * @group exec + * @group levels */ class StubsIntegrationTest extends LevelsTestCase { diff --git a/tests/PHPStan/Levels/alwaysTrue.neon b/tests/PHPStan/Levels/alwaysTrue.neon deleted file mode 100644 index b385d1c956..0000000000 --- a/tests/PHPStan/Levels/alwaysTrue.neon +++ /dev/null @@ -1,7 +0,0 @@ -includes: - - ../../../conf/bleedingEdge.neon - -parameters: - checkAlwaysTrueCheckTypeFunctionCall: true - checkAlwaysTrueInstanceof: true - checkAlwaysTrueStrictComparison: true diff --git a/tests/PHPStan/Levels/data/acceptTypes-4.json b/tests/PHPStan/Levels/data/acceptTypes-4.json new file mode 100644 index 0000000000..fbcb96fc5a --- /dev/null +++ b/tests/PHPStan/Levels/data/acceptTypes-4.json @@ -0,0 +1,22 @@ +[ + { + "message": "Call to method Levels\\AcceptTypes\\Baz::requireArray() on a separate line has no effect.", + "line": 531, + "ignorable": true + }, + { + "message": "Call to method Levels\\AcceptTypes\\Baz::requireFoo() on a separate line has no effect.", + "line": 532, + "ignorable": true + }, + { + "message": "Call to method Levels\\AcceptTypes\\Baz::requireArray() on a separate line has no effect.", + "line": 542, + "ignorable": true + }, + { + "message": "Call to method Levels\\AcceptTypes\\Baz::requireFoo() on a separate line has no effect.", + "line": 543, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes-5.json b/tests/PHPStan/Levels/data/acceptTypes-5.json index c9f5fdf981..88d4749a45 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-5.json +++ b/tests/PHPStan/Levels/data/acceptTypes-5.json @@ -94,16 +94,6 @@ "line": 251, "ignorable": true }, - { - "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface given.", - "line": 319, - "ignorable": true - }, - { - "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface given.", - "line": 320, - "ignorable": true - }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBar() expects int, float given.", "line": 412, @@ -164,6 +154,11 @@ "line": 585, "ignorable": true }, + { + "message": "Parameter #1 $static of method Levels\\AcceptTypes\\RequireObjectWithoutClassType::requireStatic() expects static(Levels\\AcceptTypes\\RequireObjectWithoutClassType), object given.", + "line": 648, + "ignorable": true + }, { "message": "Parameter #1 $min (0) of function random_int expects lower number than parameter #2 $max (-1).", "line": 671, @@ -184,21 +179,11 @@ "line": 707, "ignorable": true }, - { - "message": "Parameter #1 $numericString of method Levels\\AcceptTypes\\NumericStrings::doBar() expects numeric-string, string given.", - "line": 708, - "ignorable": true - }, { "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array{} given.", "line": 733, "ignorable": true }, - { - "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array given.", - "line": 735, - "ignorable": true - }, { "message": "Parameter #2 $array of function implode expects array|null, int given.", "line": 763, diff --git a/tests/PHPStan/Levels/data/acceptTypes-7.json b/tests/PHPStan/Levels/data/acceptTypes-7.json index 8345aca34c..9e0afc2f64 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-7.json +++ b/tests/PHPStan/Levels/data/acceptTypes-7.json @@ -19,11 +19,6 @@ "line": 92, "ignorable": true }, - { - "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Foo::doBarArray() expects array, array given.", - "line": 131, - "ignorable": true - }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Foo::doBarArray() expects array, array given.", "line": 132, @@ -40,25 +35,35 @@ "ignorable": true }, { - "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(Levels\\AcceptTypes\\FooInterface, mixed, mixed): Levels\\AcceptTypes\\FooImpl)|(Closure(): Levels\\AcceptTypes\\FooInterface) given.", + "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface, mixed, mixed): Levels\\AcceptTypes\\FooImpl) given.", "line": 283, "ignorable": true }, { - "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(Levels\\AcceptTypes\\FooInterface, mixed, mixed): Levels\\AcceptTypes\\FooImpl)|(Closure(): Levels\\AcceptTypes\\FooInterface) given.", + "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface, mixed, mixed): Levels\\AcceptTypes\\FooImpl) given.", "line": 284, "ignorable": true }, { - "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl)|(Closure(): Levels\\AcceptTypes\\FooInterface) given.", + "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl) given.", "line": 301, "ignorable": true }, { - "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl)|(Closure(): Levels\\AcceptTypes\\FooInterface) given.", + "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl) given.", "line": 302, "ignorable": true }, + { + "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface) given.", + "line": 319, + "ignorable": true + }, + { + "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface) given.", + "line": 320, + "ignorable": true + }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBar() expects int, float|int given.", "line": 415, @@ -134,11 +139,6 @@ "line": 647, "ignorable": true }, - { - "message": "Parameter #1 $static of method Levels\\AcceptTypes\\RequireObjectWithoutClassType::requireStatic() expects Levels\\AcceptTypes\\RequireObjectWithoutClassType, object given.", - "line": 648, - "ignorable": true - }, { "message": "Parameter #1 $min (int<-1, 1>) of function random_int expects lower number than parameter #2 $max (int<0, 1>).", "line": 690, @@ -154,6 +154,16 @@ "line": 692, "ignorable": true }, + { + "message": "Parameter #1 $numericString of method Levels\\AcceptTypes\\NumericStrings::doBar() expects numeric-string, string given.", + "line": 708, + "ignorable": true + }, + { + "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array given.", + "line": 735, + "ignorable": true + }, { "message": "Parameter #2 $array of function implode expects array|null, array|int|string given.", "line": 756, diff --git a/tests/PHPStan/Levels/data/acceptTypes-8.json b/tests/PHPStan/Levels/data/acceptTypes-8.json index 49eddfd569..10728d1f25 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-8.json +++ b/tests/PHPStan/Levels/data/acceptTypes-8.json @@ -14,6 +14,11 @@ "line": 91, "ignorable": true }, + { + "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Foo::doBarArray() expects array, array given.", + "line": 131, + "ignorable": true + }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBar() expects int, int|null given.", "line": 414, @@ -28,5 +33,15 @@ "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBarArray() expects array, array given.", "line": 495, "ignorable": true + }, + { + "message": "Method Levels\\AcceptTypes\\Discussion8209::test1() should return int but returns int|null.", + "line": 771, + "ignorable": true + }, + { + "message": "Method Levels\\AcceptTypes\\Discussion8209::test2() should return array but returns array.", + "line": 779, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes.php b/tests/PHPStan/Levels/data/acceptTypes.php index d16184f062..61b2c1fbbb 100644 --- a/tests/PHPStan/Levels/data/acceptTypes.php +++ b/tests/PHPStan/Levels/data/acceptTypes.php @@ -763,3 +763,19 @@ public function invalidType($invalid) { $imploded = implode('abc', $invalid); } } + +class Discussion8209 +{ + public function test1(?int $id): int + { + return $id; + } + + /** + * @return array + */ + public function test2(?int $id): array + { + return [$id]; + } +} diff --git a/tests/PHPStan/Levels/data/arrayDimFetches-7.json b/tests/PHPStan/Levels/data/arrayDimFetches-7.json index 3b315d40bf..8ff137110b 100644 --- a/tests/PHPStan/Levels/data/arrayDimFetches-7.json +++ b/tests/PHPStan/Levels/data/arrayDimFetches-7.json @@ -15,7 +15,7 @@ "ignorable": true }, { - "message": "Offset 'b' does not exist on array{a: 1, b?: 1}.", + "message": "Offset 'b' might not exist on array{a: 1, b?: 1}.", "line": 40, "ignorable": true }, @@ -29,4 +29,4 @@ "line": 58, "ignorable": true } -] +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/arrayDimFetches-8.json b/tests/PHPStan/Levels/data/arrayDimFetches-8.json index 9f4c150113..07568e3768 100644 --- a/tests/PHPStan/Levels/data/arrayDimFetches-8.json +++ b/tests/PHPStan/Levels/data/arrayDimFetches-8.json @@ -1,11 +1,11 @@ [ { - "message": "Offset 0 does not exist on array|null.", + "message": "Offset 0 might not exist on array|null.", "line": 15, "ignorable": true }, { - "message": "Offset 0 does not exist on array|null.", + "message": "Offset 0 might not exist on array|null.", "line": 50, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/binaryOps.php b/tests/PHPStan/Levels/data/binaryOps.php index 7b04fedbae..fd53d3f946 100644 --- a/tests/PHPStan/Levels/data/binaryOps.php +++ b/tests/PHPStan/Levels/data/binaryOps.php @@ -18,14 +18,14 @@ public function doFoo( $stringOrObject ) { - $int + $int; - $int + $intOrString; - $int + $stringOrObject; - $int + $string; - $string + $string; - $intOrString + $stringOrObject; - $intOrString + $string; - $stringOrObject + $stringOrObject; + $result = $int + $int; + $result = $int + $intOrString; + $result = $int + $stringOrObject; + $result = $int + $string; + $result = $string + $string; + $result = $intOrString + $stringOrObject; + $result = $intOrString + $string; + $result = $stringOrObject + $stringOrObject; } } diff --git a/tests/PHPStan/Levels/data/callableCalls.php b/tests/PHPStan/Levels/data/callableCalls.php index 4b22fa723c..52f311d8d8 100644 --- a/tests/PHPStan/Levels/data/callableCalls.php +++ b/tests/PHPStan/Levels/data/callableCalls.php @@ -23,7 +23,7 @@ public function doFoo( $c(); $d(); $f = function (int $i) { - + echo '1'; }; $f(1); $f(1.1); diff --git a/tests/PHPStan/Levels/data/callableVariance-4.json b/tests/PHPStan/Levels/data/callableVariance-4.json new file mode 100644 index 0000000000..1af09ec95a --- /dev/null +++ b/tests/PHPStan/Levels/data/callableVariance-4.json @@ -0,0 +1,27 @@ +[ + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 81, + "ignorable": true + }, + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 82, + "ignorable": true + }, + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 83, + "ignorable": true + }, + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 84, + "ignorable": true + }, + { + "message": "Call to function Levels\\CallableVariance\\d() on a separate line has no effect.", + "line": 85, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/callableVariance-5.json b/tests/PHPStan/Levels/data/callableVariance-5.json index 81682ac6e7..c37c7adeb0 100644 --- a/tests/PHPStan/Levels/data/callableVariance-5.json +++ b/tests/PHPStan/Levels/data/callableVariance-5.json @@ -1,6 +1,6 @@ [ { - "message": "Parameter #1 $ of callable callable(Levels\\CallableVariance\\B): void expects Levels\\CallableVariance\\B, Levels\\CallableVariance\\A given.", + "message": "Parameter #1 of callable callable(Levels\\CallableVariance\\B): void expects Levels\\CallableVariance\\B, Levels\\CallableVariance\\A given.", "line": 14, "ignorable": true }, @@ -39,4 +39,4 @@ "line": 85, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/clone.php b/tests/PHPStan/Levels/data/clone.php index 6bd72cfe38..4697debb13 100644 --- a/tests/PHPStan/Levels/data/clone.php +++ b/tests/PHPStan/Levels/data/clone.php @@ -26,14 +26,14 @@ public function doFoo( $mixed ) { - clone $int; - clone $intOrString; - clone $foo; - clone $nullableFoo; - clone $fooOrInt; - clone $nullableInt; - clone $nullableUnion; - clone $mixed; + $result = clone $int; + $result = clone $intOrString; + $result = clone $foo; + $result = clone $nullableFoo; + $result = clone $fooOrInt; + $result = clone $nullableInt; + $result = clone $nullableUnion; + $result = clone $mixed; } } diff --git a/tests/PHPStan/Levels/data/comparison.php b/tests/PHPStan/Levels/data/comparison.php index d208e6051c..ee10f825be 100644 --- a/tests/PHPStan/Levels/data/comparison.php +++ b/tests/PHPStan/Levels/data/comparison.php @@ -8,29 +8,29 @@ class Foo private const FOO_CONST = 'foo'; /** - * @param \stdClass $object + * @param \stdClass $object * @param int $int - * @param float $float + * @param float $float * @param string $string * @param int|string $intOrString * @param int|\stdClass $intOrObject */ public function doFoo( - \stdClass $object, + \stdClass $object, int $int, float $float, string $string, $intOrString, - $intOrObject + $intOrObject ) { - $object == $int; - $object == $float; - $object == $string; - $object == $intOrString; - $object == $intOrObject; + $result = $object == $int; + $result = $object == $float; + $result = $object == $string; + $result = $object == $intOrString; + $result = $object == $intOrObject; - self::FOO_CONST === 'bar'; + $result = self::FOO_CONST === 'bar'; } public function doBar(\ffmpeg_movie $movie): void diff --git a/tests/PHPStan/Levels/data/constantAccesses83-2.json b/tests/PHPStan/Levels/data/constantAccesses83-2.json new file mode 100644 index 0000000000..e36b08fc79 --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses83-2.json @@ -0,0 +1,7 @@ +[ + { + "message": "Class constant name in dynamic fetch can only be a string, int given.", + "line": 18, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/constantAccesses83-7.json b/tests/PHPStan/Levels/data/constantAccesses83-7.json new file mode 100644 index 0000000000..3dd562d1b3 --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses83-7.json @@ -0,0 +1,7 @@ +[ + { + "message": "Class constant name in dynamic fetch can only be a string, int|string given.", + "line": 19, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/constantAccesses83-8.json b/tests/PHPStan/Levels/data/constantAccesses83-8.json new file mode 100644 index 0000000000..e9c93a1ee7 --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses83-8.json @@ -0,0 +1,7 @@ +[ + { + "message": "Class constant name in dynamic fetch can only be a string, string|null given.", + "line": 20, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/constantAccesses83.php b/tests/PHPStan/Levels/data/constantAccesses83.php new file mode 100644 index 0000000000..ceb192d073 --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses83.php @@ -0,0 +1,21 @@ += 8.3 + +namespace Levels\ConstantAccesses83; + +class Foo +{ + + public const FOO_CONSTANT = 'foo'; + +} + +function (Foo $foo, string $a, int $i, int|string $is, ?string $sn): void { + echo Foo::FOO_CONSTANT; + + echo Foo::{$a}; + echo $foo::{$a}; + + echo Foo::{$i}; + echo Foo::{$is}; + echo Foo::{$sn}; +}; diff --git a/tests/PHPStan/Levels/data/listType-3.json b/tests/PHPStan/Levels/data/listType-3.json new file mode 100644 index 0000000000..c25a0ea723 --- /dev/null +++ b/tests/PHPStan/Levels/data/listType-3.json @@ -0,0 +1,7 @@ +[ + { + "message": "Property Levels\\ListType\\Foo::$list (list) does not accept array.", + "line": 24, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/listType-7.json b/tests/PHPStan/Levels/data/listType-7.json new file mode 100644 index 0000000000..620ac9317f --- /dev/null +++ b/tests/PHPStan/Levels/data/listType-7.json @@ -0,0 +1,7 @@ +[ + { + "message": "Property Levels\\ListType\\Foo::$list (list) does not accept array.", + "line": 25, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/listType-8.json b/tests/PHPStan/Levels/data/listType-8.json new file mode 100644 index 0000000000..388a730546 --- /dev/null +++ b/tests/PHPStan/Levels/data/listType-8.json @@ -0,0 +1,7 @@ +[ + { + "message": "Property Levels\\ListType\\Foo::$list (list) does not accept list.", + "line": 26, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/listType.php b/tests/PHPStan/Levels/data/listType.php new file mode 100644 index 0000000000..2123effeef --- /dev/null +++ b/tests/PHPStan/Levels/data/listType.php @@ -0,0 +1,30 @@ + */ + public $list; + + /** + * @param array $stringKeyArray + * @param array $intKeyArray + * @param list $stringOrNullList + * @param list $stringList + */ + public function doFoo( + array $stringKeyArray, + array $intKeyArray, + array $stringOrNullList, + array $stringList + ): void + { + $this->list = $stringKeyArray; + $this->list = $intKeyArray; + $this->list = $stringOrNullList; + $this->list = $stringList; + } + +} diff --git a/tests/PHPStan/Levels/data/propertyAccesses-6.json b/tests/PHPStan/Levels/data/propertyAccesses-6.json index edeb6d1b42..b62abeefd8 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-6.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-6.json @@ -19,11 +19,6 @@ "line": 74, "ignorable": true }, - { - "message": "Method Levels\\PropertyAccesses\\ClassWithMagicMethod::__set() has no return type specified.", - "line": 83, - "ignorable": true - }, { "message": "Method Levels\\PropertyAccesses\\AnotherClassWithMagicMethod::doFoo() has no return type specified.", "line": 93, @@ -39,4 +34,4 @@ "line": 158, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/stringOffsetAccess-2.json b/tests/PHPStan/Levels/data/stringOffsetAccess-2.json new file mode 100644 index 0000000000..9188b4d36d --- /dev/null +++ b/tests/PHPStan/Levels/data/stringOffsetAccess-2.json @@ -0,0 +1,7 @@ +[ + { + "message": "PHPDoc tag @var with type int|object is not subtype of native type null.", + "line": 7, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/stringOffsetAccess-3.json b/tests/PHPStan/Levels/data/stringOffsetAccess-3.json index 9eb7139340..be9fa763e2 100644 --- a/tests/PHPStan/Levels/data/stringOffsetAccess-3.json +++ b/tests/PHPStan/Levels/data/stringOffsetAccess-3.json @@ -8,5 +8,10 @@ "message": "Offset 12.34 does not exist on 'foo'.", "line": 16, "ignorable": true + }, + { + "message": "Invalid array key type stdClass.", + "line": 59, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/stringOffsetAccess-7.json b/tests/PHPStan/Levels/data/stringOffsetAccess-7.json index 389aa0b2db..5471fbcf70 100644 --- a/tests/PHPStan/Levels/data/stringOffsetAccess-7.json +++ b/tests/PHPStan/Levels/data/stringOffsetAccess-7.json @@ -1,11 +1,11 @@ [ { - "message": "Offset 'foo' does not exist on array|string.", + "message": "Offset 'foo' might not exist on array|string.", "line": 27, "ignorable": true }, { - "message": "Offset 12.34 does not exist on array|string.", + "message": "Offset 12.34 might not exist on array|string.", "line": 31, "ignorable": true }, @@ -13,5 +13,10 @@ "message": "Possibly invalid array key type int|object.", "line": 35, "ignorable": true + }, + { + "message": "Possibly invalid array key type int|object.", + "line": 55, + "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/stringOffsetAccess.php b/tests/PHPStan/Levels/data/stringOffsetAccess.php index afed1e9694..452e38960a 100644 --- a/tests/PHPStan/Levels/data/stringOffsetAccess.php +++ b/tests/PHPStan/Levels/data/stringOffsetAccess.php @@ -49,4 +49,12 @@ function () { /** @var mixed $mixed */ $mixed = null; echo $mixed[$maybeInt]; + + /** @var array{foo: 17, bar: 19}|array{baz: 21} $arrayUnion */ + $arrayUnion = []; + echo $arrayUnion[$maybeInt]; + + /** @var array{foo: 17, bar: 19}|array{baz: 21} $arrayUnion */ + $arrayUnion = []; + echo $arrayUnion[new \stdClass()]; }; diff --git a/tests/PHPStan/Levels/data/unreachable-4-alwaysTrue.json b/tests/PHPStan/Levels/data/unreachable-4-alwaysTrue.json deleted file mode 100644 index 5f0119364d..0000000000 --- a/tests/PHPStan/Levels/data/unreachable-4-alwaysTrue.json +++ /dev/null @@ -1,102 +0,0 @@ -[ - { - "message": "Strict comparison using === between 5 and 5 will always evaluate to true.", - "line": 11, - "ignorable": true - }, - { - "message": "Else branch is unreachable because previous condition is always true.", - "line": 13, - "ignorable": true - }, - { - "message": "Instanceof between $this(Levels\\Unreachable\\Foo) and Levels\\Unreachable\\Foo will always evaluate to true.", - "line": 20, - "ignorable": true - }, - { - "message": "Else branch is unreachable because previous condition is always true.", - "line": 22, - "ignorable": true - }, - { - "message": "Call to function is_string() with string will always evaluate to true.", - "line": 29, - "ignorable": true - }, - { - "message": "Else branch is unreachable because previous condition is always true.", - "line": 31, - "ignorable": true - }, - { - "message": "If condition is always true.", - "line": 38, - "ignorable": true - }, - { - "message": "If condition is always true.", - "line": 47, - "ignorable": true - }, - { - "message": "Left side of && is always true.", - "line": 59, - "ignorable": true - }, - { - "message": "Right side of && is always true.", - "line": 59, - "ignorable": true - }, - { - "message": "Else branch is unreachable because ternary operator condition is always true.", - "line": 74, - "ignorable": true - }, - { - "message": "Strict comparison using === between 5 and 5 will always evaluate to true.", - "line": 74, - "ignorable": true - }, - { - "message": "Else branch is unreachable because ternary operator condition is always true.", - "line": 79, - "ignorable": true - }, - { - "message": "Instanceof between $this(Levels\\Unreachable\\Bar) and Levels\\Unreachable\\Bar will always evaluate to true.", - "line": 79, - "ignorable": true - }, - { - "message": "Call to function is_string() with string will always evaluate to true.", - "line": 84, - "ignorable": true - }, - { - "message": "Else branch is unreachable because ternary operator condition is always true.", - "line": 84, - "ignorable": true - }, - { - "message": "Ternary operator condition is always true.", - "line": 89, - "ignorable": true - }, - { - "message": "Ternary operator condition is always true.", - "line": 94, - "ignorable": true - }, - { - "message": "Left side of && is always true.", - "line": 102, - "ignorable": true - }, - { - "message": "Right side of && is always true.", - "line": 102, - "ignorable": true - } -] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/unreachable-4.json b/tests/PHPStan/Levels/data/unreachable-4.json index f87bfc8cea..6f937312ae 100644 --- a/tests/PHPStan/Levels/data/unreachable-4.json +++ b/tests/PHPStan/Levels/data/unreachable-4.json @@ -1,17 +1,17 @@ [ { - "message": "Else branch is unreachable because previous condition is always true.", - "line": 13, + "message": "Strict comparison using === between 5 and 5 will always evaluate to true.", + "line": 11, "ignorable": true }, { - "message": "Else branch is unreachable because previous condition is always true.", - "line": 22, + "message": "Instanceof between $this(Levels\\Unreachable\\Foo) and Levels\\Unreachable\\Foo will always evaluate to true.", + "line": 20, "ignorable": true }, { - "message": "Else branch is unreachable because previous condition is always true.", - "line": 31, + "message": "Call to function is_string() with string will always evaluate to true.", + "line": 29, "ignorable": true }, { @@ -35,17 +35,32 @@ "ignorable": true }, { - "message": "Else branch is unreachable because ternary operator condition is always true.", + "message": "Strict comparison using === between 5 and 5 will always evaluate to true.", "line": 74, "ignorable": true }, { - "message": "Else branch is unreachable because ternary operator condition is always true.", + "message": "Unused result of ternary operator.", + "line": 74, + "ignorable": true + }, + { + "message": "Instanceof between $this(Levels\\Unreachable\\Bar) and Levels\\Unreachable\\Bar will always evaluate to true.", + "line": 79, + "ignorable": true + }, + { + "message": "Unused result of ternary operator.", "line": 79, "ignorable": true }, { - "message": "Else branch is unreachable because ternary operator condition is always true.", + "message": "Call to function is_string() with string will always evaluate to true.", + "line": 84, + "ignorable": true + }, + { + "message": "Unused result of ternary operator.", "line": 84, "ignorable": true }, @@ -59,6 +74,11 @@ "line": 94, "ignorable": true }, + { + "message": "Unused result of ternary operator.", + "line": 94, + "ignorable": true + }, { "message": "Left side of && is always true.", "line": 102, @@ -68,5 +88,10 @@ "message": "Right side of && is always true.", "line": 102, "ignorable": true + }, + { + "message": "Unused result of ternary operator.", + "line": 102, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/unreachable-6-alwaysTrue.json b/tests/PHPStan/Levels/data/unreachable-6-alwaysTrue.json deleted file mode 100644 index b285fc696d..0000000000 --- a/tests/PHPStan/Levels/data/unreachable-6-alwaysTrue.json +++ /dev/null @@ -1,62 +0,0 @@ -[ - { - "message": "Method Levels\\Unreachable\\Foo::doStrictComparison() has no return type specified.", - "line": 8, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doInstanceOf() has no return type specified.", - "line": 18, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doTypeSpecifyingFunction() has no return type specified.", - "line": 27, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doOtherFunction() has no return type specified.", - "line": 36, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doOtherValue() has no return type specified.", - "line": 45, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Foo::doBooleanAnd() has no return type specified.", - "line": 54, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doStrictComparison() has no return type specified.", - "line": 71, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doInstanceOf() has no return type specified.", - "line": 77, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doTypeSpecifyingFunction() has no return type specified.", - "line": 82, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doOtherFunction() has no return type specified.", - "line": 87, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doOtherValue() has no return type specified.", - "line": 92, - "ignorable": true - }, - { - "message": "Method Levels\\Unreachable\\Bar::doBooleanAnd() has no return type specified.", - "line": 97, - "ignorable": true - } -] \ No newline at end of file diff --git a/tests/PHPStan/Levels/dynamicConstantNames.neon b/tests/PHPStan/Levels/dynamicConstantNames.neon index 89f4491bc4..f52cabb14e 100644 --- a/tests/PHPStan/Levels/dynamicConstantNames.neon +++ b/tests/PHPStan/Levels/dynamicConstantNames.neon @@ -4,3 +4,4 @@ includes: parameters: dynamicConstantNames: - Levels\Comparison\Foo::FOO_CONST + phpVersion: 80300 diff --git a/tests/PHPStan/Node/AttributeArgRule.php b/tests/PHPStan/Node/AttributeArgRule.php index 90865da170..084671311f 100644 --- a/tests/PHPStan/Node/AttributeArgRule.php +++ b/tests/PHPStan/Node/AttributeArgRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; /** * @implements Rule @@ -19,13 +20,13 @@ public function getNodeType(): string return Node\Arg::class; } - /** - * @param Node\Arg $node - * @return string[] - */ public function processNode(Node $node, Scope $scope): array { - return [self::ERROR_MESSAGE]; + return [ + RuleErrorBuilder::message(self::ERROR_MESSAGE) + ->identifier('tests.attributeArg') + ->build(), + ]; } } diff --git a/tests/PHPStan/Node/AttributeArgRuleTest.php b/tests/PHPStan/Node/AttributeArgRuleTest.php index 29773859d8..fb4c1d99e9 100644 --- a/tests/PHPStan/Node/AttributeArgRuleTest.php +++ b/tests/PHPStan/Node/AttributeArgRuleTest.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -42,4 +43,18 @@ public function testRule(string $file, string $expectedError, array $lines): voi $this->analyse([$file], $errors); } + public function testEnumCaseAttribute(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/enum-case-attribute.php'], [ + [ + AttributeArgRule::ERROR_MESSAGE, + 10, + ], + ]); + } + } diff --git a/tests/PHPStan/Node/AttributeGroupRule.php b/tests/PHPStan/Node/AttributeGroupRule.php index 127c4f1f62..5081ff14ee 100644 --- a/tests/PHPStan/Node/AttributeGroupRule.php +++ b/tests/PHPStan/Node/AttributeGroupRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; /** * @implements Rule @@ -19,13 +20,13 @@ public function getNodeType(): string return Node\AttributeGroup::class; } - /** - * @param Node\AttributeGroup $node - * @return string[] - */ public function processNode(Node $node, Scope $scope): array { - return [self::ERROR_MESSAGE]; + return [ + RuleErrorBuilder::message(self::ERROR_MESSAGE) + ->identifier('tests.attributeGroup') + ->build(), + ]; } } diff --git a/tests/PHPStan/Node/AttributeRule.php b/tests/PHPStan/Node/AttributeRule.php index 9b7519bce8..cf9afea21f 100644 --- a/tests/PHPStan/Node/AttributeRule.php +++ b/tests/PHPStan/Node/AttributeRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; /** * @implements Rule @@ -19,13 +20,13 @@ public function getNodeType(): string return Node\Attribute::class; } - /** - * @param Node\Attribute $node - * @return string[] - */ public function processNode(Node $node, Scope $scope): array { - return [self::ERROR_MESSAGE]; + return [ + RuleErrorBuilder::message(self::ERROR_MESSAGE) + ->identifier('tests.attribute') + ->build(), + ]; } } diff --git a/tests/PHPStan/Node/FileNodeTest.php b/tests/PHPStan/Node/FileNodeTest.php index 852ec3b136..60a132a7f8 100644 --- a/tests/PHPStan/Node/FileNodeTest.php +++ b/tests/PHPStan/Node/FileNodeTest.php @@ -5,8 +5,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\File\SimpleRelativePathHelper; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Testing\RuleTestCase; use function get_class; @@ -27,7 +27,7 @@ public function getNodeType(): string /** * @param FileNode $node - * @return RuleError[] + * @return list */ public function processNode(Node $node, Scope $scope): array { @@ -35,14 +35,16 @@ public function processNode(Node $node, Scope $scope): array $pathHelper = new SimpleRelativePathHelper(__DIR__ . DIRECTORY_SEPARATOR . 'data'); if (!isset($nodes[0])) { return [ - RuleErrorBuilder::message(sprintf('File %s is empty.', $pathHelper->getRelativePath($scope->getFile())))->line(1)->build(), + RuleErrorBuilder::message(sprintf('File %s is empty.', $pathHelper->getRelativePath($scope->getFile())))->line(1) + ->identifier('tests.fileNode') + ->build(), ]; } return [ RuleErrorBuilder::message( sprintf('First node in file %s is: %s', $pathHelper->getRelativePath($scope->getFile()), get_class($nodes[0])), - )->build(), + )->identifier('tests.fileNode')->build(), ]; } diff --git a/tests/PHPStan/Node/ParentStmtTypesRule.php b/tests/PHPStan/Node/ParentStmtTypesRule.php index 1483f6b99d..d011615773 100644 --- a/tests/PHPStan/Node/ParentStmtTypesRule.php +++ b/tests/PHPStan/Node/ParentStmtTypesRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; use function array_reverse; use function implode; use function sprintf; @@ -23,10 +24,10 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { return [ - sprintf( + RuleErrorBuilder::message(sprintf( 'Parents: %s', implode(', ', array_reverse($node->getAttribute('parentStmtTypes'))), - ), + ))->identifier('tests.parentStmtTypes')->build(), ]; } diff --git a/tests/PHPStan/Node/TryCatchTypeRule.php b/tests/PHPStan/Node/TryCatchTypeRule.php index b356792a60..e71958eedf 100644 --- a/tests/PHPStan/Node/TryCatchTypeRule.php +++ b/tests/PHPStan/Node/TryCatchTypeRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ObjectType; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; @@ -30,10 +31,10 @@ public function processNode(Node $node, Scope $scope): array $type = TypeCombinator::union(...array_map(static fn (string $name) => new ObjectType($name), $tryCatchTypes)); } return [ - sprintf( + RuleErrorBuilder::message(sprintf( 'Try catch type: %s', $type !== null ? $type->describe(VerbosityLevel::precise()) : 'nothing', - ), + ))->identifier('tests.tryCatchType')->build(), ]; } diff --git a/tests/PHPStan/Node/data/enum-case-attribute.php b/tests/PHPStan/Node/data/enum-case-attribute.php new file mode 100644 index 0000000000..93e87ef1f6 --- /dev/null +++ b/tests/PHPStan/Node/data/enum-case-attribute.php @@ -0,0 +1,13 @@ += 8.1 + +namespace EnumCaseAttributeCheck; + +use NodeCallbackCalled\UniversalAttribute; + +enum Foo +{ + + #[UniversalAttribute(1)] + case TEST; + +} diff --git a/tests/PHPStan/Parallel/ParallelAnalyserIntegrationTest.php b/tests/PHPStan/Parallel/ParallelAnalyserIntegrationTest.php index 5d5b136063..3ae92e09dc 100644 --- a/tests/PHPStan/Parallel/ParallelAnalyserIntegrationTest.php +++ b/tests/PHPStan/Parallel/ParallelAnalyserIntegrationTest.php @@ -65,6 +65,7 @@ public function testRun(string $command): void 'message' => 'Method ParallelAnalyserIntegrationTest\\Bar::doFoo() has no return type specified.', 'line' => 8, 'ignorable' => true, + 'identifier' => 'missingType.return', ], ], ], @@ -75,16 +76,21 @@ public function testRun(string $command): void 'message' => 'Method ParallelAnalyserIntegrationTest\\Foo::doFoo() has no return type specified.', 'line' => 8, 'ignorable' => true, + 'identifier' => 'missingType.return', ], [ 'message' => 'Access to an undefined property ParallelAnalyserIntegrationTest\\Foo::$test.', 'line' => 10, 'ignorable' => true, + 'identifier' => 'property.notFound', + 'tip' => 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property', ], [ 'message' => 'Access to an undefined property ParallelAnalyserIntegrationTest\\Foo::$test.', 'line' => 15, 'ignorable' => true, + 'identifier' => 'property.notFound', + 'tip' => 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property', ], ], ], diff --git a/tests/PHPStan/Parser/CachedParserTest.php b/tests/PHPStan/Parser/CachedParserTest.php index 8fbfc1a87c..76bb9e215d 100644 --- a/tests/PHPStan/Parser/CachedParserTest.php +++ b/tests/PHPStan/Parser/CachedParserTest.php @@ -2,8 +2,6 @@ namespace PHPStan\Parser; -use PhpParser\Node; - use PhpParser\Node\Stmt\Namespace_; use PHPStan\File\FileHelper; use PHPStan\File\FileReader; @@ -85,7 +83,6 @@ public function testParseTheSameFileWithDifferentMethod(): void self::getContainer()->getService('currentPhpVersionRichParser'), self::getContainer()->getService('currentPhpVersionSimpleDirectParser'), self::getContainer()->getService('php8Parser'), - null ); $parser = new CachedParser($pathRoutingParser, 500); $path = $fileHelper->normalizePath(__DIR__ . '/data/test.php'); diff --git a/tests/PHPStan/Parser/RichParserTest.php b/tests/PHPStan/Parser/RichParserTest.php new file mode 100644 index 0000000000..08a15b480b --- /dev/null +++ b/tests/PHPStan/Parser/RichParserTest.php @@ -0,0 +1,301 @@ + null, + ], + ]; + + yield [ + ' null, + ], + ]; + + yield [ + ' null, + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['return.ref'], + ], + ]; + + yield [ + ' ['return.ref', 'return.non'], + ], + ]; + + yield [ + ' ['return.ref', 'return.non'], + ], + ]; + + yield [ + ' ['return.ref', 'return.non'], + ], + ]; + + yield [ + ' ['return.ref', 'return.non'], + ], + ]; + + yield [ + ' ['return.ref', 'return.non'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test', 'test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['test'], + ], + ]; + + yield [ + ' ['return.ref', 'return.non'], + ], + ]; + } + + /** + * @dataProvider dataLinesToIgnore + * @param array|null> $expectedLines + */ + public function testLinesToIgnore(string $code, array $expectedLines): void + { + /** @var RichParser $parser */ + $parser = self::getContainer()->getService('currentPhpVersionRichParser'); + $ast = $parser->parseString($code); + $lines = $ast[0]->getAttribute('linesToIgnore'); + $this->assertSame($expectedLines, $lines); + $this->assertNull($ast[0]->getAttribute('linesToIgnoreParseErrors')); + } + + public function dataLinesToIgnoreParseErrors(): iterable + { + yield [ + ' ['Unexpected comma (,)'], + ], + ]; + + yield [ + ' ['Closing parenthesis ")" before opening parenthesis "("'], + ], + ]; + + yield [ + ' ['Unclosed opening parenthesis "(" without closing parenthesis ")"'], + ], + ]; + } + + /** + * @dataProvider dataLinesToIgnoreParseErrors + * @param array> $expectedErrors + */ + public function testLinesToIgnoreParseErrors(string $code, array $expectedErrors): void + { + /** @var RichParser $parser */ + $parser = self::getContainer()->getService('currentPhpVersionRichParser'); + $ast = $parser->parseString($code); + $errors = $ast[0]->getAttribute('linesToIgnoreParseErrors'); + $this->assertIsArray($errors); + $this->assertSame($expectedErrors, $errors); + + $lines = $ast[0]->getAttribute('linesToIgnore'); + $this->assertIsArray($lines); + $this->assertCount(0, $lines); + } + +} diff --git a/tests/PHPStan/Parser/data/cleaning-1-after.php b/tests/PHPStan/Parser/data/cleaning-1-after.php index e13a195e7d..1d22a6ac92 100644 --- a/tests/PHPStan/Parser/data/cleaning-1-after.php +++ b/tests/PHPStan/Parser/data/cleaning-1-after.php @@ -39,3 +39,13 @@ public function doFoo() \func_get_args(); } } +class ContainsClosure +{ + public function doFoo() + { + static function () { + yield; + }; + yield; + } +} diff --git a/tests/PHPStan/Parser/data/cleaning-1-before.php b/tests/PHPStan/Parser/data/cleaning-1-before.php index 468db05176..ae93a81b77 100644 --- a/tests/PHPStan/Parser/data/cleaning-1-before.php +++ b/tests/PHPStan/Parser/data/cleaning-1-before.php @@ -67,3 +67,19 @@ public function doFoo() } } } + +class ContainsClosure +{ + + public function doFoo() + { + return static function () { + if (doFoo()) { + echo 'foo'; + } + + yield; + }; + } + +} diff --git a/tests/PHPStan/Php/PhpVersionFactoryTest.php b/tests/PHPStan/Php/PhpVersionFactoryTest.php index ff16f0c27d..f84fe900ce 100644 --- a/tests/PHPStan/Php/PhpVersionFactoryTest.php +++ b/tests/PHPStan/Php/PhpVersionFactoryTest.php @@ -68,8 +68,14 @@ public function dataCreate(): array [ null, '8.3', - 80299, - '8.2.99', + 80300, + '8.3', + ], + [ + null, + '8.4', + 80399, + '8.3.99', ], [ null, diff --git a/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php b/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php new file mode 100644 index 0000000000..e7ea48c599 --- /dev/null +++ b/tests/PHPStan/PhpDoc/DefaultStubFilesProviderTest.php @@ -0,0 +1,53 @@ +currentWorkingDirectory = $this->getContainer()->getParameter('currentWorkingDirectory'); + } + + public function testGetStubFiles(): void + { + $thirdPartyStubFile = sprintf('%s/vendor/thirdpartyStub.stub', $this->currentWorkingDirectory); + $defaultStubFilesProvider = $this->createDefaultStubFilesProvider(['/projectStub.stub', $thirdPartyStubFile]); + $stubFiles = $defaultStubFilesProvider->getStubFiles(); + $this->assertContains('/projectStub.stub', $stubFiles); + $this->assertContains($thirdPartyStubFile, $stubFiles); + } + + public function testGetProjectStubFiles(): void + { + $thirdPartyStubFile = sprintf('%s/vendor/thirdpartyStub.stub', $this->currentWorkingDirectory); + $defaultStubFilesProvider = $this->createDefaultStubFilesProvider(['/projectStub.stub', $thirdPartyStubFile]); + $projectStubFiles = $defaultStubFilesProvider->getProjectStubFiles(); + $this->assertContains('/projectStub.stub', $projectStubFiles); + $this->assertNotContains($thirdPartyStubFile, $projectStubFiles); + } + + public function testGetProjectStubFilesWhenPathContainsWindowsSeparator(): void + { + $thirdPartyStubFile = sprintf('%s\\vendor\\thirdpartyStub.stub', $this->currentWorkingDirectory); + $defaultStubFilesProvider = $this->createDefaultStubFilesProvider(['/projectStub.stub', $thirdPartyStubFile]); + $projectStubFiles = $defaultStubFilesProvider->getProjectStubFiles(); + $this->assertContains('/projectStub.stub', $projectStubFiles); + $this->assertNotContains($thirdPartyStubFile, $projectStubFiles); + } + + /** + * @param string[] $stubFiles + */ + private function createDefaultStubFilesProvider(array $stubFiles): DefaultStubFilesProvider + { + return new DefaultStubFilesProvider($this->getContainer(), $stubFiles, $this->currentWorkingDirectory); + } + +} diff --git a/tests/PHPStan/Process/Runnable/RunnableQueueLoggerStub.php b/tests/PHPStan/Process/Runnable/RunnableQueueLoggerStub.php deleted file mode 100644 index 39f6a91833..0000000000 --- a/tests/PHPStan/Process/Runnable/RunnableQueueLoggerStub.php +++ /dev/null @@ -1,24 +0,0 @@ -messages; - } - - public function log(string $message): void - { - $this->messages[] = $message; - } - -} diff --git a/tests/PHPStan/Process/Runnable/RunnableQueueTest.php b/tests/PHPStan/Process/Runnable/RunnableQueueTest.php deleted file mode 100644 index 43a94f661c..0000000000 --- a/tests/PHPStan/Process/Runnable/RunnableQueueTest.php +++ /dev/null @@ -1,165 +0,0 @@ -queue($one, 1); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(1, $queue->getRunningSize()); - $one->finish(); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(0, $queue->getRunningSize()); - $this->assertSame([ - 'Queue not full - looking at first item in the queue', - 'Removing top item from queue - new size is 1', - 'Running process 1', - 'Process 1 finished successfully', - 'Queue empty', - ], $logger->getMessages()); - } - - public function testComplexScenario(): void - { - $logger = new RunnableQueueLoggerStub(); - $queue = new RunnableQueue($logger, 8); - - $one = new RunnableStub('1'); - $two = new RunnableStub('2'); - $three = new RunnableStub('3'); - $four = new RunnableStub('4'); - $queue->queue($one, 4); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(4, $queue->getRunningSize()); - - $queue->queue($two, 2); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(6, $queue->getRunningSize()); - $queue->queue($three, 3); - $this->assertSame(3, $queue->getQueueSize()); - $this->assertSame(6, $queue->getRunningSize()); - $queue->queue($four, 4); - $this->assertSame(7, $queue->getQueueSize()); - $this->assertSame(6, $queue->getRunningSize()); - - $one->finish(); - $this->assertSame(4, $queue->getQueueSize()); - $this->assertSame(5, $queue->getRunningSize()); - - $two->finish(); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(7, $queue->getRunningSize()); - - $three->finish(); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(4, $queue->getRunningSize()); - - $four->finish(); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(0, $queue->getRunningSize()); - - $this->assertSame([ - 0 => 'Queue not full - looking at first item in the queue', - 1 => 'Removing top item from queue - new size is 4', - 2 => 'Running process 1', - 3 => 'Queue not full - looking at first item in the queue', - 4 => 'Removing top item from queue - new size is 6', - 5 => 'Running process 2', - 6 => 'Queue not full - looking at first item in the queue', - 7 => 'Canot remote first item from the queue - it has size 3, current queue size is 6, new size would be 9', - 8 => 'Queue not full - looking at first item in the queue', - 9 => 'Canot remote first item from the queue - it has size 3, current queue size is 6, new size would be 9', - 10 => 'Process 1 finished successfully', - 11 => 'Queue not full - looking at first item in the queue', - 12 => 'Removing top item from queue - new size is 5', - 13 => 'Running process 3', - 14 => 'Process 2 finished successfully', - 15 => 'Queue not full - looking at first item in the queue', - 16 => 'Removing top item from queue - new size is 7', - 17 => 'Running process 4', - 18 => 'Process 3 finished successfully', - 19 => 'Queue empty', - 20 => 'Process 4 finished successfully', - 21 => 'Queue empty', - ], $logger->getMessages()); - } - - public function testCancel(): void - { - $logger = new RunnableQueueLoggerStub(); - $queue = new RunnableQueue($logger, 8); - $one = new RunnableStub('1'); - $promise = $queue->queue($one, 4); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(4, $queue->getRunningSize()); - - $promise->then(static function () use ($logger): void { - $logger->log('Should not happen'); - }, static function (Exception $e) use ($logger): void { - $logger->log(sprintf('Else callback in test called: %s', $e->getMessage())); - }); - $promise->cancel(); - - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(0, $queue->getRunningSize()); - - $this->assertSame([ - 0 => 'Queue not full - looking at first item in the queue', - 1 => 'Removing top item from queue - new size is 4', - 2 => 'Running process 1', - 3 => 'Process 1 finished unsuccessfully: Runnable 1 canceled', - 4 => 'Else callback in test called: Runnable 1 canceled', - 5 => 'Queue empty', - ], $logger->getMessages()); - } - - public function testCancelAll(): void - { - $logger = new RunnableQueueLoggerStub(); - $queue = new RunnableQueue($logger, 6); - $one = new RunnableStub('1'); - $two = new RunnableStub('2'); - $three = new RunnableStub('3'); - $queue->queue($one, 3); - $queue->queue($two, 2); - $queue->queue($three, 3); - - $this->assertSame(3, $queue->getQueueSize()); - $this->assertSame(5, $queue->getRunningSize()); - - $queue->cancelAll(); - $this->assertSame(0, $queue->getQueueSize()); - $this->assertSame(0, $queue->getRunningSize()); - - $this->assertSame([ - 0 => 'Queue not full - looking at first item in the queue', - 1 => 'Removing top item from queue - new size is 3', - 2 => 'Running process 1', - 3 => 'Queue not full - looking at first item in the queue', - 4 => 'Removing top item from queue - new size is 5', - 5 => 'Running process 2', - 6 => 'Queue not full - looking at first item in the queue', - 7 => 'Canot remote first item from the queue - it has size 3, current queue size is 5, new size would be 8', - 8 => 'Process 1 finished unsuccessfully: Runnable 1 canceled', - 9 => 'Queue not full - looking at first item in the queue', - 10 => 'Removing top item from queue - new size is 5', - 11 => 'Running process 3', - 12 => 'Process 3 finished unsuccessfully: Runnable 3 canceled', - 13 => 'Queue empty', - 14 => 'Process 2 finished unsuccessfully: Runnable 2 canceled', - 15 => 'Queue empty', - ], $logger->getMessages()); - } - -} diff --git a/tests/PHPStan/Process/Runnable/RunnableStub.php b/tests/PHPStan/Process/Runnable/RunnableStub.php deleted file mode 100644 index 08a3701c90..0000000000 --- a/tests/PHPStan/Process/Runnable/RunnableStub.php +++ /dev/null @@ -1,40 +0,0 @@ -deferred = new Deferred(); - } - - public function getName(): string - { - return $this->name; - } - - public function finish(): void - { - $this->deferred->resolve(); - } - - public function run(): CancellablePromiseInterface - { - /** @var CancellablePromiseInterface */ - return $this->deferred->promise(); - } - - public function cancel(): void - { - $this->deferred->reject(new RunnableCanceledException(sprintf('Runnable %s canceled', $this->getName()))); - } - -} diff --git a/tests/PHPStan/Reflection/AllowedSubTypesClassReflectionExtensionTest.php b/tests/PHPStan/Reflection/AllowedSubTypesClassReflectionExtensionTest.php new file mode 100644 index 0000000000..58890ef52e --- /dev/null +++ b/tests/PHPStan/Reflection/AllowedSubTypesClassReflectionExtensionTest.php @@ -0,0 +1,36 @@ +gatherAssertTypes(__DIR__ . '/data/allowed-sub-types.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/data/allowed-sub-types.neon', + ]; + } + +} diff --git a/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php b/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php index 6027b270ac..dfdb480fb8 100644 --- a/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php @@ -459,7 +459,7 @@ public function dataMethods(): array ], 'conflictingMethod' => [ 'class' => Bar::class, - 'returnType' => Bar::class, + 'returnType' => Foo::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], diff --git a/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php b/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php index 14dabbece3..d58eeb7217 100644 --- a/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\Annotations; +use AnnotationsProperties\Asymmetric; use AnnotationsProperties\Bar; use AnnotationsProperties\Baz; use AnnotationsProperties\BazBaz; @@ -23,43 +24,50 @@ public function dataProperties(): array [ 'otherTest' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Test', + 'readableType' => 'OtherNamespace\Test', + 'writableType' => 'OtherNamespace\Test', 'writable' => true, 'readable' => true, ], 'otherTestReadOnly' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Ipsum', + 'readableType' => 'OtherNamespace\Ipsum', + 'writableType' => '*NEVER*', 'writable' => false, 'readable' => true, ], 'fooOrBar' => [ 'class' => Foo::class, - 'type' => 'AnnotationsProperties\Foo', + 'readableType' => 'AnnotationsProperties\Foo', + 'writableType' => 'AnnotationsProperties\Foo', 'writable' => true, 'readable' => true, ], 'conflictingProperty' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Ipsum', + 'readableType' => 'OtherNamespace\Ipsum', + 'writableType' => 'OtherNamespace\Ipsum', 'writable' => true, 'readable' => true, ], 'interfaceProperty' => [ 'class' => FooInterface::class, - 'type' => FooInterface::class, + 'readableType' => FooInterface::class, + 'writableType' => FooInterface::class, 'writable' => true, 'readable' => true, ], 'overridenProperty' => [ 'class' => Foo::class, - 'type' => Foo::class, + 'readableType' => Foo::class, + 'writableType' => Foo::class, 'writable' => true, 'readable' => true, ], 'overridenPropertyWithAnnotation' => [ 'class' => Foo::class, - 'type' => Foo::class, + 'readableType' => Foo::class, + 'writableType' => Foo::class, 'writable' => true, 'readable' => true, ], @@ -70,43 +78,50 @@ public function dataProperties(): array [ 'otherTest' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Test', + 'readableType' => 'OtherNamespace\Test', + 'writableType' => 'OtherNamespace\Test', 'writable' => true, 'readable' => true, ], 'otherTestReadOnly' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Ipsum', + 'readableType' => 'OtherNamespace\Ipsum', + 'writableType' => '*NEVER*', 'writable' => false, 'readable' => true, ], 'fooOrBar' => [ 'class' => Foo::class, - 'type' => 'AnnotationsProperties\Foo', + 'readableType' => 'AnnotationsProperties\Foo', + 'writableType' => 'AnnotationsProperties\Foo', 'writable' => true, 'readable' => true, ], 'conflictingProperty' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Ipsum', + 'readableType' => 'OtherNamespace\Ipsum', + 'writableType' => 'OtherNamespace\Ipsum', 'writable' => true, 'readable' => true, ], 'overridenProperty' => [ 'class' => Bar::class, - 'type' => Bar::class, + 'readableType' => Bar::class, + 'writableType' => Bar::class, 'writable' => true, 'readable' => true, ], 'overridenPropertyWithAnnotation' => [ 'class' => Bar::class, - 'type' => Bar::class, + 'readableType' => Bar::class, + 'writableType' => Bar::class, 'writable' => true, 'readable' => true, ], 'conflictingAnnotationProperty' => [ 'class' => Bar::class, - 'type' => Bar::class, + 'readableType' => Foo::class, + 'writableType' => Foo::class, 'writable' => true, 'readable' => true, ], @@ -117,43 +132,50 @@ public function dataProperties(): array [ 'otherTest' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Test', + 'readableType' => 'OtherNamespace\Test', + 'writableType' => 'OtherNamespace\Test', 'writable' => true, 'readable' => true, ], 'otherTestReadOnly' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Ipsum', + 'readableType' => 'OtherNamespace\Ipsum', + 'writableType' => '*NEVER*', 'writable' => false, 'readable' => true, ], 'fooOrBar' => [ 'class' => Foo::class, - 'type' => 'AnnotationsProperties\Foo', + 'readableType' => 'AnnotationsProperties\Foo', + 'writableType' => 'AnnotationsProperties\Foo', 'writable' => true, 'readable' => true, ], 'conflictingProperty' => [ 'class' => Baz::class, - 'type' => 'AnnotationsProperties\Dolor', + 'readableType' => 'AnnotationsProperties\Dolor', + 'writableType' => 'AnnotationsProperties\Dolor', 'writable' => true, 'readable' => true, ], 'bazProperty' => [ 'class' => Baz::class, - 'type' => 'AnnotationsProperties\Lorem', + 'readableType' => 'AnnotationsProperties\Lorem', + 'writableType' => 'AnnotationsProperties\Lorem', 'writable' => true, 'readable' => true, ], 'traitProperty' => [ 'class' => Baz::class, - 'type' => 'AnnotationsProperties\BazBaz', + 'readableType' => 'AnnotationsProperties\BazBaz', + 'writableType' => 'AnnotationsProperties\BazBaz', 'writable' => true, 'readable' => true, ], 'writeOnlyProperty' => [ 'class' => Baz::class, - 'type' => 'AnnotationsProperties\Lorem|null', + 'readableType' => '*NEVER*', + 'writableType' => 'AnnotationsProperties\Lorem|null', 'writable' => true, 'readable' => false, ], @@ -164,49 +186,83 @@ public function dataProperties(): array [ 'otherTest' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Test', + 'readableType' => 'OtherNamespace\Test', + 'writableType' => 'OtherNamespace\Test', 'writable' => true, 'readable' => true, ], 'otherTestReadOnly' => [ 'class' => Foo::class, - 'type' => 'OtherNamespace\Ipsum', + 'readableType' => 'OtherNamespace\Ipsum', + 'writableType' => '*NEVER*', 'writable' => false, 'readable' => true, ], 'fooOrBar' => [ 'class' => Foo::class, - 'type' => 'AnnotationsProperties\Foo', + 'readableType' => 'AnnotationsProperties\Foo', + 'writableType' => 'AnnotationsProperties\Foo', 'writable' => true, 'readable' => true, ], 'conflictingProperty' => [ 'class' => Baz::class, - 'type' => 'AnnotationsProperties\Dolor', + 'readableType' => 'AnnotationsProperties\Dolor', + 'writableType' => 'AnnotationsProperties\Dolor', 'writable' => true, 'readable' => true, ], 'bazProperty' => [ 'class' => Baz::class, - 'type' => 'AnnotationsProperties\Lorem', + 'readableType' => 'AnnotationsProperties\Lorem', + 'writableType' => 'AnnotationsProperties\Lorem', 'writable' => true, 'readable' => true, ], 'traitProperty' => [ 'class' => Baz::class, - 'type' => 'AnnotationsProperties\BazBaz', + 'readableType' => 'AnnotationsProperties\BazBaz', + 'writableType' => 'AnnotationsProperties\BazBaz', 'writable' => true, 'readable' => true, ], 'writeOnlyProperty' => [ 'class' => Baz::class, - 'type' => 'AnnotationsProperties\Lorem|null', + 'readableType' => '*NEVER*', + 'writableType' => 'AnnotationsProperties\Lorem|null', 'writable' => true, 'readable' => false, ], 'numericBazBazProperty' => [ 'class' => BazBaz::class, - 'type' => 'float|int', + 'readableType' => 'float|int', + 'writableType' => 'float|int', + 'writable' => true, + 'readable' => true, + ], + ], + ], + [ + Asymmetric::class, + [ + 'asymmetricPropertyRw' => [ + 'class' => Asymmetric::class, + 'readableType' => 'int', + 'writableType' => 'int|string', + 'writable' => true, + 'readable' => true, + ], + 'asymmetricPropertyXw' => [ + 'class' => Asymmetric::class, + 'readableType' => 'int', + 'writableType' => 'int|string', + 'writable' => true, + 'readable' => true, + ], + 'asymmetricPropertyRx' => [ + 'class' => Asymmetric::class, + 'readableType' => 'int', + 'writableType' => 'int|string', 'writable' => true, 'readable' => true, ], @@ -240,9 +296,14 @@ public function testProperties(string $className, array $properties): void sprintf('Declaring class of property $%s does not match.', $propertyName), ); $this->assertSame( - $expectedPropertyData['type'], + $expectedPropertyData['readableType'], $property->getReadableType()->describe(VerbosityLevel::precise()), - sprintf('Type of property %s::$%s does not match.', $property->getDeclaringClass()->getName(), $propertyName), + sprintf('Readable type of property %s::$%s does not match.', $property->getDeclaringClass()->getName(), $propertyName), + ); + $this->assertSame( + $expectedPropertyData['writableType'], + $property->getWritableType()->describe(VerbosityLevel::precise()), + sprintf('Writable type of property %s::$%s does not match.', $property->getDeclaringClass()->getName(), $propertyName), ); $this->assertSame( $expectedPropertyData['readable'], diff --git a/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php b/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php index 2d108fb0c9..75fbfa4527 100644 --- a/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php @@ -2,11 +2,14 @@ namespace PHPStan\Reflection\Annotations; +use DeprecatedAnnotations\Baz; +use DeprecatedAnnotations\BazInterface; use DeprecatedAnnotations\DeprecatedBar; use DeprecatedAnnotations\DeprecatedFoo; use DeprecatedAnnotations\DeprecatedWithMultipleTags; use DeprecatedAnnotations\Foo; use DeprecatedAnnotations\FooInterface; +use DeprecatedAnnotations\SubBazInterface; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Testing\PHPStanTestCase; @@ -141,4 +144,13 @@ public function testDeprecatedMethodsFromInterface(): void $this->assertTrue($class->getNativeMethod('superDeprecated')->isDeprecated()->yes()); } + public function testNotDeprecatedChildMethods(): void + { + $reflectionProvider = $this->createReflectionProvider(); + + $this->assertTrue($reflectionProvider->getClass(BazInterface::class)->getNativeMethod('superDeprecated')->isDeprecated()->yes()); + $this->assertTrue($reflectionProvider->getClass(SubBazInterface::class)->getNativeMethod('superDeprecated')->isDeprecated()->no()); + $this->assertTrue($reflectionProvider->getClass(Baz::class)->getNativeMethod('superDeprecated')->isDeprecated()->no()); + } + } diff --git a/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php b/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php index 1095002d92..553c2444ef 100644 --- a/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php +++ b/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php @@ -215,3 +215,26 @@ public function superDeprecated() } } + +interface BazInterface +{ + /** + * @deprecated Use the SubBazInterface instead. + */ + public function superDeprecated(); +} + +interface SubBazInterface extends BazInterface +{ + /** + * @not-deprecated + */ + public function superDeprecated(); +} + +class Baz implements SubBazInterface +{ + public function superDeprecated() + { + } +} diff --git a/tests/PHPStan/Reflection/Annotations/data/annotations-properties.php b/tests/PHPStan/Reflection/Annotations/data/annotations-properties.php index 240e142c62..9e4adf962f 100644 --- a/tests/PHPStan/Reflection/Annotations/data/annotations-properties.php +++ b/tests/PHPStan/Reflection/Annotations/data/annotations-properties.php @@ -57,6 +57,22 @@ class BazBaz extends Baz } +/** + * @property-read int $asymmetricPropertyRw + * @property-write int|string $asymmetricPropertyRw + * + * @property int $asymmetricPropertyXw + * @property-write int|string $asymmetricPropertyXw + * + * @property-read int $asymmetricPropertyRx + * @property int|string $asymmetricPropertyRx + */ +#[AllowDynamicProperties] +class Asymmetric +{ + +} + /** * @property FooInterface $interfaceProperty */ diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php index f56a41be51..6e4c91a783 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php @@ -14,7 +14,7 @@ function testFunctionForLocator(): void // phpcs:disable { - + echo 'test'; } class AutoloadSourceLocatorTest extends PHPStanTestCase diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php index bbd35cff3f..80b588643f 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php @@ -2,6 +2,8 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use PHPStan\BetterReflection\Identifier\IdentifierType; +use PHPStan\BetterReflection\Reflection\Reflection; use PHPStan\BetterReflection\Reflector\DefaultReflector; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; use PHPStan\Reflection\InitializerExprContext; @@ -10,6 +12,7 @@ use PHPStan\Type\VerbosityLevel; use SingleFileSourceLocatorTestClass; use TestSingleFileSourceLocator\AFoo; +use function array_map; use const PHP_VERSION_ID; class OptimizedSingleFileSourceLocatorTest extends PHPStanTestCase @@ -51,6 +54,79 @@ public function dataClass(): iterable ]; } + public function dataForIdenifiersByType(): iterable + { + yield from [ + 'classes wrapped in conditions' => [ + new IdentifierType(IdentifierType::IDENTIFIER_CLASS), + [ + 'TestSingleFileSourceLocator\AFoo', + 'TestSingleFileSourceLocator\InCondition', + 'TestSingleFileSourceLocator\InCondition', + 'TestSingleFileSourceLocator\InCondition', + ], + __DIR__ . '/data/a.php', + ], + 'class with function in same file' => [ + new IdentifierType(IdentifierType::IDENTIFIER_CLASS), + ['SingleFileSourceLocatorTestClass'], + __DIR__ . '/data/b.php', + ], + 'class bug-5525' => [ + new IdentifierType(IdentifierType::IDENTIFIER_CLASS), + ['Faker\Provider\nl_BE\Text'], + __DIR__ . '/data/bug-5525.php', + ], + 'file without classes' => [ + new IdentifierType(IdentifierType::IDENTIFIER_CLASS), + [], + __DIR__ . '/data/const.php', + ], + 'plain function in complex file' => [ + new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION), + [ + 'TestSingleFileSourceLocator\doFoo', + ], + __DIR__ . '/data/a.php', + ], + 'function with class in same file' => [ + new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION), + ['singleFileSourceLocatorTestFunction'], + __DIR__ . '/data/b.php', + ], + 'file without functions' => [ + new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION), + [], + __DIR__ . '/data/only-class.php', + ], + 'constants' => [ + new IdentifierType(IdentifierType::IDENTIFIER_CONSTANT), + [ + 'ANOTHER_NAME', + 'ConstFile\ANOTHER_NAME', + 'ConstFile\TABLE_NAME', + 'OPTIMIZED_SFSL_OBJECT_CONSTANT', + 'const_with_dir_const', + ], + __DIR__ . '/data/const.php', + ], + ]; + + if (PHP_VERSION_ID < 80100) { + return; + } + + yield 'enums as classes' => [ + new IdentifierType(IdentifierType::IDENTIFIER_CLASS), + [ + 'OptimizedDirectory\BackedByStringWithoutSpace', + 'OptimizedDirectory\TestEnum', + 'OptimizedDirectory\UppercaseEnum', + ], + __DIR__ . '/data/directory/enum.php', + ]; + } + /** * @dataProvider dataClass */ @@ -165,4 +241,28 @@ public function testConstUnknown(string $constantName): void $reflector->reflectConstant($constantName); } + /** + * @dataProvider dataForIdenifiersByType + * @param class-string[] $expectedIdentifiers + */ + public function testLocateIdentifiersByType( + IdentifierType $identifierType, + array $expectedIdentifiers, + string $file, + ): void + { + /** @var OptimizedSingleFileSourceLocatorFactory $factory */ + $factory = self::getContainer()->getByType(OptimizedSingleFileSourceLocatorFactory::class); + $locator = $factory->create($file); + $reflector = new DefaultReflector($locator); + + $reflections = $locator->locateIdentifiersByType( + $reflector, + $identifierType, + ); + + $actualIdentifiers = array_map(static fn (Reflection $reflection) => $reflection->getName(), $reflections); + $this->assertEqualsCanonicalizing($expectedIdentifiers, $actualIdentifiers); + } + } diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/only-class.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/only-class.php new file mode 100644 index 0000000000..f38b77dbe9 --- /dev/null +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/only-class.php @@ -0,0 +1,6 @@ += 80100 ? TrinaryLogic::createYes() : TrinaryLogic::createNo(), + null, + ]; + + yield [ + new Name('\CURLOPT_FTP_SSL'), + TrinaryLogic::createYes(), + 'use CURLOPT_USE_SSL instead.', + ]; + + yield [ + new Name('\DeprecatedConst\FINE'), + TrinaryLogic::createNo(), + null, + ]; + yield [ + new Name('\DeprecatedConst\MY_CONST'), + TrinaryLogic::createYes(), + null, + ]; + yield [ + new Name('\DeprecatedConst\MY_CONST2'), + TrinaryLogic::createYes(), + "don't use it!", + ]; + } + + /** + * @dataProvider dataDeprecatedConstants + */ + public function testDeprecatedConstants(Name $constName, TrinaryLogic $isDeprecated, ?string $deprecationMessage): void + { + require_once __DIR__ . '/data/deprecated-constant.php'; + + $reflectionProvider = $this->createReflectionProvider(); + + $this->assertTrue($reflectionProvider->hasConstant($constName, null)); + $this->assertSame($isDeprecated->describe(), $reflectionProvider->getConstant($constName, null)->isDeprecated()->describe()); + $this->assertSame($deprecationMessage, $reflectionProvider->getConstant($constName, null)->getDeprecatedDescription()); + } + +} diff --git a/tests/PHPStan/Reflection/Constant/data/deprecated-constant.php b/tests/PHPStan/Reflection/Constant/data/deprecated-constant.php new file mode 100644 index 0000000000..9dfc01dea9 --- /dev/null +++ b/tests/PHPStan/Reflection/Constant/data/deprecated-constant.php @@ -0,0 +1,15 @@ +createReflectionProvider(); + + $functionReflection = $reflectionProvider->getFunction(new Node\Name($functionName), null); + $this->assertSame($expectedDoc, $functionReflection->getDocComment()); + } + + public function dataPhpdocMethods(): iterable + { + yield [ + 'FunctionReflectionDocTest\\ClassWithPhpdoc', + '__construct', + '/** construct doc via stub */', + ]; + yield [ + 'FunctionReflectionDocTest\\ClassWithPhpdoc', + 'aMethod', + '/** some method phpdoc */', + ]; + yield [ + 'FunctionReflectionDocTest\\ClassWithPhpdoc', + 'noDocMethod', + null, + ]; + yield [ + 'FunctionReflectionDocTest\\ClassWithPhpdoc', + 'docViaStub', + '/** method doc via stub */', + ]; + yield [ + 'FunctionReflectionDocTest\\ClassWithPhpdoc', + 'existingDocButStubOverridden', + '/** stub overridden phpdoc */', + ]; + yield [ + 'FunctionReflectionDocTest\\ClassWithInheritedPhpdoc', + 'aMethod', + '/** some method phpdoc */', + ]; + yield [ + 'FunctionReflectionDocTest\\ClassWithInheritedPhpdoc', + 'noDocMethod', + null, + ]; + yield [ + 'FunctionReflectionDocTest\\ClassWithInheritedPhpdoc', + 'docViaStub', + '/** method doc via stub */', + ]; + yield [ + 'FunctionReflectionDocTest\\ClassWithInheritedPhpdoc', + 'existingDocButStubOverridden', + '/** stub overridden phpdoc */', + ]; + yield [ + 'FunctionReflectionDocTest\\ClassWithInheritedPhpdoc', + 'aMethodInheritanceOverridden', + '/** some inheritance overridden method phpdoc */', + ]; + yield [ + '\\DateTime', + '__construct', + '/** php-src native construct stub overridden phpdoc */', + ]; + yield [ + '\\DateTime', + 'modify', + '/** php-src native method stub overridden phpdoc */', + ]; + } + + /** + * @dataProvider dataPhpdocMethods + */ + public function testMethodHasPhpdoc(string $className, string $methodName, ?string $expectedDocComment): void + { + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); + $scope = $this->createMock(Scope::class); + $scope->method('isInClass')->willReturn(true); + $scope->method('getClassReflection')->willReturn($class); + $scope->method('canAccessProperty')->willReturn(true); + $classReflection = $reflectionProvider->getClass($className); + + $methodReflection = $classReflection->getMethod($methodName, $scope); + $this->assertSame($expectedDocComment, $methodReflection->getDocComment()); + } + + public function dataFunctionReturnsByReference(): iterable + { + yield ['\\implode', TrinaryLogic::createNo()]; + + yield ['ReturnsByReference\\foo', TrinaryLogic::createNo()]; + yield ['ReturnsByReference\\refFoo', TrinaryLogic::createYes()]; + } + + /** + * @dataProvider dataFunctionReturnsByReference + */ + public function testFunctionReturnsByReference(string $functionName, TrinaryLogic $expectedReturnsByRef): void + { + require_once __DIR__ . '/data/returns-by-reference.php'; + + $reflectionProvider = $this->createReflectionProvider(); + + $functionReflection = $reflectionProvider->getFunction(new Node\Name($functionName), null); + $this->assertSame($expectedReturnsByRef, $functionReflection->returnsByReference()); + } + + public function dataMethodReturnsByReference(): iterable + { + yield ['ReturnsByReference\\X', 'foo', TrinaryLogic::createNo()]; + yield ['ReturnsByReference\\X', 'refFoo', TrinaryLogic::createYes()]; + + yield ['ReturnsByReference\\SubX', 'foo', TrinaryLogic::createNo()]; + yield ['ReturnsByReference\\SubX', 'refFoo', TrinaryLogic::createYes()]; + yield ['ReturnsByReference\\SubX', 'subRefFoo', TrinaryLogic::createYes()]; + + yield ['ReturnsByReference\\TraitX', 'traitFoo', TrinaryLogic::createNo()]; + yield ['ReturnsByReference\\TraitX', 'refTraitFoo', TrinaryLogic::createYes()]; + + if (PHP_VERSION_ID < 80100) { + return; + } + + yield ['ReturnsByReference\\E', 'enumFoo', TrinaryLogic::createNo()]; + yield ['ReturnsByReference\\E', 'refEnumFoo', TrinaryLogic::createYes()]; + // cases() method cannot be overridden; https://3v4l.org/ebm83 + yield ['ReturnsByReference\\E', 'cases', TrinaryLogic::createNo()]; + } + + /** + * @dataProvider dataMethodReturnsByReference + */ + public function testMethodReturnsByReference(string $className, string $methodName, TrinaryLogic $expectedReturnsByRef): void + { + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); + $scope = $this->createMock(Scope::class); + $scope->method('isInClass')->willReturn(true); + $scope->method('getClassReflection')->willReturn($class); + $scope->method('canAccessProperty')->willReturn(true); + $classReflection = $reflectionProvider->getClass($className); + + $methodReflection = $classReflection->getMethod($methodName, $scope); + $this->assertSame($expectedReturnsByRef, $methodReflection->returnsByReference()); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/data/function-reflection.neon', + ]; + } + +} diff --git a/tests/PHPStan/Reflection/GenericParametersAcceptorResolverTest.php b/tests/PHPStan/Reflection/GenericParametersAcceptorResolverTest.php index f75e021960..4bcd1d6809 100644 --- a/tests/PHPStan/Reflection/GenericParametersAcceptorResolverTest.php +++ b/tests/PHPStan/Reflection/GenericParametersAcceptorResolverTest.php @@ -14,7 +14,6 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; -use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -22,7 +21,7 @@ use function get_class; use function sprintf; -class GenericParametersAcceptorResolverTest extends PHPStanTestCase +class GenericParametersAcceptorResolverTest extends PHPStanTestCase { /** @@ -316,7 +315,11 @@ public function dataResolve(): array ), new DummyParameter( 'b', - new IntegerType(), + new UnionType([ + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + ]), false, PassedByReference::createNo(), true, @@ -324,7 +327,11 @@ public function dataResolve(): array ), ], false, - new IntegerType(), + new UnionType([ + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + ]), ), ], 'missing args' => [ @@ -405,10 +412,10 @@ public function dataResolve(): array TemplateTypeMap::createEmpty(), null, [ - new DummyParameter('str', new StringType(), false, null, false, null), + new DummyParameter('str', new ConstantStringType('foooooo'), false, null, false, null), ], false, - new StringType(), + new ConstantStringType('foooooo'), ), ], ]; @@ -420,6 +427,7 @@ public function dataResolve(): array */ public function testResolve(array $argTypes, ParametersAcceptor $parametersAcceptor, ParametersAcceptor $expectedResult): void { + self::getContainer(); // to initialize bleeding edge $result = GenericParametersAcceptorResolver::resolve( $argTypes, $parametersAcceptor, @@ -455,4 +463,11 @@ public function testResolve(array $argTypes, ParametersAcceptor $parametersAccep } } + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + ]; + } + } diff --git a/tests/PHPStan/Reflection/InitializerExprTypeResolverTest.php b/tests/PHPStan/Reflection/InitializerExprTypeResolverTest.php new file mode 100644 index 0000000000..9288f62e8d --- /dev/null +++ b/tests/PHPStan/Reflection/InitializerExprTypeResolverTest.php @@ -0,0 +1,129 @@ + new ConstantIntegerType(1), + ConstantIntegerType::class, + ]; + } + + /** + * @dataProvider dataExplicitNever + * + * @param class-string $resultClass + * @param callable(Expr): Type $callback + */ + public function testExplicitNever(Expr $left, Expr $right, callable $callback, string $resultClass, ?bool $resultIsExplicit = null): void + { + $initializerExprTypeResolver = self::getContainer()->getByType(InitializerExprTypeResolver::class); + + $result = $initializerExprTypeResolver->getPlusType( + $left, + $right, + $callback, + ); + $this->assertInstanceOf($resultClass, $result); + + if (!($result instanceof NeverType)) { + return; + } + + if ($resultIsExplicit === null) { + throw new ShouldNotHappenException(); + } + $this->assertSame($resultIsExplicit, $result->isExplicit()); + } + +} diff --git a/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php b/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php index 645138a0e9..2b2b96c00f 100644 --- a/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php +++ b/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php @@ -9,6 +9,7 @@ use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\Php\DummyParameter; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -18,6 +19,7 @@ use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; @@ -198,7 +200,7 @@ public function dataSelectFromTypes(): Generator ), ], false, - new UnionType([new StringType(), new ConstantBooleanType(false)]), + new UnionType([new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), new ConstantBooleanType(false)]), ), ]; yield [ diff --git a/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php b/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php new file mode 100644 index 0000000000..e6fe0eed75 --- /dev/null +++ b/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php @@ -0,0 +1,661 @@ +> */ + public static function data(): iterable + { + $inputFile = self::getTestInputFile(); + $contents = file_get_contents($inputFile); + + if ($contents === false) { + self::fail('Input file \'' . $inputFile . '\' is missing.'); + } + + $parts = explode('-----', $contents); + + for ($i = 1; $i + 1 < count($parts); $i += 2) { + $input = trim($parts[$i]); + $output = trim($parts[$i + 1]); + + yield $input => [ + $input, + $output, + ]; + } + } + + /** @dataProvider data */ + public function test(string $input, string $expectedOutput): void + { + $output = self::generateSymbolDescription($input); + $output = trim($output); + $this->assertSame($expectedOutput, $output); + } + + private static function generateSymbolDescription(string $symbol): string + { + [$type, $name] = explode(' ', $symbol); + + try { + switch ($type) { + case 'FUNCTION': + return self::generateFunctionDescription($name); + case 'CLASS': + return self::generateClassDescription($name); + case 'METHOD': + return self::generateClassMethodDescription($name); + case 'PROPERTY': + return self::generateClassPropertyDescription($name); + default: + self::fail('Unknown symbol type ' . $type); + } + } catch (Throwable $e) { + // phpcs:ignore SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly + if ($e instanceof \PHPUnit\Exception) { + throw $e; + } + + // Skip stack trace - it's not fully consistent between dump and test. + return "Generating symbol description failed:\n" + . get_class($e) . ': ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() . "\n"; + } + } + + public static function dumpOutput(): void + { + $symbolsTxt = file_get_contents(self::getPhpSymbolsFile()); + + if ($symbolsTxt === false) { + throw new ShouldNotHappenException('Cannot read phpSymbols.txt'); + } + + $symbols = explode("\n", $symbolsTxt); + $separator = '-----'; + $contents = ''; + + foreach ($symbols as $line) { + $contents .= $separator . "\n"; + $contents .= $line . "\n"; + $contents .= $separator . "\n"; + $contents .= self::generateSymbolDescription($line); + } + + $result = file_put_contents(self::getTestInputFile(), $contents); + + if ($result !== false) { + return; + } + + throw new ShouldNotHappenException('Failed write dump for reflection golden test.'); + } + + private static function getTestInputFile(): string + { + $fileFromEnv = getenv('REFLECTION_GOLDEN_TEST_FILE'); + + if ($fileFromEnv !== false) { + return $fileFromEnv; + } + + $first = (int) floor(PHP_VERSION_ID / 10000); + $second = (int) (floor(PHP_VERSION_ID % 10000) / 100); + $currentVersion = $first . '.' . $second; + + return __DIR__ . '/data/golden/reflection-' . $currentVersion . '.test'; + } + + private static function getPhpSymbolsFile(): string + { + $fileFromEnv = getenv('REFLECTION_GOLDEN_SYMBOLS_FILE'); + + if ($fileFromEnv !== false) { + return $fileFromEnv; + } + + return __DIR__ . '/data/golden/phpSymbols.txt'; + } + + private static function generateFunctionDescription(string $functionName): string + { + $nameNode = new Name($functionName); + $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class); + + if (! $reflectionProvider->hasFunction($nameNode, null)) { + return "MISSING\n"; + } + + $functionReflection = $reflectionProvider->getFunction($nameNode, null); + $result = self::generateFunctionMethodBaseDescription($functionReflection); + + if (! $functionReflection->isBuiltin()) { + $result .= "NOT BUILTIN\n"; + } + + $result .= self::generateVariantsDescription($functionReflection->getName(), $functionReflection->getVariants(), false); + $namedArgumentsVariants = $functionReflection->getNamedArgumentsVariants(); + + if ($namedArgumentsVariants !== null) { + $result .= self::generateVariantsDescription($functionReflection->getName(), $namedArgumentsVariants, true); + } + + return $result; + } + + private static function generateClassDescription(string $className): string + { + $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class); + + if (! $reflectionProvider->hasClass($className)) { + return "MISSING\n"; + } + + $result = ''; + $classReflection = $reflectionProvider->getClass($className); + + if ($classReflection->isDeprecated()) { + $result .= "Deprecated\n"; + } + + if (! $classReflection->isBuiltin()) { + $result .= "Not builtin\n"; + } + + if ($classReflection->isInternal()) { + $result .= "Internal\n"; + } + + if ($classReflection->isImmutable()) { + $result .= "Immutable\n"; + } + + if ($classReflection->hasConsistentConstructor()) { + $result .= "Consistent constructor\n"; + } + + $parentReflection = $classReflection->getParentClass(); + $extends = ''; + + if ($parentReflection !== null) { + $extends = ' extends ' . $parentReflection->getName(); + } + + $attributes = []; + + if ($classReflection->allowsDynamicProperties()) { + $attributes[] = "#[AllowDynamicProperties]\n"; + } + + $attributesTxt = implode('', $attributes); + $abstractTxt = $classReflection->isAbstract() + ? 'abstract ' + : ''; + + switch (true) { + case $classReflection->isEnum(): + $keyword = 'enum'; + break; + case $classReflection->isInterface(): + $keyword = 'interface'; + break; + case $classReflection->isTrait(): + $keyword = 'trait'; + break; + case $classReflection->isClass(): + $keyword = 'class'; + break; + default: + $keyword = self::fail(); + } + + $verbosityLevel = VerbosityLevel::precise(); + $backedEnumType = $classReflection->getBackedEnumType(); + $backedEnumTypeTxt = $backedEnumType !== null + ? ': ' . $backedEnumType->describe($verbosityLevel) + : ''; + $readonlyTxt = $classReflection->isReadOnly() + ? 'readonly ' + : ''; + $interfaceNames = array_keys($classReflection->getImmediateInterfaces()); + $implementsTxt = $interfaceNames !== [] + ? ($classReflection->isInterface() ? ' extends ' : ' implements ') . implode(', ', $interfaceNames) + : ''; + $finalTxt = $classReflection->isFinal() + ? 'final ' + : ''; + $result .= $attributesTxt . $finalTxt . $readonlyTxt . $abstractTxt . $keyword . ' ' + . $classReflection->getName() . $extends . $implementsTxt . $backedEnumTypeTxt . "\n"; + $result .= "{\n"; + $ident = ' '; + + foreach (array_keys($classReflection->getTraits()) as $trait) { + $result .= $ident . 'use ' . $trait . ";\n"; + } + + $result .= "}\n"; + + return $result; + } + + private static function generateClassMethodDescription(string $classMethodName): string + { + [$className, $methodName] = explode('::', $classMethodName); + + $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class); + + if (! $reflectionProvider->hasClass($className)) { + return "MISSING\n"; + } + + $classReflection = $reflectionProvider->getClass($className); + + if (! $classReflection->hasNativeMethod($methodName)) { + return "MISSING\n"; + } + + $methodReflection = $classReflection->getNativeMethod($methodName); + $result = self::generateFunctionMethodBaseDescription($methodReflection); + $verbosityLevel = VerbosityLevel::precise(); + + if ($methodReflection->getSelfOutType() !== null) { + $result .= 'Self out type: ' . $methodReflection->getSelfOutType()->describe($verbosityLevel) . "\n"; + } + + if ($methodReflection->isStatic()) { + $result .= "Static\n"; + } + + switch (true) { + case $methodReflection->isPublic(): + $visibility = 'public'; + break; + case $methodReflection->isPrivate(): + $visibility = 'private'; + break; + default: + $visibility = 'protected'; + break; + } + + $result .= 'Visibility: ' . $visibility . "\n"; + $result .= self::generateVariantsDescription($methodReflection->getName(), $methodReflection->getVariants(), false); + $namedArgumentsVariants = $methodReflection->getNamedArgumentsVariants(); + + if ($namedArgumentsVariants !== null) { + $result .= self::generateVariantsDescription($methodReflection->getName(), $namedArgumentsVariants, true); + } + + return $result; + } + + /** @param FunctionReflection|ExtendedMethodReflection $reflection */ + private static function generateFunctionMethodBaseDescription($reflection): string + { + $result = ''; + + if (! $reflection->isDeprecated()->no()) { + $result .= 'Is deprecated: ' . $reflection->isDeprecated()->describe() . "\n"; + } + + if (! $reflection->isFinal()->no()) { + $result .= 'Is final: ' . $reflection->isFinal()->describe() . "\n"; + } + + if (! $reflection->isInternal()->no()) { + $result .= 'Is internal: ' . $reflection->isInternal()->describe() . "\n"; + } + + if (! $reflection->returnsByReference()->no()) { + $result .= 'Returns by reference: ' . $reflection->returnsByReference()->describe() . "\n"; + } + + if (! $reflection->hasSideEffects()->no()) { + $result .= 'Has side-effects: ' . $reflection->hasSideEffects()->describe() . "\n"; + } + + if ($reflection->getThrowType() !== null) { + $result .= 'Throw type: ' . $reflection->getThrowType()->describe(VerbosityLevel::precise()) . "\n"; + } + + return $result; + } + + /** @param ParametersAcceptorWithPhpDocs[] $variants */ + private static function generateVariantsDescription(string $name, array $variants, bool $isNamedArguments): string + { + $variantCount = count($variants); + $result = $isNamedArguments + ? 'Named arguments variants: ' + : 'Variants: '; + $result .= $variantCount . "\n"; + $variantIdent = ' '; + $verbosityLevel = VerbosityLevel::precise(); + + foreach ($variants as $variant) { + $paramsNative = []; + $paramsPhpDoc = []; + + foreach ($variant->getParameters() as $param) { + $paramsPhpDoc[] = $variantIdent . ' * @param ' . $param->getType()->describe($verbosityLevel) . ' $' . $param->getName() . "\n"; + + if ($param->getOutType() !== null) { + $paramsPhpDoc[] = $variantIdent . ' * @param-out ' . $param->getOutType()->describe($verbosityLevel) . ' $' . $param->getName() . "\n"; + } + + $passedByRef = $param->passedByReference(); + + if ($passedByRef->no()) { + $refDes = ''; + } elseif ($passedByRef->createsNewVariable()) { + $refDes = '&rw'; + } else { + $refDes = '&r'; + } + + $variadicDesc = $param->isVariadic() ? '...' : ''; + $defValueDesc = $param->getDefaultValue() !== null + ? ' = ' . $param->getDefaultValue()->describe($verbosityLevel) + : ''; + + $paramsNative[] = $param->getNativeType()->describe($verbosityLevel) . ' ' . $variadicDesc . $refDes . '$' . $param->getName() . $defValueDesc; + } + + $result .= $variantIdent . "/**\n"; + $result .= implode('', $paramsPhpDoc); + $result .= $variantIdent . ' * @return ' . $variant->getReturnType()->describe($verbosityLevel) . "\n"; + $result .= $variantIdent . " */\n"; + $paramsTxt = implode(', ', $paramsNative); + $result .= $variantIdent . 'function ' . $name . '(' . $paramsTxt . '): ' . $variant->getNativeReturnType()->describe($verbosityLevel) . "\n"; + } + + return $result; + } + + private static function generateClassPropertyDescription(string $propertyName): string + { + [$className, $propertyName] = explode('::', $propertyName); + + $reflectionProvider = self::getContainer()->getByType(ReflectionProvider::class); + + if (! $reflectionProvider->hasClass($className)) { + return "MISSING\n"; + } + + $classReflection = $reflectionProvider->getClass($className); + + if (! $classReflection->hasNativeProperty($propertyName)) { + return "MISSING\n"; + } + + $result = ''; + $propertyReflection = $classReflection->getNativeProperty($propertyName); + + if (! $propertyReflection->isDeprecated()->no()) { + $result .= 'Is deprecated: ' . $propertyReflection->isDeprecated()->describe() . "\n"; + } + + if (! $propertyReflection->isInternal()->no()) { + $result .= 'Is internal: ' . $propertyReflection->isDeprecated()->describe() . "\n"; + } + + if ($propertyReflection->isStatic()) { + $result .= "Static\n"; + } + + if ($propertyReflection->isReadOnly()) { + $result .= "Readonly\n"; + } + + switch (true) { + case $propertyReflection->isPublic(): + $visibility = 'public'; + break; + case $propertyReflection->isPrivate(): + $visibility = 'private'; + break; + default: + $visibility = 'protected'; + break; + } + + $result .= 'Visibility: ' . $visibility . "\n"; + $verbosityLevel = VerbosityLevel::precise(); + + if ($propertyReflection->isReadable()) { + $result .= 'Read type: ' . $propertyReflection->getReadableType()->describe($verbosityLevel) . "\n"; + } + + if ($propertyReflection->isWritable()) { + $result .= 'Write type: ' . $propertyReflection->getWritableType()->describe($verbosityLevel) . "\n"; + } + + return $result; + } + + public static function dumpInputSymbols(): void + { + $symbols = self::scrapeInputSymbols(); + $symbolsFile = self::getPhpSymbolsFile(); + @mkdir(dirname($symbolsFile), 0777, true); + $result = file_put_contents($symbolsFile, implode("\n", $symbols)); + + if ($result !== false) { + return; + } + + throw new ShouldNotHappenException('Failed write dump for reflection golden test.'); + } + + /** @return list */ + public static function scrapeInputSymbols(): array + { + $result = array_keys( + self::scrapeInputSymbolsFromFunctionMap() + + self::scrapeInputSymbolsFromPhp8Stubs() + + self::scrapeInputSymbolsFromPhpStormStubs() + + self::scrapeInputSymbolsFromReflection(), + ); + sort($result); + + return $result; + } + + /** @return array */ + private static function scrapeInputSymbolsFromFunctionMap(): array + { + $finder = new Finder(); + $files = $finder->files()->name('functionMap*.php')->in(__DIR__ . '/../../../resources'); + $combinedMap = []; + + foreach ($files as $file) { + if ($file->getBasename() === 'functionMap.php') { + $combinedMap += require $file->getPathname(); + continue; + } + + $deltaMap = require $file->getPathname(); + + // Deltas have new/old sections which contain the same format as the base functionMap.php + foreach ($deltaMap as $functionMap) { + $combinedMap += $functionMap; + } + } + + $result = []; + + foreach (array_keys($combinedMap) as $symbol) { + // skip duplicated variants + if (strpos($symbol, "'") !== false) { + continue; + } + + $parts = explode('::', $symbol); + + switch (count($parts)) { + case 1: + $result['FUNCTION ' . $symbol] = true; + break; + case 2: + $result['CLASS ' . $parts[0]] = true; + $result['METHOD ' . $symbol] = true; + break; + default: + throw new ShouldNotHappenException('Invalid symbol ' . $symbol); + } + } + + return $result; + } + + /** @return array */ + private static function scrapeInputSymbolsFromPhp8Stubs(): array + { + // Currently the Php8StubsMap only adds symbols for later versions, so let's max it. + $map = new Php8StubsMap(PHP_INT_MAX); + $files = []; + + foreach (array_merge($map->classes, $map->functions) as $file) { + $files[] = __DIR__ . '/../../../vendor/phpstan/php-8-stubs/' . $file; + } + + return self::scrapeSymbolsFromStubs($files); + } + + /** @return array */ + private static function scrapeInputSymbolsFromPhpStormStubs(): array + { + $files = []; + + foreach (PhpStormStubsMap::CLASSES as $file) { + $files[] = PhpStormStubsMap::DIR . '/' . $file; + } + + return self::scrapeSymbolsFromStubs($files); + } + + /** @return array */ + private static function scrapeInputSymbolsFromReflection(): array + { + $result = []; + + foreach (get_defined_functions()['internal'] as $function) { + $result['FUNCTION ' . $function] = true; + } + + foreach (get_declared_classes() as $class) { + $reflection = new ReflectionClass($class); + + if ($reflection->getFileName() !== false) { + continue; + } + + $className = $reflection->getName(); + $result['CLASS ' . $className] = true; + + foreach ($reflection->getMethods() as $method) { + $result['METHOD ' . $className . '::' . $method->getName()] = true; + } + + foreach ($reflection->getProperties() as $property) { + $result['PROPERTY ' . $className . '::$' . $property->getName()] = true; + } + } + + return $result; + } + + /** + * @param array $stubFiles + * @return array + */ + private static function scrapeSymbolsFromStubs(array $stubFiles): array + { + $parser = self::getContainer()->getService('defaultAnalysisParser'); + self::assertInstanceOf(Parser::class, $parser); + $visitor = new class () extends NodeVisitorAbstract { + + /** @var array */ + public array $symbols = []; + + private Node\Stmt\ClassLike $classLike; + + public function enterNode(Node $node) + { + if ($node instanceof Node\Stmt\ClassLike && $node->namespacedName !== null) { + $this->symbols['CLASS ' . $node->namespacedName->toString()] = true; + $this->classLike = $node; + } + + if ($node instanceof Node\Stmt\ClassMethod && isset($this->classLike->namespacedName)) { + $this->symbols['METHOD ' . $this->classLike->namespacedName->toString() . '::' . $node->name->name] = true; + } + + if ($node instanceof Node\Stmt\PropertyProperty && isset($this->classLike->namespacedName)) { + $this->symbols['PROPERTY ' . $this->classLike->namespacedName->toString() . '::$' . $node->name->toString()] = true; + } + + if ($node instanceof Node\Stmt\Function_) { + $this->symbols['FUNCTION ' . $node->name->name] = true; + } + + return null; + } + + public function leaveNode(Node $node) + { + if ($node instanceof Node\Stmt\ClassLike) { + unset($this->classLike); + } + + return null; + } + + }; + $traverser = new NodeTraverser(); + $traverser->addVisitor($visitor); + + foreach ($stubFiles as $file) { + $ast = $parser->parseFile($file); + $traverser->traverse($ast); + } + + return $visitor->symbols; + } + +} diff --git a/tests/PHPStan/Reflection/ReflectionProviderTest.php b/tests/PHPStan/Reflection/ReflectionProviderTest.php index 31ce22c6bb..06f7545d49 100644 --- a/tests/PHPStan/Reflection/ReflectionProviderTest.php +++ b/tests/PHPStan/Reflection/ReflectionProviderTest.php @@ -45,7 +45,7 @@ public function dataFunctionThrowType(): iterable yield [ 'random_int', - new ObjectType('Exception'), + new ObjectType('Random\RandomException'), ]; } @@ -140,4 +140,18 @@ public function testMethodThrowType(string $className, string $methodName, ?Type ); } + public function testNativeClassConstantTypeInEvaledClass(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + eval('namespace NativeClassConstantInEvaledClass; class Foo { public const int FOO = 1; }'); + + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass('NativeClassConstantInEvaledClass\\Foo'); + $constant = $class->getConstant('FOO'); + $this->assertSame('int', $constant->getValueType()->describe(VerbosityLevel::precise())); + } + } diff --git a/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php index 910e6eaf93..88a1b9d9c7 100644 --- a/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php @@ -14,10 +14,12 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\CallableType; +use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FileTypeMapper; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; @@ -94,9 +96,9 @@ public function dataFunctions(): array new ConstantStringType('error_count'), new ConstantStringType('errors'), ], [ - new IntegerType(), + IntegerRangeType::fromInterval(0, null), new ArrayType(new IntegerType(), new StringType()), - new IntegerType(), + IntegerRangeType::fromInterval(0, null), new ArrayType(new IntegerType(), new StringType()), ]), ]), @@ -139,7 +141,7 @@ public function testFunctions( { $provider = $this->createProvider(); $reflector = self::getContainer()->getByType(Reflector::class); - $signatures = $provider->getFunctionSignatures($functionName, null, new ReflectionFunction($reflector->reflectFunction($functionName))); + $signatures = $provider->getFunctionSignatures($functionName, null, new ReflectionFunction($reflector->reflectFunction($functionName)))['positional']; $this->assertCount(1, $signatures); $this->assertSignature($parameters, $returnType, $nativeReturnType, $variadic, $signatures[0]); } @@ -153,6 +155,7 @@ private function createProvider(): Php8SignatureMapProvider self::getContainer()->getByType(SignatureMapParser::class), self::getContainer()->getByType(InitializerExprTypeResolver::class), $phpVersion, + true, ), self::getContainer()->getByType(FileNodesFetcher::class), self::getContainer()->getByType(FileTypeMapper::class), @@ -187,7 +190,8 @@ public function dataMethods(): array 'optional' => true, 'type' => new UnionType([ new ObjectWithoutClassType(), - new StringType(), + new ClassStringType(), + new ConstantStringType('static'), new NullType(), ]), 'nativeType' => new UnionType([ @@ -266,7 +270,7 @@ public function testMethods( ): void { $provider = $this->createProvider(); - $signatures = $provider->getMethodSignatures($className, $methodName, null); + $signatures = $provider->getMethodSignatures($className, $methodName, null)['positional']; $this->assertCount(1, $signatures); $this->assertSignature($parameters, $returnType, $nativeReturnType, $variadic, $signatures[0]); } diff --git a/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php b/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php index 5e30b2b90c..00ed1a4159 100644 --- a/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php @@ -56,6 +56,7 @@ public function dataGetFunctions(): array PassedByReference::createNo(), false, null, + null, ), new ParameterSignature( 'fields', @@ -65,6 +66,7 @@ public function dataGetFunctions(): array PassedByReference::createNo(), false, null, + null, ), new ParameterSignature( 'delimiter', @@ -74,6 +76,7 @@ public function dataGetFunctions(): array PassedByReference::createNo(), false, null, + null, ), new ParameterSignature( 'enclosure', @@ -83,6 +86,7 @@ public function dataGetFunctions(): array PassedByReference::createNo(), false, null, + null, ), new ParameterSignature( 'escape_char', @@ -92,6 +96,7 @@ public function dataGetFunctions(): array PassedByReference::createNo(), false, null, + null, ), ], new IntegerType(), @@ -112,6 +117,7 @@ public function dataGetFunctions(): array PassedByReference::createNo(), false, null, + null, ), ], new BooleanType(), @@ -132,6 +138,7 @@ public function dataGetFunctions(): array PassedByReference::createReadsArgument(), false, null, + null, ), ], new BooleanType(), @@ -155,6 +162,7 @@ public function dataGetFunctions(): array PassedByReference::createNo(), false, null, + null, ), new ParameterSignature( 'out', @@ -164,6 +172,7 @@ public function dataGetFunctions(): array PassedByReference::createCreatesNewVariable(), false, null, + null, ), new ParameterSignature( 'notext', @@ -173,6 +182,7 @@ public function dataGetFunctions(): array PassedByReference::createNo(), false, null, + null, ), ], new BooleanType(), @@ -217,6 +227,7 @@ public function dataGetFunctions(): array PassedByReference::createNo(), false, null, + null, ), new ParameterSignature( 'arr2', @@ -226,6 +237,7 @@ public function dataGetFunctions(): array PassedByReference::createNo(), false, null, + null, ), new ParameterSignature( '...', @@ -235,6 +247,7 @@ public function dataGetFunctions(): array PassedByReference::createNo(), true, null, + null, ), ], new ArrayType(new MixedType(), new MixedType()), @@ -255,6 +268,7 @@ public function dataGetFunctions(): array PassedByReference::createNo(), false, null, + null, ), new ParameterSignature( 'event', @@ -264,6 +278,7 @@ public function dataGetFunctions(): array PassedByReference::createNo(), false, null, + null, ), new ParameterSignature( '...', @@ -273,6 +288,7 @@ public function dataGetFunctions(): array PassedByReference::createNo(), true, null, + null, ), ], new ResourceType(), @@ -293,6 +309,7 @@ public function dataGetFunctions(): array PassedByReference::createNo(), false, null, + null, ), new ParameterSignature( 'args', @@ -302,6 +319,7 @@ public function dataGetFunctions(): array PassedByReference::createNo(), true, null, + null, ), ], new StringType(), @@ -322,6 +340,7 @@ public function dataGetFunctions(): array PassedByReference::createNo(), false, null, + null, ), new ParameterSignature( 'args', @@ -331,6 +350,7 @@ public function dataGetFunctions(): array PassedByReference::createNo(), true, null, + null, ), ], new StringType(), @@ -361,6 +381,7 @@ public function dataGetFunctions(): array PassedByReference::createNo(), false, null, + null, ), ], new StaticType($reflectionProvider->getClass(DateTime::class)), @@ -381,6 +402,7 @@ public function dataGetFunctions(): array PassedByReference::createReadsArgument(), false, null, + null, ), new ParameterSignature( 'strings', @@ -390,6 +412,7 @@ public function dataGetFunctions(): array PassedByReference::createReadsArgument(), true, null, + null, ), ], new BooleanType(), @@ -473,7 +496,7 @@ public function dataParseAll(): array public function testParseAll(int $phpVersionId): void { $parser = self::getContainer()->getByType(SignatureMapParser::class); - $provider = new FunctionSignatureMapProvider($parser, self::getContainer()->getByType(InitializerExprTypeResolver::class), new PhpVersion($phpVersionId)); + $provider = new FunctionSignatureMapProvider($parser, self::getContainer()->getByType(InitializerExprTypeResolver::class), new PhpVersion($phpVersionId), true); $signatureMap = $provider->getSignatureMap(); $reflector = self::getContainer()->getByType(Reflector::class); @@ -512,7 +535,7 @@ public function testParseAll(int $phpVersionId): void } try { - $signatures = $provider->getFunctionSignatures($functionName, $className, $reflectionFunction); + $signatures = $provider->getFunctionSignatures($functionName, $className, $reflectionFunction)['positional']; $count += count($signatures); } catch (ParserException $e) { $this->fail(sprintf('Could not parse %s: %s.', $functionName, $e->getMessage())); diff --git a/tests/PHPStan/Reflection/Type/IntersectionTypeMethodReflectionTest.php b/tests/PHPStan/Reflection/Type/IntersectionTypeMethodReflectionTest.php index f6c315b219..2d7d3ca357 100644 --- a/tests/PHPStan/Reflection/Type/IntersectionTypeMethodReflectionTest.php +++ b/tests/PHPStan/Reflection/Type/IntersectionTypeMethodReflectionTest.php @@ -2,7 +2,7 @@ namespace PHPStan\Reflection\Type; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; @@ -36,9 +36,9 @@ public function testMultipleDeprecationsAreJoined(): void $this->assertSame('Deprecated #1 Deprecated #2', $reflection->getDeprecatedDescription()); } - private function createDeprecatedMethod(TrinaryLogic $deprecated, ?string $deprecationText): MethodReflection + private function createDeprecatedMethod(TrinaryLogic $deprecated, ?string $deprecationText): ExtendedMethodReflection { - $method = $this->createMock(MethodReflection::class); + $method = $this->createMock(ExtendedMethodReflection::class); $method->method('isDeprecated')->willReturn($deprecated); $method->method('getDeprecatedDescription')->willReturn($deprecationText); return $method; diff --git a/tests/PHPStan/Reflection/Type/UnionTypeMethodReflectionTest.php b/tests/PHPStan/Reflection/Type/UnionTypeMethodReflectionTest.php index 9df28a8e83..b41d8d9636 100644 --- a/tests/PHPStan/Reflection/Type/UnionTypeMethodReflectionTest.php +++ b/tests/PHPStan/Reflection/Type/UnionTypeMethodReflectionTest.php @@ -2,7 +2,7 @@ namespace PHPStan\Reflection\Type; -use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; @@ -36,9 +36,9 @@ public function testMultipleDeprecationsAreJoined(): void $this->assertSame('Deprecated #1 Deprecated #2', $reflection->getDeprecatedDescription()); } - private function createDeprecatedMethod(TrinaryLogic $deprecated, ?string $deprecationText): MethodReflection + private function createDeprecatedMethod(TrinaryLogic $deprecated, ?string $deprecationText): ExtendedMethodReflection { - $method = $this->createMock(MethodReflection::class); + $method = $this->createMock(ExtendedMethodReflection::class); $method->method('isDeprecated')->willReturn($deprecated); $method->method('getDeprecatedDescription')->willReturn($deprecationText); return $method; diff --git a/tests/PHPStan/Reflection/data/ClassWithInheritedPhpdoc.php b/tests/PHPStan/Reflection/data/ClassWithInheritedPhpdoc.php new file mode 100644 index 0000000000..e4a0c5e696 --- /dev/null +++ b/tests/PHPStan/Reflection/data/ClassWithInheritedPhpdoc.php @@ -0,0 +1,10 @@ +getName() === 'AllowedSubTypesClassReflectionExtensionTest\\Foo'; + } + + public function getAllowedSubTypes(ClassReflection $classReflection): array + { + return [ + new ObjectType('AllowedSubTypesClassReflectionExtensionTest\\Bar'), + new ObjectType('AllowedSubTypesClassReflectionExtensionTest\\Baz'), + new ObjectType('AllowedSubTypesClassReflectionExtensionTest\\Qux'), + ]; + } +} + +function acceptsFoo(Foo $foo): void { + assertType('AllowedSubTypesClassReflectionExtensionTest\\Foo', $foo); + + if ($foo instanceof Bar) { + return; + } + + assertType('AllowedSubTypesClassReflectionExtensionTest\\Foo~AllowedSubTypesClassReflectionExtensionTest\\Bar', $foo); + + if ($foo instanceof Qux) { + return; + } + + assertType('AllowedSubTypesClassReflectionExtensionTest\\Baz', $foo); +} diff --git a/tests/PHPStan/Reflection/data/function-reflection.neon b/tests/PHPStan/Reflection/data/function-reflection.neon new file mode 100644 index 0000000000..22af4a6bdf --- /dev/null +++ b/tests/PHPStan/Reflection/data/function-reflection.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - function-reflection.stub diff --git a/tests/PHPStan/Reflection/data/function-reflection.stub b/tests/PHPStan/Reflection/data/function-reflection.stub new file mode 100644 index 0000000000..8e4ed01324 --- /dev/null +++ b/tests/PHPStan/Reflection/data/function-reflection.stub @@ -0,0 +1,48 @@ += 8.1 + +namespace ReturnsByReference; + +enum E { + case E1; + + function enumFoo() {} + + function &refEnumFoo() {} +} diff --git a/tests/PHPStan/Reflection/data/returns-by-reference.php b/tests/PHPStan/Reflection/data/returns-by-reference.php new file mode 100644 index 0000000000..26fb1051b3 --- /dev/null +++ b/tests/PHPStan/Reflection/data/returns-by-reference.php @@ -0,0 +1,28 @@ +name instanceof Node\Name) { @@ -32,10 +28,18 @@ public function processNode(Node $node, Scope $scope): array } if (count($node->getArgs()) === 1 && $node->getArgs()[0]->value instanceof Node\Scalar\String_) { - return [$node->getArgs()[0]->value->value]; + return [ + RuleErrorBuilder::message($node->getArgs()[0]->value->value) + ->identifier('tests.alwaysFail') + ->build(), + ]; } - return ['Fail.']; + return [ + RuleErrorBuilder::message('Fail.') + ->identifier('tests.alwaysFail') + ->build(), + ]; } } diff --git a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php index 372b594683..9b95f57e25 100644 --- a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php @@ -32,17 +32,32 @@ public function testRuleOutOfPhpStan(): void $this->analyse([__DIR__ . '/data/class-implements-out-of-phpstan.php'], [ [ 'Implementing PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 18, + 19, $tip, ], [ 'Implementing PHPStan\Type\Type is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 52, + 53, $tip, ], [ 'Implementing PHPStan\Reflection\ReflectionProvider is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 317, + 333, + $tip, + ], + [ + 'Implementing PHPStan\Analyser\Scope is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', + 338, + $tip, + ], + [ + 'Implementing PHPStan\Reflection\FunctionReflection is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', + 343, + $tip, + ], + [ + 'Implementing PHPStan\Reflection\ExtendedMethodReflection is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', + 347, $tip, ], ]); diff --git a/tests/PHPStan/Rules/Api/ApiInstanceofTypeRuleTest.php b/tests/PHPStan/Rules/Api/ApiInstanceofTypeRuleTest.php new file mode 100644 index 0000000000..6be9f18d4b --- /dev/null +++ b/tests/PHPStan/Rules/Api/ApiInstanceofTypeRuleTest.php @@ -0,0 +1,46 @@ + + */ +class ApiInstanceofTypeRuleTest extends RuleTestCase +{ + + public function getRule(): Rule + { + return new ApiInstanceofTypeRule($this->createReflectionProvider(), true, true); + } + + public function testRule(): void + { + $tipText = 'Learn more: https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated'; + $this->analyse([__DIR__ . '/data/instanceof-type.php'], [ + [ + 'Doing instanceof PHPStan\Type\TypeWithClassName is error-prone and deprecated. Use Type::getObjectClassNames() or Type::getObjectClassReflections() instead.', + 20, + $tipText, + ], + [ + 'Doing instanceof phpstan\type\typewithclassname is error-prone and deprecated. Use Type::getObjectClassNames() or Type::getObjectClassReflections() instead.', + 24, + $tipText, + ], + [ + 'Doing instanceof PHPStan\Type\TypeWithClassName is error-prone and deprecated. Use Type::getObjectClassNames() or Type::getObjectClassReflections() instead.', + 36, + $tipText, + ], + [ + 'Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated.', + 40, + $tipText, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php b/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php index 2b14437482..770c59da3a 100644 --- a/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php @@ -32,17 +32,22 @@ public function testRuleOutOfPhpStan(): void $this->analyse([__DIR__ . '/data/interface-extends-out-of-phpstan.php'], [ [ 'Extending PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 9, + 10, $tip, ], [ 'Extending PHPStan\Type\Type is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 19, + 20, $tip, ], [ 'Extending PHPStan\Reflection\ReflectionProvider is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 24, + 25, + $tip, + ], + [ + 'Extending PHPStan\Reflection\ExtendedMethodReflection is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', + 30, $tip, ], ]); diff --git a/tests/PHPStan/Rules/Api/GetTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Api/GetTemplateTypeRuleTest.php new file mode 100644 index 0000000000..c65e8a6fd4 --- /dev/null +++ b/tests/PHPStan/Rules/Api/GetTemplateTypeRuleTest.php @@ -0,0 +1,33 @@ + + */ +class GetTemplateTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new GetTemplateTypeRule($this->createReflectionProvider()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/get-template-type.php'], [ + [ + 'Call to PHPStan\Type\Type::getTemplateType() references unknown template type TSendd on class Generator.', + 15, + ], + [ + 'Call to PHPStan\Type\ObjectType::getTemplateType() references unknown template type TSendd on class Generator.', + 21, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php b/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php index e705828b93..b7b9d69943 100644 --- a/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php +++ b/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; use PHPStan\Reflection\ClassMemberAccessAnswerer; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\DynamicFunctionThrowTypeExtension; @@ -106,7 +107,7 @@ public function hasMethod(string $methodName): \PHPStan\TrinaryLogic // TODO: Implement hasMethod() method. } - public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): \PHPStan\Reflection\MethodReflection + public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): \PHPStan\Reflection\ExtendedMethodReflection { // TODO: Implement getMethod() method. } @@ -161,6 +162,11 @@ public function isOversizedArray(): \PHPStan\TrinaryLogic // TODO: Implement isOversizedArray() method. } + public function isList(): \PHPStan\TrinaryLogic + { + // TODO: Implement isList() method. + } + public function isOffsetAccessible(): \PHPStan\TrinaryLogic { // TODO: Implement isOffsetAccessible() method. @@ -231,6 +237,11 @@ public function toArray(): \PHPStan\Type\Type // TODO: Implement toArray() method. } + public function toArrayKey(): \PHPStan\Type\Type + { + // TODO: Implement toArrayKey() method. + } + public function isSmallerThan(Type $otherType): \PHPStan\TrinaryLogic { // TODO: Implement isSmallerThan() method. @@ -296,6 +307,11 @@ public function traverse(callable $cb): \PHPStan\Type\Type // TODO: Implement traverse() method. } + public function traverseSimultaneously(Type $right, callable $cb): Type + { + // TODO: Implement traverseSimultaneously() method. + } + public function generalize(GeneralizePrecision $precision): Type { // TODO: Implement generalize() method. @@ -318,3 +334,15 @@ abstract class Dolor implements ReflectionProvider { } + +abstract class MyScope implements Scope +{ + +} + +abstract class MyFunctionReflection implements FunctionReflection +{} + + +abstract class MyMethodReflection implements ExtendedMethodReflection +{} diff --git a/tests/PHPStan/Rules/Api/data/get-template-type.php b/tests/PHPStan/Rules/Api/data/get-template-type.php new file mode 100644 index 0000000000..5cb59086b8 --- /dev/null +++ b/tests/PHPStan/Rules/Api/data/get-template-type.php @@ -0,0 +1,24 @@ +getTemplateType(Generator::class, 'TSend'); + $type->getTemplateType(Generator::class, 'TSendd'); + } + + public function doBar(ObjectType $type): void + { + $type->getTemplateType(Generator::class, 'TSend'); + $type->getTemplateType(Generator::class, 'TSendd'); + } + +} diff --git a/tests/PHPStan/Rules/Api/data/instanceof-type.php b/tests/PHPStan/Rules/Api/data/instanceof-type.php new file mode 100644 index 0000000000..eabc5f6c5e --- /dev/null +++ b/tests/PHPStan/Rules/Api/data/instanceof-type.php @@ -0,0 +1,45 @@ +createReflectionProvider(), true, false, true, false, false), + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false), ); } diff --git a/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php b/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php index 79bfa5af52..a536ce6cf5 100644 --- a/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php @@ -13,13 +13,15 @@ class ArrayDestructuringRuleTest extends RuleTestCase { + private bool $bleedingEdge = false; + protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false); + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false); return new ArrayDestructuringRule( $ruleLevelHelper, - new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true), + new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, $this->bleedingEdge, false, false), ); } diff --git a/tests/PHPStan/Rules/Arrays/ArrayUnpackingRuleTest.php b/tests/PHPStan/Rules/Arrays/ArrayUnpackingRuleTest.php index 50f1b2fc3e..b733df51cd 100644 --- a/tests/PHPStan/Rules/Arrays/ArrayUnpackingRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/ArrayUnpackingRuleTest.php @@ -16,11 +16,13 @@ class ArrayUnpackingRuleTest extends RuleTestCase private bool $checkUnions; + private bool $checkBenevolentUnions = false; + protected function getRule(): Rule { return new ArrayUnpackingRule( self::getContainer()->getByType(PhpVersion::class), - new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnions, false, false), + new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnions, false, false, true, $this->checkBenevolentUnions), ); } @@ -31,6 +33,7 @@ public function testRule(): void } $this->checkUnions = true; + $this->checkBenevolentUnions = true; $this->analyse([__DIR__ . '/data/array-unpacking.php'], [ [ 'Array unpacking cannot be used on an array with potential string keys: array{foo: \'bar\', 0: 1, 1: 2, 2: 3}', @@ -60,6 +63,45 @@ public function testRule(): void 'Array unpacking cannot be used on an array with string keys: array{foo: string, bar: int}', 63, ], + [ + 'Array unpacking cannot be used on an array with potential string keys: array', + 71, + ], + ]); + } + + public function testRuleDoNotCheckBenevolentUnion(): void + { + if (PHP_VERSION_ID >= 80100) { + $this->markTestSkipped('Test requires PHP version <= 8.0'); + } + + $this->checkUnions = true; + $this->analyse([__DIR__ . '/data/array-unpacking.php'], [ + [ + 'Array unpacking cannot be used on an array with potential string keys: array{foo: \'bar\', 0: 1, 1: 2, 2: 3}', + 7, + ], + [ + 'Array unpacking cannot be used on an array with string keys: array', + 18, + ], + [ + 'Array unpacking cannot be used on an array with potential string keys: array', + 40, + ], + [ + 'Array unpacking cannot be used on an array with potential string keys: array', + 52, + ], + [ + 'Array unpacking cannot be used on an array with string keys: array{foo: string, bar: int}', + 63, + ], + [ + 'Array unpacking cannot be used on an array with potential string keys: array', + 71, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php b/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php index a570343822..a99a20557c 100644 --- a/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php @@ -30,4 +30,14 @@ public function testRule(): void ]); } + public function testBug7913(): void + { + $this->analyse([__DIR__ . '/data/bug-7913.php'], []); + } + + public function testBug8292(): void + { + $this->analyse([__DIR__ . '/data/bug-8292.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php index 625f884a61..70c671135a 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php @@ -3,7 +3,9 @@ namespace PHPStan\Rules\Arrays; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -13,7 +15,8 @@ class InvalidKeyInArrayDimFetchRuleTest extends RuleTestCase protected function getRule(): Rule { - return new InvalidKeyInArrayDimFetchRule(true); + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false); + return new InvalidKeyInArrayDimFetchRule($ruleLevelHelper, true); } public function testInvalidKey(): void @@ -35,6 +38,60 @@ public function testInvalidKey(): void 'Invalid array key type DateTimeImmutable.', 31, ], + [ + 'Invalid array key type DateTimeImmutable.', + 45, + ], + [ + 'Invalid array key type DateTimeImmutable.', + 46, + ], + [ + 'Invalid array key type DateTimeImmutable.', + 47, + ], + [ + 'Invalid array key type stdClass.', + 47, + ], + [ + 'Invalid array key type DateTimeImmutable.', + 48, + ], + ]); + } + + public function testBug6315(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-6315.php'], [ + [ + 'Invalid array key type Bug6315\FooEnum::A.', + 18, + ], + [ + 'Invalid array key type Bug6315\FooEnum::A.', + 19, + ], + [ + 'Invalid array key type Bug6315\FooEnum::A.', + 20, + ], + [ + 'Invalid array key type Bug6315\FooEnum::B.', + 21, + ], + [ + 'Invalid array key type Bug6315\FooEnum::A.', + 21, + ], + [ + 'Invalid array key type Bug6315\FooEnum::A.', + 22, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php index 62cf77d9f4..7a40122d1c 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -62,4 +63,18 @@ public function testInvalidKeyShortArray(): void ]); } + public function testInvalidKeyEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/invalid-key-array-item-enum.php'], [ + [ + 'Invalid array key type InvalidKeyArrayItemEnum\FooEnum::A.', + 14, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php b/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php index 88efeaa9ab..dbb64b29ca 100644 --- a/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use function array_merge; +use function usort; use const PHP_VERSION_ID; /** @@ -15,9 +17,11 @@ class IterableInForeachRuleTest extends RuleTestCase private bool $checkExplicitMixed = false; + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { - return new IterableInForeachRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, false)); + return new IterableInForeachRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false)); } public function testCheckWithMaybes(): void @@ -75,4 +79,65 @@ public function testBug6564(): void $this->analyse([__DIR__ . '/data/bug-6564.php'], []); } + public function testBug4335(): void + { + $this->analyse([__DIR__ . '/data/bug-4335.php'], []); + } + + public function dataMixed(): array + { + $explicitOnlyErrors = [ + [ + 'Argument of an invalid type T of mixed supplied for foreach, only iterables are supported.', + 11, + ], + [ + 'Argument of an invalid type mixed supplied for foreach, only iterables are supported.', + 14, + ], + ]; + $implicitOnlyErrors = [ + [ + 'Argument of an invalid type mixed supplied for foreach, only iterables are supported.', + 17, + ], + ]; + $combinedErrors = array_merge($explicitOnlyErrors, $implicitOnlyErrors); + usort($combinedErrors, static fn (array $a, array $b): int => $a[1] <=> $b[1]); + + return [ + [ + true, + false, + $explicitOnlyErrors, + ], + [ + false, + true, + $implicitOnlyErrors, + ], + [ + true, + true, + $combinedErrors, + ], + [ + false, + false, + [], + ], + ]; + } + + /** + * @dataProvider dataMixed + * @param list $errors + */ + public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void + { + $this->checkExplicitMixed = $checkExplicitMixed; + $this->checkImplicitMixed = $checkImplicitMixed; + $this->analyse([__DIR__ . '/data/foreach-mixed.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index cc2dca6fbf..7cf2a42ef1 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -15,13 +15,21 @@ class NonexistentOffsetInArrayDimFetchRuleTest extends RuleTestCase private bool $checkExplicitMixed = false; + private bool $checkImplicitMixed = false; + + private bool $bleedingEdge = false; + + private bool $reportPossiblyNonexistentGeneralArrayOffset = false; + + private bool $reportPossiblyNonexistentConstantArrayOffset = false; + protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, false); + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false); return new NonexistentOffsetInArrayDimFetchRule( $ruleLevelHelper, - new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true), + new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true, $this->bleedingEdge, $this->reportPossiblyNonexistentGeneralArrayOffset, $this->reportPossiblyNonexistentConstantArrayOffset), true, ); } @@ -103,18 +111,10 @@ public function testRule(): void 'Offset \'b\' does not exist on array{a: \'blabla\'}.', 228, ], - [ - 'Offset string does not exist on array.', - 240, - ], [ 'Cannot access offset \'a\' on Closure(): void.', 253, ], - [ - 'Offset string does not exist on array.', - 308, - ], [ 'Offset null does not exist on array.', 310, @@ -170,6 +170,143 @@ public function testRule(): void ]); } + public function testRuleBleedingEdge(): void + { + $this->bleedingEdge = true; + $this->analyse([__DIR__ . '/data/nonexistent-offset.php'], [ + [ + 'Offset \'b\' does not exist on array{a: stdClass, 0: 2}.', + 17, + ], + [ + 'Offset 1 does not exist on array{a: stdClass, 0: 2}.', + 18, + ], + [ + 'Offset \'a\' does not exist on array{b: 1}.', + 55, + ], + [ + 'Access to offset \'bar\' on an unknown class NonexistentOffset\Bar.', + 101, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Access to an offset on an unknown class NonexistentOffset\Bar.', + 102, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Offset 0 does not exist on array.', + 111, + ], + [ + 'Offset \'0\' does not exist on array.', + 112, + ], + [ + 'Offset int does not exist on array.', + 114, + ], + [ + 'Offset \'test\' does not exist on null.', + 126, + ], + [ + 'Cannot access offset 42 on int.', + 142, + ], + [ + 'Cannot access offset 42 on float.', + 143, + ], + [ + 'Cannot access offset 42 on bool.', + 144, + ], + [ + 'Cannot access offset 42 on resource.', + 145, + ], + [ + 'Offset \'c\' might not exist on array{c: false}|array{c: true}|array{e: true}.', + 171, + ], + [ + 'Offset int might not exist on array{}|array{1: 1, 2: 2}|array{3: 3, 4: 4}.', + 190, + ], + [ + 'Offset int might not exist on array{}|array{1: 1, 2: 2}|array{3: 3, 4: 4}.', + 193, + ], + [ + 'Offset \'b\' does not exist on array{a: \'blabla\'}.', + 225, + ], + [ + 'Offset \'b\' does not exist on array{a: \'blabla\'}.', + 228, + ], + [ + 'Cannot access offset \'a\' on Closure(): void.', + 253, + ], + [ + 'Offset null does not exist on array.', + 310, + ], + [ + 'Offset int does not exist on array.', + 312, + ], + [ + 'Offset \'baz\' might not exist on array{bar: 1, baz?: 2}.', + 344, + ], + [ + 'Offset \'foo\' does not exist on ArrayAccess.', + 411, + ], + [ + 'Cannot access offset \'foo\' on stdClass.', + 423, + ], + [ + 'Cannot access offset \'foo\' on true.', + 426, + ], + [ + 'Cannot access offset \'foo\' on false.', + 429, + ], + [ + 'Cannot access offset \'foo\' on resource.', + 433, + ], + [ + 'Cannot access offset \'foo\' on 42.', + 436, + ], + [ + 'Cannot access offset \'foo\' on 4.141.', + 439, + ], + [ + 'Cannot access offset \'foo\' on array|int.', + 443, + ], + [ + 'Offset \'feature_pretty…\' might not exist on array{version: non-falsy-string, commit: string|null, pretty_version: string|null, feature_version: non-falsy-string, feature_pretty_version?: string|null}.', + 504, + ], + [ + "Cannot access offset 'foo' on bool.", + 517, + ], + ]); + } + public function testStrings(): void { $this->analyse([__DIR__ . '/data/strings-offset-access.php'], [ @@ -405,14 +542,6 @@ public function testBug7229(): void 'Cannot access offset string on mixed.', 24, ], - [ - 'Cannot access offset string on mixed.', - 29, - ], - [ - 'Cannot access offset string on mixed.', - 32, - ], ]); } @@ -544,4 +673,127 @@ public function testBug8068(): void ]); } + public function testBug6243(): void + { + if (PHP_VERSION_ID < 704000) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->analyse([__DIR__ . '/data/bug-6243.php'], []); + } + + public function testBug8356(): void + { + $this->bleedingEdge = true; + $this->analyse([__DIR__ . '/data/bug-8356.php'], [ + [ + "Offset 'x' might not exist on array|string.", + 7, + ], + ]); + } + + public function testBug6605(): void + { + $this->analyse([__DIR__ . '/data/bug-6605.php'], [ + [ + "Cannot access offset 'invalidoffset' on Bug6605\\X.", + 11, + ], + [ + "Offset 'invalid' does not exist on array{a: array{b: array{5}}}.", + 16, + ], + [ + "Offset 'invalid' does not exist on array{b: array{5}}.", + 17, + ], + ]); + } + + public function testBug9991(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-9991.php'], [ + [ + 'Cannot access offset \'title\' on mixed.', + 9, + ], + ]); + } + + public function testBug8166(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8166.php'], [ + [ + 'Offset \'b\' does not exist on array{a: 1}.', + 22, + ], + [ + 'Offset \'b\' does not exist on array<\'a\', string>.', + 23, + ], + ]); + } + + public function testMixed(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/offset-access-mixed.php'], [ + [ + 'Cannot access offset 5 on T of mixed.', + 11, + ], + [ + 'Cannot access offset 5 on mixed.', + 16, + ], + [ + 'Cannot access offset 5 on mixed.', + 21, + ], + ]); + } + + public function dataReportPossiblyNonexistentArrayOffset(): iterable + { + yield [false, false, []]; + yield [false, true, [ + [ + 'Offset string might not exist on array{foo: 1}.', + 20, + ], + ]]; + yield [true, false, [ + [ + "Offset 'foo' might not exist on array.", + 9, + ], + ]]; + yield [true, true, [ + [ + "Offset 'foo' might not exist on array.", + 9, + ], + [ + 'Offset string might not exist on array{foo: 1}.', + 20, + ], + ]]; + } + + /** + * @dataProvider dataReportPossiblyNonexistentArrayOffset + * @param list $errors + */ + public function testReportPossiblyNonexistentArrayOffset(bool $reportPossiblyNonexistentGeneralArrayOffset, bool $reportPossiblyNonexistentConstantArrayOffset, array $errors): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = $reportPossiblyNonexistentGeneralArrayOffset; + $this->reportPossiblyNonexistentConstantArrayOffset = $reportPossiblyNonexistentConstantArrayOffset; + + $this->analyse([__DIR__ . '/data/report-possibly-nonexistent-array-offset.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php index 7393ca6d11..2daf7b7ed4 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php @@ -17,7 +17,7 @@ class OffsetAccessAssignOpRuleTest extends RuleTestCase protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnions, false, false); + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnions, false, false, true, false); return new OffsetAccessAssignOpRule($ruleLevelHelper); } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php index 0db767dad1..ea8b897b57 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php @@ -17,7 +17,7 @@ class OffsetAccessAssignmentRuleTest extends RuleTestCase protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnionTypes, false, false); + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnionTypes, false, false, true, false); return new OffsetAccessAssignmentRule($ruleLevelHelper); } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php index 52c10bafeb..0129923ae8 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php @@ -15,7 +15,7 @@ class OffsetAccessValueAssignmentRuleTest extends RuleTestCase protected function getRule(): Rule { - return new OffsetAccessValueAssignmentRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false)); + return new OffsetAccessValueAssignmentRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false)); } public function testRule(): void @@ -66,4 +66,13 @@ public function testRuleWithNullsafeVariant(): void ]); } + public function testBug5655b(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-5655b.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php b/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php index f84ae3e5b3..32d4900686 100644 --- a/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use function array_merge; +use function usort; use const PHP_VERSION_ID; /** @@ -13,9 +15,13 @@ class UnpackIterableInArrayRuleTest extends RuleTestCase { + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { - return new UnpackIterableInArrayRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false)); + return new UnpackIterableInArrayRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false)); } public function testRule(): void @@ -50,4 +56,60 @@ public function testRuleWithNullsafeVariant(): void ]); } + public function dataMixed(): array + { + $explicitOnlyErrors = [ + [ + 'Only iterables can be unpacked, T of mixed given.', + 11, + ], + [ + 'Only iterables can be unpacked, mixed given.', + 12, + ], + ]; + $implicitOnlyErrors = [ + [ + 'Only iterables can be unpacked, mixed given.', + 13, + ], + ]; + $combinedErrors = array_merge($explicitOnlyErrors, $implicitOnlyErrors); + usort($combinedErrors, static fn (array $a, array $b): int => $a[1] <=> $b[1]); + + return [ + [ + true, + false, + $explicitOnlyErrors, + ], + [ + false, + true, + $implicitOnlyErrors, + ], + [ + true, + true, + $combinedErrors, + ], + [ + false, + false, + [], + ], + ]; + } + + /** + * @dataProvider dataMixed + * @param list $errors + */ + public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void + { + $this->checkExplicitMixed = $checkExplicitMixed; + $this->checkImplicitMixed = $checkImplicitMixed; + $this->analyse([__DIR__ . '/data/unpack-mixed.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/array-unpacking.php b/tests/PHPStan/Rules/Arrays/data/array-unpacking.php index 7e1afcf9db..d7d1e64de5 100644 --- a/tests/PHPStan/Rules/Arrays/data/array-unpacking.php +++ b/tests/PHPStan/Rules/Arrays/data/array-unpacking.php @@ -19,7 +19,7 @@ function stringKeyedArray(array $bar) } /** @param array $bar */ -function unionKeyedArray(array $bar) +function benevolentUnionKeyedArray(array $bar) { $baz = [...$bar]; } @@ -64,3 +64,9 @@ function unpackingArrayShapes(array $foo, array $bar) ...$bar, ]; } + +/** @param array $bar */ +function unionKeyedArray(array $bar) +{ + $baz = [...$bar]; +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-4335.php b/tests/PHPStan/Rules/Arrays/data/bug-4335.php new file mode 100644 index 0000000000..0824514d15 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-4335.php @@ -0,0 +1,19 @@ + $v) { + var_dump($k, $v); + } + foreach (class_parents($this) as $k => $v) { + var_dump($k, $v); + } + foreach (class_uses($this) as $k => $v) { + var_dump($k, $v); + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-5655b.php b/tests/PHPStan/Rules/Arrays/data/bug-5655b.php new file mode 100644 index 0000000000..3f61021623 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-5655b.php @@ -0,0 +1,33 @@ + */ + $list = []; + + $list[] = [ + 'foo' => 'baz', + ]; + +// Case with map... FAIL + + /** @var WeakMap */ + $map = new WeakMap(); + + $map[new stdClass()] = [ + 'foo' => 'foo', + 'bar' => 'bar', + ]; + + $map[new stdClass()] = [ + 'foo' => 'baz', + ]; +} + diff --git a/tests/PHPStan/Rules/Arrays/data/bug-6000.php b/tests/PHPStan/Rules/Arrays/data/bug-6000.php index a84e7b4499..c409cbfda1 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-6000.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-6000.php @@ -9,9 +9,10 @@ function (): void { $data = []; foreach ($data as $key => $value) { - assertType('array|string>', $data[$key]); + assertType('array|string, array|string>', $data[$key]); if ($key === 'classmap') { - assertType('array', $data[$key]); + assertType('list', $data[$key]); + assertType('list', $value); echo implode(', ', $value); // not working :( echo implode(', ', $data[$key]); // this works though?! } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-6243.php b/tests/PHPStan/Rules/Arrays/data/bug-6243.php new file mode 100644 index 0000000000..97c62f8512 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-6243.php @@ -0,0 +1,22 @@ += 7.4 + +namespace Bug6243; + +class Foo +{ + /** @var list|(\ArrayAccess&iterable) */ + private iterable $values; + + /** + * @param list $values + */ + public function update(array $values): void { + foreach ($this->values as $key => $_) { + unset($this->values[$key]); + } + + foreach ($values as $value) { + $this->values[] = $value; + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-6315.php b/tests/PHPStan/Rules/Arrays/data/bug-6315.php new file mode 100644 index 0000000000..b3bef3c1c2 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-6315.php @@ -0,0 +1,23 @@ += 8.1 + +namespace Bug6315; + +enum FooEnum +{ + case A; + case B; +} + +/** + * @param array $flatArr + * @param array> $deepArr + * @return void + */ +function foo(array $flatArr, array $deepArr): void +{ + var_dump($flatArr[FooEnum::A]); + var_dump($deepArr[FooEnum::A][5]); + var_dump($deepArr[5][FooEnum::A]); + var_dump($deepArr[FooEnum::A][FooEnum::B]); + $deepArr[FooEnum::A][] = 5; +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-6605.php b/tests/PHPStan/Rules/Arrays/data/bug-6605.php new file mode 100644 index 0000000000..03df34d565 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-6605.php @@ -0,0 +1,19 @@ + 'bar' + ]; + + $arr = ['a' => ['b' => [5]]]; + var_dump($arr['invalid']['c']); + var_dump($arr['a']['invalid']); + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-7469.php b/tests/PHPStan/Rules/Arrays/data/bug-7469.php index 54b5855c51..d5aa696748 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-7469.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-7469.php @@ -32,14 +32,14 @@ function doFoo() { array_walk($data['languages'], static function (&$item) { $item = strtolower(trim($item)); }); - assertType("non-empty-array<'address'|'bankAccount'|'birthDate'|'email'|'firstName'|'ic'|'invoicing'|'invoicingAddress'|'languages'|'lastName'|'note'|'phone'|'radio'|'videoOnline'|'videoTvc'|'voiceExample', mixed>&hasOffsetValue('languages', non-empty-array)", $data); + assertType("non-empty-array<'address'|'bankAccount'|'birthDate'|'email'|'firstName'|'ic'|'invoicing'|'invoicingAddress'|'languages'|'lastName'|'note'|'phone'|'radio'|'videoOnline'|'videoTvc'|'voiceExample', mixed>&hasOffsetValue('languages', non-empty-list)", $data); $data['videoOnline'] = normalizePrice($data['videoOnline']); $data['videoTvc'] = normalizePrice($data['videoTvc']); $data['radio'] = normalizePrice($data['radio']); $data['invoicing'] = $data['invoicing'] === 'ANO'; - assertType("non-empty-array<'address'|'bankAccount'|'birthDate'|'email'|'firstName'|'ic'|'invoicing'|'invoicingAddress'|'languages'|'lastName'|'note'|'phone'|'radio'|'videoOnline'|'videoTvc'|'voiceExample', mixed>&hasOffsetValue('invoicing', bool)&hasOffsetValue('languages', non-empty-array)&hasOffsetValue('radio', mixed)&hasOffsetValue('videoOnline', mixed)&hasOffsetValue('videoTvc', mixed)", $data); + assertType("non-empty-array<'address'|'bankAccount'|'birthDate'|'email'|'firstName'|'ic'|'invoicing'|'invoicingAddress'|'languages'|'lastName'|'note'|'phone'|'radio'|'videoOnline'|'videoTvc'|'voiceExample', mixed>&hasOffsetValue('invoicing', bool)&hasOffsetValue('languages', non-empty-list)&hasOffsetValue('radio', mixed)&hasOffsetValue('videoOnline', mixed)&hasOffsetValue('videoTvc', mixed)", $data); } function normalizePrice($value) diff --git a/tests/PHPStan/Rules/Arrays/data/bug-7913.php b/tests/PHPStan/Rules/Arrays/data/bug-7913.php new file mode 100644 index 0000000000..1bd3465b0b --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-7913.php @@ -0,0 +1,17 @@ + $arr + * + * @return array + */ +function strings(array $arr): array +{ + return $arr; +} + +function (): void { + $x = ['a' => 1]; + + $y = strings($x); + + var_dump($x['b']); + var_dump($y['b']); +}; diff --git a/tests/PHPStan/Rules/Arrays/data/bug-8292.php b/tests/PHPStan/Rules/Arrays/data/bug-8292.php new file mode 100644 index 0000000000..31ea30148a --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-8292.php @@ -0,0 +1,32 @@ += 7.4 + +namespace Bug8292; + +class World{ + public function addOnUnloadCallback(\Closure $c) : void{} +} + +interface Compressor{} + +class ChunkCache +{ + /** @var self[][] */ + private static array $instances = []; + + /** + * Fetches the ChunkCache instance for the given world. This lazily creates cache systems as needed. + */ + public static function getInstance(World $world, Compressor $compressor) : void{ + $worldId = spl_object_id($world); + $compressorId = spl_object_id($compressor); + if(!isset(self::$instances[$worldId])){ + self::$instances[$worldId] = []; + $world->addOnUnloadCallback(static function() use ($worldId) : void{ + foreach(self::$instances[$worldId] as $cache){ + $cache->caches = []; + } + unset(self::$instances[$worldId]); + }); + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-8356.php b/tests/PHPStan/Rules/Arrays/data/bug-8356.php new file mode 100644 index 0000000000..192a71d363 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-8356.php @@ -0,0 +1,9 @@ +, psr-4?: array, classmap?: list, files?: list, exclude-from-classmap?: list} + */ +interface CompletePackageInterface { + /** + * Returns an associative array of autoloading rules + * + * {"": {""}} + * + * Type is either "psr-4", "psr-0", "classmap" or "files". Namespaces are mapped to + * directories for autoloading using the type specified. + * + * @return array Mapping of autoloading rules + * @phpstan-return AutoloadRules + */ + public function getAutoload(): array; +} + +class Test { + public function foo (CompletePackageInterface $package): void { + if (\count($package->getAutoload()) > 0) { + $autoloadConfig = $package->getAutoload(); + foreach ($autoloadConfig as $type => $autoloads) { + assertType('array|string, array|string>', $autoloadConfig[$type]); + if ($type === 'psr-0' || $type === 'psr-4') { + + } elseif ($type === 'classmap') { + assertType('list', $autoloadConfig[$type]); + implode(', ', $autoloadConfig[$type]); + } + } + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-9991.php b/tests/PHPStan/Rules/Arrays/data/bug-9991.php new file mode 100644 index 0000000000..c080a1d730 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-9991.php @@ -0,0 +1,14 @@ += 8.0 + +namespace ForeachMixed; + +/** + * @template T + * @param T $t + */ +function foo(mixed $t, mixed $explicit, $implicit): void +{ + foreach ($t as $v) { + } + + foreach ($explicit as $v) { + } + + foreach ($implicit as $v) { + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/invalid-key-array-dim-fetch.php b/tests/PHPStan/Rules/Arrays/data/invalid-key-array-dim-fetch.php index 6824050420..fb7514c6cb 100644 --- a/tests/PHPStan/Rules/Arrays/data/invalid-key-array-dim-fetch.php +++ b/tests/PHPStan/Rules/Arrays/data/invalid-key-array-dim-fetch.php @@ -35,3 +35,14 @@ foreach ($array as $i => $val) { echo $array[$i]; } + +/** @var mixed $mixed */ +$mixed = null; +$a[$mixed]; + +/** @var array> $array */ +$array = doFoo(); +$array[new \DateTimeImmutable()][5]; +$array[5][new \DateTimeImmutable()]; +$array[new \stdClass()][new \DateTimeImmutable()]; +$array[new \DateTimeImmutable()][] = 5; diff --git a/tests/PHPStan/Rules/Arrays/data/invalid-key-array-item-enum.php b/tests/PHPStan/Rules/Arrays/data/invalid-key-array-item-enum.php new file mode 100644 index 0000000000..f7d2790525 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/invalid-key-array-item-enum.php @@ -0,0 +1,16 @@ += 8.1 + +namespace InvalidKeyArrayItemEnum; + +enum FooEnum +{ + case A; + case B; +} + +function doFoo(): void +{ + $a = [ + FooEnum::A => 5, + ]; +} diff --git a/tests/PHPStan/Rules/Arrays/data/nonexistent-offset-intersection.php b/tests/PHPStan/Rules/Arrays/data/nonexistent-offset-intersection.php index 999252ea9c..749bd92d88 100644 --- a/tests/PHPStan/Rules/Arrays/data/nonexistent-offset-intersection.php +++ b/tests/PHPStan/Rules/Arrays/data/nonexistent-offset-intersection.php @@ -1,5 +1,7 @@ */ diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-mixed.php b/tests/PHPStan/Rules/Arrays/data/offset-access-mixed.php new file mode 100644 index 0000000000..9f3300ce65 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-mixed.php @@ -0,0 +1,22 @@ += 8.0 + +namespace OffsetAccessMixed; + +/** + * @template T + * @param T $a + */ +function foo(mixed $a): void +{ + var_dump($a[5]); +} + +function foo2(mixed $a): void +{ + var_dump($a[5]); +} + +function foo3($a): void +{ + var_dump($a[5]); +} diff --git a/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php b/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php new file mode 100644 index 0000000000..fc54d96f00 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php @@ -0,0 +1,60 @@ + 1]; + echo $a[$s]; + } + + /** + * @param array{bool|float|int|string|null} $a + * @return void + */ + public function testConstantArray(array $a): void + { + echo $a[0]; + } + + /** + * @param array $a + * @return void + */ + public function testConstantArray2(array $a): void + { + if (isset($a[0])) { + echo $a[0]; + } + } + + /** + * @param array{0: '9', A: 'Z', a: 'z'} $a + * @param '0'|'A'|'a' $dim + */ + public function testDimUnion(array $a, string $dim): void + { + echo $a[$dim]; + } + + /** + * @param non-empty-list $a + */ + public function nonEmpty(array $a): void + { + echo $a[0]; + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/unpack-mixed.php b/tests/PHPStan/Rules/Arrays/data/unpack-mixed.php new file mode 100644 index 0000000000..0270fbc3eb --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/unpack-mixed.php @@ -0,0 +1,14 @@ += 8.0 + +namespace UnpackMixed; + +/** + * @template T + * @param T $t + */ +function foo(mixed $t, mixed $explicit, $implicit): void +{ + var_dump([...$t]); + var_dump([...$explicit]); + var_dump([...$implicit]); +} diff --git a/tests/PHPStan/Rules/Cast/EchoRuleTest.php b/tests/PHPStan/Rules/Cast/EchoRuleTest.php index ba2b5b3e56..c536b8f130 100644 --- a/tests/PHPStan/Rules/Cast/EchoRuleTest.php +++ b/tests/PHPStan/Rules/Cast/EchoRuleTest.php @@ -16,7 +16,7 @@ class EchoRuleTest extends RuleTestCase protected function getRule(): Rule { return new EchoRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false), + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false), ); } diff --git a/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php b/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php index e03aacfc2f..5734b47928 100644 --- a/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php +++ b/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use function array_merge; +use function usort; use const PHP_VERSION_ID; /** @@ -13,10 +15,14 @@ class InvalidCastRuleTest extends RuleTestCase { + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new InvalidCastRule($broker, new RuleLevelHelper($broker, true, false, true, false, false)); + return new InvalidCastRule($broker, new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false)); } public function testRule(): void @@ -34,6 +40,10 @@ public function testRule(): void 'Cannot cast stdClass to float.', 24, ], + [ + 'Cannot cast object to string.', + 35, + ], [ 'Cannot cast Test\\Foo to string.', 41, @@ -64,4 +74,98 @@ public function testRuleWithNullsafeVariant(): void ]); } + public function testCastObjectToString(): void + { + $this->analyse([__DIR__ . '/data/cast-object-to-string.php'], [ + [ + 'Cannot cast object to string.', + 12, + ], + [ + 'Cannot cast object|string to string.', + 13, + ], + ]); + } + + public function dataMixed(): array + { + $explicitOnlyErrors = [ + [ + 'Cannot cast T to int.', + 11, + ], + [ + 'Cannot cast T to float.', + 13, + ], + [ + 'Cannot cast T to string.', + 14, + ], + [ + 'Cannot cast mixed to int.', + 18, + ], + [ + 'Cannot cast mixed to float.', + 20, + ], + [ + 'Cannot cast mixed to string.', + 21, + ], + ]; + $implicitOnlyErrors = [ + [ + 'Cannot cast mixed to int.', + 25, + ], + [ + 'Cannot cast mixed to float.', + 27, + ], + [ + 'Cannot cast mixed to string.', + 28, + ], + ]; + $combinedErrors = array_merge($explicitOnlyErrors, $implicitOnlyErrors); + usort($combinedErrors, static fn (array $a, array $b): int => $a[1] <=> $b[1]); + + return [ + [ + true, + false, + $explicitOnlyErrors, + ], + [ + false, + true, + $implicitOnlyErrors, + ], + [ + true, + true, + $combinedErrors, + ], + [ + false, + false, + [], + ], + ]; + } + + /** + * @dataProvider dataMixed + * @param list $errors + */ + public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void + { + $this->checkImplicitMixed = $checkImplicitMixed; + $this->checkExplicitMixed = $checkExplicitMixed; + $this->analyse([__DIR__ . '/data/mixed-cast.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php b/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php index 83474b940b..3ba7d5ce61 100644 --- a/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php +++ b/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php @@ -19,7 +19,7 @@ protected function getRule(): Rule { return new InvalidPartOfEncapsedStringRule( new ExprPrinter(new Printer()), - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false), + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false), ); } diff --git a/tests/PHPStan/Rules/Cast/PrintRuleTest.php b/tests/PHPStan/Rules/Cast/PrintRuleTest.php index 85da6bfcd4..c7a52e8123 100644 --- a/tests/PHPStan/Rules/Cast/PrintRuleTest.php +++ b/tests/PHPStan/Rules/Cast/PrintRuleTest.php @@ -16,7 +16,7 @@ class PrintRuleTest extends RuleTestCase protected function getRule(): Rule { return new PrintRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false), + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false), ); } diff --git a/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php b/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php index 923423b1e5..ebab5c0aa6 100644 --- a/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php +++ b/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php @@ -40,7 +40,7 @@ public function dataRule(): array /** * @dataProvider dataRule - * @param mixed[] $errors + * @param list $errors */ public function testRule(int $phpVersion, array $errors): void { diff --git a/tests/PHPStan/Rules/Cast/data/cast-object-to-string.php b/tests/PHPStan/Rules/Cast/data/cast-object-to-string.php new file mode 100644 index 0000000000..a4a1c74a2d --- /dev/null +++ b/tests/PHPStan/Rules/Cast/data/cast-object-to-string.php @@ -0,0 +1,22 @@ += 8.0 + +namespace MixedCast; + +/** + * @template T + * @param T $t + */ +function foo(mixed $t, mixed $explicit, $implicit): void +{ + var_dump((int) $t); + var_dump((bool) $t); + var_dump((float) $t); + var_dump((string) $t); + var_dump((array) $t); + var_dump((object) $t); + + var_dump((int) $explicit); + var_dump((bool) $explicit); + var_dump((float) $explicit); + var_dump((string) $explicit); + var_dump((array) $explicit); + var_dump((object) $explicit); + + var_dump((int) $implicit); + var_dump((bool) $implicit); + var_dump((float) $implicit); + var_dump((string) $implicit); + var_dump((array) $implicit); + var_dump((object) $implicit); +} diff --git a/tests/PHPStan/Rules/Classes/AllowedSubTypesRuleTest.php b/tests/PHPStan/Rules/Classes/AllowedSubTypesRuleTest.php new file mode 100644 index 0000000000..403a35e6ff --- /dev/null +++ b/tests/PHPStan/Rules/Classes/AllowedSubTypesRuleTest.php @@ -0,0 +1,37 @@ + + */ +class AllowedSubTypesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new AllowedSubTypesRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/allowed-sub-types.php'], [ + [ + 'Type AllowedSubTypes\\Baz is not allowed to be a subtype of AllowedSubTypes\\Foo.', + 11, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../../conf/bleedingEdge.neon', + __DIR__ . '/data/allowed-sub-types.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php index a4adab731e..6fa6252277 100644 --- a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -27,7 +29,7 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), @@ -38,7 +40,11 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, ), ); } diff --git a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php index bc36347b31..b132e3fe08 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -26,7 +28,7 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), @@ -37,7 +39,11 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, ), ); } diff --git a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php index 3d85bde3b5..42378de1f2 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -19,8 +21,16 @@ class ClassConstantRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ClassConstantRule($broker, new RuleLevelHelper($broker, true, false, true, false, false), new ClassCaseSensitivityCheck($broker, true), new PhpVersion($this->phpVersion)); + $reflectionProvider = $this->createReflectionProvider(); + return new ClassConstantRule( + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new PhpVersion($this->phpVersion), + ); } public function testClassConstant(): void @@ -227,7 +237,7 @@ public function dataClassConstantOnExpression(): array /** * @dataProvider dataClassConstantOnExpression - * @param mixed[] $errors + * @param list $errors */ public function testClassConstantOnExpression(int $phpVersion, array $errors): void { @@ -286,4 +296,128 @@ public function testBug7675(): void $this->analyse([__DIR__ . '/data/bug-7675.php'], []); } + public function testBug8034(): void + { + $this->phpVersion = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-8034.php'], [ + [ + 'Access to undefined constant static(Bug8034\HelloWorld)::FIELDS.', + 19, + ], + ]); + } + + public function testClassConstFetchDefined(): void + { + $this->phpVersion = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/class-const-fetch-defined.php'], [ + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 12, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 14, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 16, + ], + [ + 'Access to undefined constant Foo::TEST.', + 17, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 18, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 22, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 24, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 26, + ], + [ + 'Access to undefined constant Foo::TEST.', + 27, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 28, + ], + [ + 'Access to undefined constant Foo::TEST.', + 33, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 36, + ], + [ + 'Access to undefined constant Foo::TEST.', + 37, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 38, + ], + [ + 'Access to undefined constant Foo::TEST.', + 43, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 46, + ], + [ + 'Access to undefined constant Foo::TEST.', + 47, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 48, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 52, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 54, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 56, + ], + [ + 'Access to undefined constant Foo::TEST.', + 57, + ], + [ + 'Access to undefined constant ClassConstFetchDefined\Foo::TEST.', + 58, + ], + ]); + } + + public function testPhpstanInternalClass(): void + { + $tip = 'This is most likely unintentional. Did you mean to type \AClass?'; + + $this->phpVersion = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/phpstan-internal-class.php'], [ + [ + 'Referencing prefixed PHPStan class: _PHPStan_156ee64ba\AClass.', + 28, + $tip, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/DuplicateClassDeclarationRuleTest.php b/tests/PHPStan/Rules/Classes/DuplicateClassDeclarationRuleTest.php new file mode 100644 index 0000000000..e16ccce93c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/DuplicateClassDeclarationRuleTest.php @@ -0,0 +1,52 @@ + + */ +class DuplicateClassDeclarationRuleTest extends RuleTestCase +{ + + private const FILENAME = __DIR__ . '/data/duplicate-class.php'; + + protected function getRule(): Rule + { + $fileHelper = new FileHelper(__DIR__ . '/data'); + + return new DuplicateClassDeclarationRule( + new DefaultReflector(new OptimizedSingleFileSourceLocator( + self::getContainer()->getByType(FileNodesFetcher::class), + self::FILENAME, + )), + new SimpleRelativePathHelper($fileHelper->normalizePath($fileHelper->getWorkingDirectory(), '/')), + ); + } + + public function testRule(): void + { + $this->analyse([self::FILENAME], [ + [ + "Class DuplicateClassDeclaration\Foo declared multiple times:\n- duplicate-class.php:15\n- duplicate-class.php:20", + 10, + ], + [ + "Class DuplicateClassDeclaration\Foo declared multiple times:\n- duplicate-class.php:10\n- duplicate-class.php:20", + 15, + ], + [ + "Class DuplicateClassDeclaration\Foo declared multiple times:\n- duplicate-class.php:10\n- duplicate-class.php:15", + 20, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php index 1dc2605335..c1e85d214f 100644 --- a/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php +++ b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php @@ -14,13 +14,13 @@ class EnumSanityRuleTest extends RuleTestCase protected function getRule(): Rule { - return new EnumSanityRule($this->createReflectionProvider()); + return new EnumSanityRule(); } public function testRule(): void { - if (PHP_VERSION_ID < 80000) { - $this->markTestSkipped('Test requires PHP 8.0'); + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); } $expected = [ @@ -76,16 +76,51 @@ public function testRule(): void 'Enum EnumSanity\EnumWithSerialize contains magic method __unserialize().', 81, ], + [ + 'Enum EnumSanity\EnumDuplicateValue has duplicate value 1 for cases A, E.', + 86, + ], + [ + 'Enum EnumSanity\EnumDuplicateValue has duplicate value 2 for cases B, C.', + 86, + ], + [ + 'Enum case EnumSanity\EnumInconsistentCaseType::FOO value \'foo\' does not match the "int" type.', + 105, + ], + [ + 'Enum case EnumSanity\EnumInconsistentCaseType::BAR does not have a value but the enum is backed with the "int" type.', + 106, + ], + [ + 'Enum case EnumSanity\EnumInconsistentStringCaseType::BAR does not have a value but the enum is backed with the "string" type.', + 110, + ], + [ + 'Enum EnumSanity\EnumWithValueButNotBacked is not backed, but case FOO has value 1.', + 114, + ], + [ + 'Enum EnumSanity\EnumMayNotSerializable cannot implement the Serializable interface.', + 117, + ], ]; - if (PHP_VERSION_ID >= 80100) { - $expected[] = [ - 'Enum EnumSanity\EnumMayNotSerializable cannot implement the Serializable interface.', - 86, - ]; + $this->analyse([__DIR__ . '/data/enum-sanity.php'], $expected); + } + + public function testBug9402(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); } - $this->analyse([__DIR__ . '/data/enum-sanity.php'], $expected); + $this->analyse([__DIR__ . '/data/bug-9402.php'], [ + [ + 'Enum case Bug9402\Foo::Two value \'foo\' does not match the "int" type.', + 13, + ], + ]); } } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php index e3ba1adb14..62697c9c89 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -15,10 +17,13 @@ class ExistingClassInClassExtendsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassInClassExtendsRule( - new ClassCaseSensitivityCheck($broker, true), - $broker, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + $reflectionProvider, ); } @@ -91,4 +96,58 @@ public function testEnums(): void ]); } + public function testPhpstanInternalClass(): void + { + $tip = 'This is most likely unintentional. Did you mean to type \AClass?'; + + $this->analyse([__DIR__ . '/data/phpstan-internal-class.php'], [ + [ + 'Referencing prefixed PHPStan class: _PHPStan_156ee64ba\AClass.', + 34, + $tip, + ], + [ + 'Referencing prefixed Rector class: RectorPrefix202302\AClass.', + 56, + $tip, + ], + [ + 'Referencing prefixed PHP-Scoper class: _PhpScoper19ae93be897e\AClass.', + 59, + $tip, + ], + [ + 'Referencing prefixed PHPUnit class: PHPUnitPHAR\SebastianBergmann\Diff\Exception.', + 62, + 'This is most likely unintentional. Did you mean to type \SebastianBergmann\Diff\Exception?', + ], + ]); + } + + public function testReadonly(): void + { + if (PHP_VERSION_ID < 80200) { + $this->markTestSkipped('This test needs PHP 8.2'); + } + + $this->analyse([__DIR__ . '/data/extends-readonly-class.php'], [ + [ + 'Readonly class ExtendsReadOnlyClass\Foo extends non-readonly class ExtendsReadOnlyClass\Nonreadonly.', + 25, + ], + [ + 'Non-readonly class ExtendsReadOnlyClass\Bar extends readonly class ExtendsReadOnlyClass\ReadonlyClass.', + 30, + ], + [ + 'Anonymous non-readonly class extends readonly class ExtendsReadOnlyClass\ReadonlyClass.', + 35, + ], + [ + 'Anonymous readonly class extends non-readonly class ExtendsReadOnlyClass\Nonreadonly.', + 39, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php index 5533f01879..4018d2ca62 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -14,10 +16,13 @@ class ExistingClassInInstanceOfRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassInInstanceOfRule( - $broker, - new ClassCaseSensitivityCheck($broker, true), + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), true, ); } @@ -31,7 +36,7 @@ public function testClassDoesNotExist(): void ], [ [ - 'Class InstanceOfNamespace\Bar not found.', + 'Class InstanceOfNamespaceRule\Bar not found.', 7, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], @@ -40,7 +45,7 @@ public function testClassDoesNotExist(): void 9, ], [ - 'Class InstanceOfNamespace\Foo referenced with incorrect case: InstanceOfNamespace\FOO.', + 'Class InstanceOfNamespaceRule\Foo referenced with incorrect case: InstanceOfNamespaceRule\FOO.', 13, ], [ diff --git a/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php index 0e78d7b9fc..49b083cd46 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -15,10 +17,13 @@ class ExistingClassInTraitUseRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassInTraitUseRule( - new ClassCaseSensitivityCheck($broker, true), - $broker, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + $reflectionProvider, ); } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php index c217f0dadd..12183ebc85 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -15,10 +17,13 @@ class ExistingClassesInClassImplementsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassesInClassImplementsRule( - new ClassCaseSensitivityCheck($broker, true), - $broker, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + $reflectionProvider, ); } @@ -73,4 +78,20 @@ public function testEnums(): void ]); } + public function testBug8889(): void + { + $this->analyse([__DIR__ . '/data/bug-8889.php'], [ + [ + 'Class Bug8889\HelloWorld implements unknown interface iterable.', + 5, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Class Bug8889\HelloWorld2 implements unknown interface Iterable.', + 8, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php index c6cdcc20ad..f83d296867 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -18,7 +20,10 @@ protected function getRule(): Rule $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassesInEnumImplementsRule( - new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), $reflectionProvider, ); } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php index 4a3a30cf20..cadda3b992 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -15,10 +17,13 @@ class ExistingClassesInInterfaceExtendsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassesInInterfaceExtendsRule( - new ClassCaseSensitivityCheck($broker, true), - $broker, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + $reflectionProvider, ); } diff --git a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php new file mode 100644 index 0000000000..9d3ea64034 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php @@ -0,0 +1,60 @@ + + */ +class ForbiddenNameCheckExtensionRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new InstantiationRule( + $reflectionProvider, + new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + ); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge(parent::getAdditionalConfigFiles(), [ + __DIR__ . '/data/forbidden-name-class-extension.neon', + ]); + } + + public function testInternalClassFromExtensions(): void + { + $this->analyse([__DIR__ . '/data/forbidden-name-class-extension.php'], [ + [ + 'Referencing prefixed Doctrine class: App\GeneratedProxy\__CG__\App\TestDoctrineEntity.', + 31, + 'This is most likely unintentional. Did you mean to type \App\TestDoctrineEntity?', + ], + [ + 'Referencing prefixed PHPStan class: _PHPStan_15755dag8c\TestPhpStanEntity.', + 32, + 'This is most likely unintentional. Did you mean to type \TestPhpStanEntity?', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php index 6060d4d30d..271ab2dd83 100644 --- a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -15,9 +16,11 @@ class ImpossibleInstanceOfRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain; + private bool $reportAlwaysTrueInLastCondition = false; + protected function getRule(): Rule { - return new ImpossibleInstanceOfRule($this->checkAlwaysTrueInstanceOf, $this->treatPhpDocTypesAsCertain); + return new ImpossibleInstanceOfRule($this->checkAlwaysTrueInstanceOf, $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -146,19 +149,24 @@ public function testInstanceof(): void 'Instanceof between ImpossibleInstanceOf\Bar and ImpossibleInstanceOf\BarGrandChild will always evaluate to false.', 322, ], - [ + /*[ 'Instanceof between mixed and int results in an error.', 353, ], [ 'Instanceof between mixed and ImpossibleInstanceOf\InvalidTypeTest|int results in an error.', 362, - ], + ],*/ [ 'Instanceof between ImpossibleInstanceOf\Foo and ImpossibleInstanceOf\Foo will always evaluate to true.', 388, $tipText, ], + [ + 'Instanceof between T of Exception and Error will always evaluate to false.', + 404, + $tipText, + ], [ 'Instanceof between class-string and DateTimeInterface will always evaluate to false.', 418, @@ -167,6 +175,7 @@ public function testInstanceof(): void [ 'Instanceof between class-string and class-string will always evaluate to false.', 419, + $tipText, ], [ 'Instanceof between class-string and \'DateTimeInterface\' will always evaluate to false.', @@ -248,13 +257,18 @@ public function testInstanceofWithoutAlwaysTrue(): void 'Instanceof between ImpossibleInstanceOf\Bar and ImpossibleInstanceOf\BarGrandChild will always evaluate to false.', 322, ], - [ + /*[ 'Instanceof between mixed and int results in an error.', 353, ], [ 'Instanceof between mixed and ImpossibleInstanceOf\InvalidTypeTest|int results in an error.', 362, + ],*/ + [ + 'Instanceof between T of Exception and Error will always evaluate to false.', + 404, + $tipText, ], [ 'Instanceof between class-string and DateTimeInterface will always evaluate to false.', @@ -264,6 +278,7 @@ public function testInstanceofWithoutAlwaysTrue(): void [ 'Instanceof between class-string and class-string will always evaluate to false.', 419, + $tipText, ], [ 'Instanceof between class-string and \'DateTimeInterface\' will always evaluate to false.', @@ -288,11 +303,11 @@ public function testDoNotReportTypesFromPhpDocs(): void 15, ], [ - 'Instanceof between DateTimeImmutable and DateTimeInterface will always evaluate to true.', + 'Instanceof between DateTimeInterface and DateTimeInterface will always evaluate to true.', 27, ], [ - 'Instanceof between DateTimeImmutable and ImpossibleInstanceofNotPhpDoc\SomeFinalClass will always evaluate to false.', + 'Instanceof between DateTimeInterface and ImpossibleInstanceofNotPhpDoc\SomeFinalClass will always evaluate to false.', 30, ], ]); @@ -346,4 +361,266 @@ public function testBug6213(): void $this->analyse([__DIR__ . '/data/bug-6213.php'], []); } + public function testBug5333(): void + { + $this->checkAlwaysTrueInstanceOf = true; + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-5333.php'], []); + } + + public function testBug8042(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('This test needs PHP 8.0'); + } + + $this->checkAlwaysTrueInstanceOf = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8042.php'], [ + [ + 'Instanceof between Bug8042\B and Bug8042\B will always evaluate to true.', + 18, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Instanceof between Bug8042\B and Bug8042\B will always evaluate to true.', + 26, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testBug7721(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->checkAlwaysTrueInstanceOf = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7721.php'], []); + } + + public function testUnreachableIfBranches(): void + { + $this->checkAlwaysTrueInstanceOf = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../Comparison/data/unreachable-if-branches.php'], [ + [ + 'Instanceof between stdClass and stdClass will always evaluate to true.', + 5, + ], + [ + 'Instanceof between stdClass and stdClass will always evaluate to true.', + 13, + ], + [ + 'Instanceof between stdClass and stdClass will always evaluate to true.', + 23, + ], + [ + 'Instanceof between stdClass and stdClass will always evaluate to true.', + 37, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testIfBranchesDoNotReportPhpDoc(): void + { + $this->checkAlwaysTrueInstanceOf = true; + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/../Comparison/data/unreachable-if-branches-not-phpdoc.php'], [ + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 16, + ], + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 26, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 36, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testIfBranchesReportPhpDoc(): void + { + $this->checkAlwaysTrueInstanceOf = true; + $this->treatPhpDocTypesAsCertain = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/../Comparison/data/unreachable-if-branches-not-phpdoc.php'], [ + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 16, + ], + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 26, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 36, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 42, + $tipText, + ], + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 52, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 62, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testUnreachableTernaryElse(): void + { + $this->checkAlwaysTrueInstanceOf = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../Comparison/data/unreachable-ternary-else-branch.php'], [ + [ + 'Instanceof between stdClass and stdClass will always evaluate to true.', + 6, + ], + [ + 'Instanceof between stdClass and stdClass will always evaluate to true.', + 9, + ], + ]); + } + + public function testTernaryElseDoNotReportPhpDoc(): void + { + $this->checkAlwaysTrueInstanceOf = true; + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/../Comparison/data/unreachable-ternary-else-branch-not-phpdoc.php'], [ + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 16, + ], + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 17, + ], + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 20, + ], + ]); + } + + public function testTernaryElseReportPhpDoc(): void + { + $this->checkAlwaysTrueInstanceOf = true; + $this->treatPhpDocTypesAsCertain = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/../Comparison/data/unreachable-ternary-else-branch-not-phpdoc.php'], [ + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 16, + ], + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 17, + ], + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 19, + $tipText, + ], + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 20, + ], + ]); + } + + public function testBug4689(): void + { + $this->checkAlwaysTrueInstanceOf = true; + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-4689.php'], []); + } + + public function dataReportAlwaysTrueInLastCondition(): iterable + { + yield [false, [ + [ + 'Instanceof between Exception and Exception will always evaluate to true.', + 21, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]]; + yield [true, [ + [ + 'Instanceof between Exception and Exception will always evaluate to true.', + 12, + ], + [ + 'Instanceof between Exception and Exception will always evaluate to true.', + 21, + ], + [ + 'Instanceof between DateTime and DateTime will always evaluate to true.', + 34, + ], + ]]; + } + + /** + * @dataProvider dataReportAlwaysTrueInLastCondition + * @param list $expectedErrors + */ + public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + $this->checkAlwaysTrueInstanceOf = true; + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/impossible-instanceof-report-always-true-last-condition.php'], $expectedErrors); + } + + public function testBug10201(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->checkAlwaysTrueInstanceOf = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/data/bug-10201.php'], [ + [ + 'Instanceof between string and Bug10201\Hello will always evaluate to false.', + 13, + ], + ]); + } + + public function testBug3632(): void + { + $this->checkAlwaysTrueInstanceOf = true; + $this->treatPhpDocTypesAsCertain = true; + + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/bug-3632.php'], [ + [ + 'Instanceof between Bug3632\NiceClass and Bug3632\NiceClass will always evaluate to true.', + 36, + $tipText, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index bb0b72a1d9..b35d04266c 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -21,11 +23,14 @@ class InstantiationRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new InstantiationRule( - $broker, - new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, false, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true), - new ClassCaseSensitivityCheck($broker, true), + $reflectionProvider, + new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), ); } @@ -271,6 +276,10 @@ public function testPromotedProperties(): void 'Parameter #2 $bar of class InstantiationPromotedProperties\Bar constructor expects array, array given.', 33, ], + [ + 'Parameter #1 $intProp of class InstantiationPromotedProperties\PromotedPropertyNotNullable constructor expects int, null given.', + 46, + ], ]); } @@ -431,4 +440,57 @@ public function testBug7594(): void $this->analyse([__DIR__ . '/data/bug-7594.php'], []); } + public function testBug3311a(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->analyse([__DIR__ . '/data/bug-3311a.php'], [ + [ + 'Parameter #1 $bar of class Bug3311a\Foo constructor expects list, array{1: \'baz\'} given.', + 24, + "array{1: 'baz'} is not a list.", + ], + ]); + } + + public function testBug9341(): void + { + $this->analyse([__DIR__ . '/../../Analyser/data/bug-9341.php'], []); + } + + public function testBug7574(): void + { + $this->analyse([__DIR__ . '/data/bug-7574.php'], []); + } + + public function testBug9946(): void + { + $this->analyse([__DIR__ . '/data/bug-9946.php'], []); + } + + public function testBug10324(): void + { + $this->analyse([__DIR__ . '/data/bug-10324.php'], [ + [ + 'Parameter #3 $flags of class RecursiveIteratorIterator constructor expects 0|16, 2 given.', + 23, + ], + ]); + } + + public function testPhpstanInternalClass(): void + { + $tip = 'This is most likely unintentional. Did you mean to type \AClass?'; + + $this->analyse([__DIR__ . '/data/phpstan-internal-class.php'], [ + [ + 'Referencing prefixed PHPStan class: _PHPStan_156ee64ba\AClass.', + 30, + $tip, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php b/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php index a5312c0e51..de80ea8f95 100644 --- a/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -93,4 +94,14 @@ public function testSupportedOnPhp8(): void ]); } + public function testBug9577(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->phpVersion = 80100; + $this->analyse([__DIR__ . '/data/bug-9577.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php index 281e125507..e55f429317 100644 --- a/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php @@ -16,9 +16,11 @@ class LocalTypeAliasesRuleTest extends RuleTestCase protected function getRule(): Rule { return new LocalTypeAliasesRule( - ['GlobalTypeAlias' => 'int|string'], - $this->createReflectionProvider(), - self::getContainer()->getByType(TypeNodeResolver::class), + new LocalTypeAliasesCheck( + ['GlobalTypeAlias' => 'int|string'], + $this->createReflectionProvider(), + self::getContainer()->getByType(TypeNodeResolver::class), + ), ); } diff --git a/tests/PHPStan/Rules/Classes/LocalTypeTraitAliasesRuleTest.php b/tests/PHPStan/Rules/Classes/LocalTypeTraitAliasesRuleTest.php new file mode 100644 index 0000000000..49d73e9c5c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/LocalTypeTraitAliasesRuleTest.php @@ -0,0 +1,97 @@ + + */ +class LocalTypeTraitAliasesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new LocalTypeTraitAliasesRule( + new LocalTypeAliasesCheck( + ['GlobalTypeAlias' => 'int|string'], + $this->createReflectionProvider(), + self::getContainer()->getByType(TypeNodeResolver::class), + ), + $this->createReflectionProvider(), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/local-type-trait-aliases.php'], [ + [ + 'Type alias ExistingClassAlias already exists as a class in scope of LocalTypeTraitAliases\Bar.', + 23, + ], + [ + 'Type alias GlobalTypeAlias already exists as a global type alias.', + 23, + ], + [ + 'Type alias has an invalid name: int.', + 23, + ], + [ + 'Circular definition detected in type alias RecursiveTypeAlias.', + 23, + ], + [ + 'Circular definition detected in type alias CircularTypeAlias1.', + 23, + ], + [ + 'Circular definition detected in type alias CircularTypeAlias2.', + 23, + ], + [ + 'Cannot import type alias ImportedAliasFromNonClass: class LocalTypeTraitAliases\int does not exist.', + 39, + ], + [ + 'Cannot import type alias ImportedAliasFromUnknownClass: class LocalTypeTraitAliases\UnknownClass does not exist.', + 39, + ], + [ + 'Cannot import type alias ImportedUnknownAlias: type alias does not exist in LocalTypeTraitAliases\Foo.', + 39, + ], + [ + 'Type alias ExistingClassAlias already exists as a class in scope of LocalTypeTraitAliases\Baz.', + 39, + ], + [ + 'Type alias GlobalTypeAlias already exists as a global type alias.', + 39, + ], + [ + 'Imported type alias ExportedTypeAlias has an invalid name: int.', + 39, + ], + [ + 'Type alias OverwrittenTypeAlias overwrites an imported type alias of the same name.', + 39, + ], + [ + 'Circular definition detected in type alias CircularTypeAliasImport2.', + 39, + ], + [ + 'Circular definition detected in type alias CircularTypeAliasImport1.', + 47, + ], + [ + 'Invalid type definition detected in type alias InvalidTypeAlias.', + 62, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/MixinRuleTest.php b/tests/PHPStan/Rules/Classes/MixinRuleTest.php index 7c191c8fb5..5d39efdd94 100644 --- a/tests/PHPStan/Rules/Classes/MixinRuleTest.php +++ b/tests/PHPStan/Rules/Classes/MixinRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -22,9 +24,12 @@ protected function getRule(): Rule return new MixinRule( $reflectionProvider, - new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), new GenericObjectTypeCheck(), - new MissingTypehintCheck($reflectionProvider, true, true, true, true, []), + new MissingTypehintCheck(true, true, true, true, []), new UnresolvableTypeHelper(), true, ); @@ -84,6 +89,15 @@ public function testRule(): void 'PHPDoc tag @mixin contains non-object type int.', 92, ], + [ + 'Call-site variance of contravariant MixinRule\Foo in generic type MixinRule\Adipiscing in PHPDoc tag @mixin is in conflict with covariant template type T of class MixinRule\Adipiscing.', + 108, + ], + [ + 'Call-site variance of covariant MixinRule\Foo in generic type MixinRule\Adipiscing in PHPDoc tag @mixin is redundant, template type T of class MixinRule\Adipiscing has the same variance.', + 116, + 'You can safely remove the call-site variance annotation.', + ], ]); } diff --git a/tests/PHPStan/Rules/Classes/ReadOnlyClassRuleTest.php b/tests/PHPStan/Rules/Classes/ReadOnlyClassRuleTest.php new file mode 100644 index 0000000000..df5ede79f0 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/ReadOnlyClassRuleTest.php @@ -0,0 +1,39 @@ + + */ +class ReadOnlyClassRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new ReadOnlyClassRule(self::getContainer()->getByType(PhpVersion::class)); + } + + public function testRule(): void + { + $errors = []; + if (PHP_VERSION_ID < 80200) { + $errors[] = [ + 'Readonly classes are supported only on PHP 8.2 and later.', + 5, + ]; + } + if (PHP_VERSION_ID < 80300) { + $errors[] = [ + 'Anonymous readonly classes are supported only on PHP 8.3 and later.', + 15, + ]; + } + $this->analyse([__DIR__ . '/data/readonly-class.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Classes/RequireExtendsRuleTest.php b/tests/PHPStan/Rules/Classes/RequireExtendsRuleTest.php new file mode 100644 index 0000000000..0ca40d9fa4 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/RequireExtendsRuleTest.php @@ -0,0 +1,80 @@ + + */ +class RequireExtendsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RequireExtendsRule(); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); + } + + $expectedErrors = [ + [ + 'Trait IncompatibleRequireExtends\ValidTrait requires using class to extend IncompatibleRequireExtends\SomeClass, but IncompatibleRequireExtends\InValidTraitUse2 does not.', + 46, + ], + [ + 'Trait IncompatibleRequireExtends\ValidTrait requires using class to extend IncompatibleRequireExtends\SomeClass, but IncompatibleRequireExtends\InValidTraitUse does not.', + 51, + ], + [ + 'Interface IncompatibleRequireExtends\ValidInterface requires implementing class to extend IncompatibleRequireExtends\SomeClass, but IncompatibleRequireExtends\InvalidInterfaceUse2 does not.', + 56, + ], + [ + 'Interface IncompatibleRequireExtends\ValidInterface requires implementing class to extend IncompatibleRequireExtends\SomeClass, but IncompatibleRequireExtends\InvalidInterfaceUse does not.', + 58, + ], + [ + 'Trait IncompatibleRequireExtends\InvalidTrait requires using class to extend IncompatibleRequireExtends\SomeFinalClass, but IncompatibleRequireExtends\InvalidClass2 does not.', + 128, + ], + [ + 'Trait IncompatibleRequireExtends\ValidTrait requires using class to extend IncompatibleRequireExtends\SomeClass, but class@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-extends.php:146 does not.', + 146, + ], + [ + 'Trait IncompatibleRequireExtends\ValidPsalmTrait requires using class to extend IncompatibleRequireExtends\SomeClass, but class@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-extends.php:163 does not.', + 163, + ], + ]; + + $this->analyse([__DIR__ . '/../PhpDoc/data/incompatible-require-extends.php'], $expectedErrors); + } + + public function testExtendedInterfaceBug(): void + { + $this->analyse([__DIR__ . '/data/bug-10302-extended-interface.php'], [ + [ + 'Interface Bug10302ExtendedInterface\BatchAware requires implementing class to extend Bug10302ExtendedInterface\Model, but Bug10302ExtendedInterface\AnotherModel does not.', + 34, + ], + ]); + } + + public function testExtendedTraitBug(): void + { + $this->analyse([__DIR__ . '/data/bug-10302-extended-trait.php'], [ + [ + 'Trait Bug10302ExtendedTrait\Foo requires using class to extend Bug10302ExtendedTrait\Father, but Bug10302ExtendedTrait\Baz does not.', + 21, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/RequireImplementsRuleTest.php b/tests/PHPStan/Rules/Classes/RequireImplementsRuleTest.php new file mode 100644 index 0000000000..a2fe9cf7a3 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/RequireImplementsRuleTest.php @@ -0,0 +1,86 @@ + + */ +class RequireImplementsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RequireImplementsRule(); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); + } + + $expectedErrors = [ + [ + 'Trait IncompatibleRequireImplements\ValidTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface, but IncompatibleRequireImplements\InValidTraitUse2 does not.', + 47, + ], + [ + 'Trait IncompatibleRequireImplements\ValidTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface, but IncompatibleRequireImplements\InvalidEnumTraitUse does not.', + 52, + ], + [ + 'Trait IncompatibleRequireImplements\ValidTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface, but IncompatibleRequireImplements\InValidTraitUse does not.', + 56, + ], + [ + 'Trait IncompatibleRequireImplements\ValidTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface, but class@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-implements.php:117 does not.', + 117, + ], + [ + 'Trait IncompatibleRequireImplements\InvalidTrait1 requires using class to implement IncompatibleRequireImplements\SomeTrait, but IncompatibleRequireImplements\InvalidTraitUse1 does not.', + 125, + ], + [ + 'Trait IncompatibleRequireImplements\InvalidTrait2 requires using class to implement IncompatibleRequireImplements\SomeEnum, but IncompatibleRequireImplements\InvalidTraitUse2 does not.', + 129, + ], + [ + 'Trait IncompatibleRequireImplements\InvalidTrait3 requires using class to implement IncompatibleRequireImplements\TypeDoesNotExist, but IncompatibleRequireImplements\InvalidTraitUse3 does not.', + 133, + ], + [ + 'Trait IncompatibleRequireImplements\InvalidTrait4 requires using class to implement IncompatibleRequireImplements\SomeClass, but IncompatibleRequireImplements\InvalidTraitUse4 does not.', + 137, + ], + [ + 'Trait IncompatibleRequireImplements\ValidPsalmTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface2, but class@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-implements.php:164 does not.', + 164, + ], + [ + 'Trait IncompatibleRequireImplements\ValidPsalmTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface, but class@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-implements.php:168 does not.', + 168, + ], + [ + 'Trait IncompatibleRequireImplements\ValidPsalmTrait requires using class to implement IncompatibleRequireImplements\RequiredInterface2, but class@anonymous/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-implements.php:168 does not.', + 168, + ], + ]; + + $this->analyse([__DIR__ . '/../PhpDoc/data/incompatible-require-implements.php'], $expectedErrors); + } + + public function testExtendedTraitBug(): void + { + $this->analyse([__DIR__ . '/data/bug-10302-extended-implements-trait.php'], [ + [ + 'Trait Bug10302ExtendedImplementsTrait\Foo requires using class to implement Bug10302ExtendedImplementsTrait\Interface1, but Bug10302ExtendedImplementsTrait\Baz does not.', + 21, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php index 7aa4f4e62f..b6530920df 100644 --- a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php +++ b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php @@ -43,4 +43,9 @@ public function testBug1917(): void $this->analyse([__DIR__ . '/data/bug-1917.php'], []); } + public function testBug10865(): void + { + $this->analyse([__DIR__ . '/data/bug-10865.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/allowed-sub-types.neon b/tests/PHPStan/Rules/Classes/data/allowed-sub-types.neon new file mode 100644 index 0000000000..1366adc94a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/allowed-sub-types.neon @@ -0,0 +1,3 @@ +services: + - factory: AllowedSubTypes\Extension + tags: [phpstan.broker.allowedSubTypesClassReflectionExtension] diff --git a/tests/PHPStan/Rules/Classes/data/allowed-sub-types.php b/tests/PHPStan/Rules/Classes/data/allowed-sub-types.php new file mode 100644 index 0000000000..986e68ba7a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/allowed-sub-types.php @@ -0,0 +1,27 @@ +getName() === 'AllowedSubTypes\\Foo'; + } + + public function getAllowedSubTypes(ClassReflection $classReflection): array + { + return [ + new ObjectType('AllowedSubTypes\\Bar'), + ]; + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-10302-extended-implements-trait.php b/tests/PHPStan/Rules/Classes/data/bug-10302-extended-implements-trait.php new file mode 100644 index 0000000000..290d54974a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-10302-extended-implements-trait.php @@ -0,0 +1,33 @@ + $args */ + public function __construct(array $args) { + + var_dump($args); + } +} + +class Test extends TestParent { + + public function __construct(int $a) { + + parent::__construct(get_defined_vars()); + //parent::__construct(func_get_args()); + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-3311a.php b/tests/PHPStan/Rules/Classes/data/bug-3311a.php new file mode 100644 index 0000000000..d4f646deb5 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-3311a.php @@ -0,0 +1,25 @@ += 7.4 + +namespace Bug3311a; + +final class Foo +{ + /** + * @var array + * @psalm-var list + */ + public array $bar = []; + + /** + * @param array $bar + * @psalm-param list $bar + */ + public function __construct(array $bar) + { + $this->bar = $bar; + } +} + +function () { + $instance = new Foo([1 => 'baz']); +}; diff --git a/tests/PHPStan/Rules/Classes/data/bug-3632.php b/tests/PHPStan/Rules/Classes/data/bug-3632.php new file mode 100644 index 0000000000..d21950cd6c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-3632.php @@ -0,0 +1,41 @@ +getReservedKeywordsClass(); + $keywords = new $class(); + if (! $keywords instanceof KeywordList) { + throw new \Exception(); + } + + return $keywords; + } + + /** + * @throws \Exception If not supported on this platform. + * + * @psalm-return class-string + */ + protected function getReservedKeywordsClass(): string + { + throw new \Exception(); + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-5333.php b/tests/PHPStan/Rules/Classes/data/bug-5333.php new file mode 100644 index 0000000000..f38c46cddf --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-5333.php @@ -0,0 +1,122 @@ +', $foo); + assertNativeType('array', $foo); + + assertType('Bug5333\Route', $res); + assertNativeType('Bug5333\Route', $res); + + return $res; + } + + return $foo; + } +} + +class HelloWorld2 +{ + /** + * @var Route|callable():Route + **/ + private $foo; + + /** + * @param Route|callable():Route $foo + **/ + public function setFoo($foo): void + { + assertType('Bug5333\Route|(callable(): Bug5333\Route)', $foo); + assertNativeType('mixed', $foo); + + $this->foo = $foo; + } + + public function getFoo(): Route + { + assertType('Bug5333\Route|(callable(): Bug5333\Route)', $this->foo); + assertNativeType('mixed', $this->foo); + + if (\is_callable($this->foo)) { + assertType('(Bug5333\Route&callable(): mixed)|(callable(): Bug5333\Route)', $this->foo); + assertNativeType('callable(): mixed', $this->foo); + + $res = ($this->foo)(); + assertType('mixed', $res); + assertNativeType('mixed', $res); + if (!$res instanceof Route) { + throw new \Exception(); + } + + return $res; + } + + return $this->foo; + } +} + +class HelloFinalWorld +{ + /** + * @var FinalRoute|callable():FinalRoute + **/ + private $foo; + + /** + * @param FinalRoute|callable():FinalRoute $foo + **/ + public function setFoo($foo): void + { + assertType('Bug5333\FinalRoute|(callable(): Bug5333\FinalRoute)', $foo); + assertNativeType('mixed', $foo); + + $this->foo = $foo; + } + + public function getFoo(): FinalRoute + { + assertType('Bug5333\FinalRoute|(callable(): Bug5333\FinalRoute)', $this->foo); + assertNativeType('mixed', $this->foo); + + if (\is_callable($this->foo)) { + assertType('callable(): Bug5333\FinalRoute', $this->foo); + assertNativeType('callable(): mixed', $this->foo); + + $res = ($this->foo)(); + assertType('Bug5333\FinalRoute', $res); + assertNativeType('mixed', $res); + if (!$res instanceof FinalRoute) { + throw new \Exception(); + } + + return $res; + } + + return $this->foo; + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-7574.php b/tests/PHPStan/Rules/Classes/data/bug-7574.php new file mode 100644 index 0000000000..a5671051d6 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-7574.php @@ -0,0 +1,14 @@ += 8.1 + +namespace Bug7721; + +final class A { } +final class B { } +final class C +{ + public function __construct(public readonly A|B $value) { } +} + +$c = new C(value: new A()); + +echo match (true) { + $c->value instanceof A => 'A', + $c->value instanceof B => 'B' +}; diff --git a/tests/PHPStan/Rules/Classes/data/bug-8034.php b/tests/PHPStan/Rules/Classes/data/bug-8034.php new file mode 100644 index 0000000000..04668a1599 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-8034.php @@ -0,0 +1,23 @@ += 8.0 + +namespace Bug8042; + +class A {} +class B {} + +function test(A|B $ab): string { + return match (true) { + $ab instanceof A => 'a', + $ab instanceof B => 'b', + }; +} + +function test2(A|B $ab): string { + return match (true) { + $ab instanceof A => 'a', + $ab instanceof B => 'b', + rand(0, 1) => 'never' + }; +} + +function test3(A|B $ab): string { + return match (true) { + $ab instanceof A => 'a', + $ab instanceof B => 'b', + default => 'never' + }; +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-8889.php b/tests/PHPStan/Rules/Classes/data/bug-8889.php new file mode 100644 index 0000000000..3f3279df1d --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-8889.php @@ -0,0 +1,10 @@ += 8.1 + +namespace Bug9402; + +enum Foo: int +{ + + private const MY_CONST = 1; + private const MY_CONST_STRING = 'foo'; + + case Zero = 0; + case One = self::MY_CONST; + case Two = self::MY_CONST_STRING; + +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-9577.php b/tests/PHPStan/Rules/Classes/data/bug-9577.php new file mode 100644 index 0000000000..48220dec90 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-9577.php @@ -0,0 +1,40 @@ += 8.1 + +namespace Bug9577; + +trait StringableMessageTrait +{ + public function __construct( + public readonly string $message, + ) { + + } +} + +class SpecializedException +{ + use StringableMessageTrait { + StringableMessageTrait::__construct as __traitConstruct; + } + + public function __construct( + public int $code, + string $message, + ) { + $this->__traitConstruct($message); + } +} + +class SpecializedException2 +{ + use StringableMessageTrait { + StringableMessageTrait::__construct as __traitConstruct; + } + + public function __construct( + public int $code, + string $message, + ) { + //$this->__traitConstruct($message); + } +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-9946.php b/tests/PHPStan/Rules/Classes/data/bug-9946.php new file mode 100644 index 0000000000..839ac021a1 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-9946.php @@ -0,0 +1,21 @@ += 7.4 + +namespace Bug9946; + +class Foo +{ + + function test(?\DateTimeImmutable $a, ?string $b): string + { + if (!$a && !$b) { + throw new \LogicException('Either a or b MUST be set'); + } + if (!$a) { + $c = new \DateTimeImmutable($b); + } + $a ??= new \DateTimeImmutable($b); + + return $a->format('c'); + } + +} diff --git a/tests/PHPStan/Rules/Classes/data/class-const-fetch-defined.php b/tests/PHPStan/Rules/Classes/data/class-const-fetch-defined.php new file mode 100644 index 0000000000..618fc491b2 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/class-const-fetch-defined.php @@ -0,0 +1,61 @@ + 'App\GeneratedProxy\__CG__', + ]; + } + +} + +$doctrineEntity = new \App\GeneratedProxy\__CG__\App\TestDoctrineEntity(); +$phpStanEntity = new \_PHPStan_15755dag8c\TestPhpStanEntity(); diff --git a/tests/PHPStan/Rules/Classes/data/impossible-instanceof-report-always-true-last-condition.php b/tests/PHPStan/Rules/Classes/data/impossible-instanceof-report-always-true-last-condition.php new file mode 100644 index 0000000000..09c434a4fa --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/impossible-instanceof-report-always-true-last-condition.php @@ -0,0 +1,41 @@ += 8.0 + + */ +class Elit +{ + +} + +/** + * @mixin Adipiscing + */ +class Elit2 +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/phpstan-internal-class.php b/tests/PHPStan/Rules/Classes/data/phpstan-internal-class.php new file mode 100644 index 0000000000..bb96081020 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/phpstan-internal-class.php @@ -0,0 +1,64 @@ += 7.4 + +namespace _PHPStan_156ee64ba; // mimicks a prefixed class, as contained in phpstan.phar releases + +class PrefixedRuntimeException extends \RuntimeException {} + +class AClass { + const Test = 1; +} + + +namespace TestPhpstanInternalClass; + +use _PHPStan_156ee64ba\PrefixedRuntimeException; + +function doFoo(\_PHPStan_156ee64ba\AClass $e) { + try { + + } catch (\_PHPStan_156ee64ba\PrefixedRuntimeException $exception) { + + } +} + +class Foo { + private \_PHPStan_156ee64ba\AClass $e; + + public function doFoo() { + echo \_PHPStan_156ee64ba\AClass::Test; + + new \_PHPStan_156ee64ba\AClass(); + } +} + +class Bar extends \_PHPStan_156ee64ba\AClass +{} + +namespace RectorPrefix202302; // mimicks a prefixed class, as contained in rector.phar releases + +class AClass { + const Test = 1; +} + + +namespace _PhpScoper19ae93be897e; // mimicks a prefixed class, as generated by PHP-Scoper with default settings + +class AClass { + const Test = 1; +} + +namespace PHPUnitPHAR\SebastianBergmann\Diff; // mimicks a prefixed class, as contained in PHPUnit phar + +class Exception{} + +namespace TestPhpstanInternalClass2; + +class FooBar extends \RectorPrefix202302\AClass +{} + +class Baz extends \_PhpScoper19ae93be897e\AClass +{} + +class BazBar extends \PHPUnitPHAR\SebastianBergmann\Diff\Exception +{} + diff --git a/tests/PHPStan/Rules/Classes/data/readonly-class.php b/tests/PHPStan/Rules/Classes/data/readonly-class.php new file mode 100644 index 0000000000..5f26eacd5d --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/readonly-class.php @@ -0,0 +1,20 @@ += 8.3 + +namespace ReadonlyClass; + +readonly class Foo +{ + +} + +class Bar +{ + + public function doFoo(): void + { + $c = new readonly class () { + + }; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php index c33cc7d2a8..6ebe5bc544 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -13,6 +13,10 @@ class BooleanAndConstantConditionRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain; + private bool $bleedingEdge = false; + + private bool $reportAlwaysTrueInLastCondition = false; + protected function getRule(): Rule { return new BooleanAndConstantConditionRule( @@ -22,10 +26,14 @@ protected function getRule(): Rule $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, + $this->bleedingEdge, + $this->reportAlwaysTrueInLastCondition, ); } @@ -114,6 +122,183 @@ public function testRule(): void 'Right side of && is always true.', 147, ], + [ + 'Left side of && is always true.', + 178, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Right side of && is always true.', + 178, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testRuleLogicalAnd(): void + { + $this->treatPhpDocTypesAsCertain = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/boolean-logical-and.php'], [ + [ + 'Left side of && is always true.', + 15, + ], + [ + 'Right side of && is always true.', + 19, + ], + [ + 'Left side of && is always false.', + 24, + ], + [ + 'Right side of && is always false.', + 27, + ], + [ + 'Result of && is always false.', + 30, + ], + [ + 'Right side of && is always true.', + 33, + ], + [ + 'Right side of && is always true.', + 36, + ], + [ + 'Right side of && is always true.', + 39, + ], + [ + 'Result of && is always false.', + 50, + ], + [ + 'Result of && is always true.', + 54, + $tipText, + ], + [ + 'Result of && is always false.', + 60, + ], + [ + 'Result of && is always true.', + 64, + //$tipText, + ], + [ + 'Result of && is always false.', + 66, + //$tipText, + ], + [ + 'Result of && is always false.', + 125, + ], + [ + 'Left side of && is always false.', + 139, + ], + [ + 'Right side of && is always false.', + 141, + ], + [ + 'Left side of && is always true.', + 145, + ], + [ + 'Right side of && is always true.', + 147, + ], + ]); + } + + public function testRuleLogicalAndBleedingEdge(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->bleedingEdge = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/boolean-logical-and.php'], [ + [ + 'Left side of and is always true.', + 15, + ], + [ + 'Right side of and is always true.', + 19, + ], + [ + 'Left side of and is always false.', + 24, + ], + [ + 'Right side of and is always false.', + 27, + ], + [ + 'Result of and is always false.', + 30, + ], + [ + 'Right side of and is always true.', + 33, + ], + [ + 'Right side of and is always true.', + 36, + ], + [ + 'Right side of and is always true.', + 39, + ], + [ + 'Result of and is always false.', + 50, + ], + [ + 'Result of and is always true.', + 54, + $tipText, + ], + [ + 'Result of and is always false.', + 60, + ], + [ + 'Result of and is always true.', + 64, + //$tipText, + ], + [ + 'Result of and is always false.', + 66, + //$tipText, + ], + [ + 'Result of and is always false.', + 125, + ], + [ + 'Left side of and is always false.', + 139, + ], + [ + 'Right side of and is always false.', + 141, + ], + [ + 'Left side of and is always true.', + 145, + ], + [ + 'Right side of and is always true.', + 147, + ], ]); } @@ -238,7 +423,7 @@ public function testBug2741(): void public function testBug7270(): void { $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/bug-7271.php'], []); + $this->analyse([__DIR__ . '/data/bug-7270.php'], []); } public function testBug5743(): void @@ -247,4 +432,93 @@ public function testBug5743(): void $this->analyse([__DIR__ . '/data/bug-5743.php'], []); } + public function dataBug4969(): iterable + { + yield [false, []]; + yield [true, [ + [ + 'Result of && is always false.', + 15, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]]; + } + + /** + * @dataProvider dataBug4969 + * @param list $expectedErrors + */ + public function testBug4969(bool $treatPhpDocTypesAsCertain, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; + $this->analyse([__DIR__ . '/data/bug-4969.php'], $expectedErrors); + } + + public function dataReportAlwaysTrueInLastCondition(): iterable + { + yield [false, [ + [ + 'Left side of && is always true.', + 23, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Right side of && is always true.', + 50, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Result of && is always true.', + 81, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]]; + yield [true, [ + [ + 'Left side of && is always true.', + 13, + ], + [ + 'Left side of && is always true.', + 23, + ], + [ + 'Right side of && is always true.', + 40, + ], + [ + 'Right side of && is always true.', + 50, + ], + [ + 'Result of && is always true.', + 69, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Result of && is always true.', + 81, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]]; + } + + /** + * @dataProvider dataReportAlwaysTrueInLastCondition + * @param list $expectedErrors + */ + public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/boolean-and-report-always-true-last-condition.php'], $expectedErrors); + } + + public function testBug5365(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = true; + $this->analyse([__DIR__ . '/data/bug-5365.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php index 6a24d7ea3a..43ecf911a6 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php @@ -14,6 +14,8 @@ class BooleanNotConstantConditionRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain; + private bool $reportAlwaysTrueInLastCondition = false; + protected function getRule(): Rule { return new BooleanNotConstantConditionRule( @@ -23,10 +25,13 @@ protected function getRule(): Rule $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, ); } @@ -63,6 +68,11 @@ public function testRule(): void 'Negated boolean expression is always false.', 50, ], + [ + 'Negated boolean expression is always true.', + 67, + 'Remove remaining cases below this one and this error will disappear too.', + ], ]); } @@ -136,4 +146,58 @@ public function testBug5317(): void ]); } + public function testBug8797(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8797.php'], []); + } + + public function dataReportAlwaysTrueInLastCondition(): iterable + { + yield [false, [ + [ + 'Negated boolean expression is always true.', + 23, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Negated boolean expression is always false.', + 40, + ], + [ + 'Negated boolean expression is always false.', + 50, + ], + ]]; + yield [true, [ + [ + 'Negated boolean expression is always true.', + 13, + ], + [ + 'Negated boolean expression is always true.', + 23, + ], + [ + 'Negated boolean expression is always false.', + 40, + ], + [ + 'Negated boolean expression is always false.', + 50, + ], + ]]; + } + + /** + * @dataProvider dataReportAlwaysTrueInLastCondition + * @param list $expectedErrors + */ + public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/boolean-not-report-always-true-last-condition.php'], $expectedErrors); + } + } diff --git a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php index d79fb4492f..b8b0777ca2 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php @@ -14,6 +14,10 @@ class BooleanOrConstantConditionRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain; + private bool $bleedingEdge = false; + + private bool $reportAlwaysTrueInLastCondition = false; + protected function getRule(): Rule { return new BooleanOrConstantConditionRule( @@ -23,10 +27,14 @@ protected function getRule(): Rule $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, + $this->bleedingEdge, + $this->reportAlwaysTrueInLastCondition, ); } @@ -106,6 +114,165 @@ public function testRule(): void 'Right side of || is always true.', 85, ], + [ + 'Left side of || is always true.', + 101, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Right side of || is always true.', + 110, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testRuleLogicalOr(): void + { + $this->treatPhpDocTypesAsCertain = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/boolean-logical-or.php'], [ + [ + 'Left side of || is always true.', + 15, + ], + [ + 'Right side of || is always true.', + 19, + ], + [ + 'Left side of || is always false.', + 24, + ], + [ + 'Right side of || is always false.', + 27, + ], + [ + 'Right side of || is always true.', + 30, + ], + [ + 'Result of || is always true.', + 33, + ], + [ + 'Right side of || is always false.', + 36, + ], + [ + 'Right side of || is always false.', + 39, + ], + [ + 'Result of || is always true.', + 50, + $tipText, + ], + [ + 'Result of || is always true.', + 54, + $tipText, + ], + [ + 'Result of || is always true.', + 61, + ], + [ + 'Result of || is always true.', + 65, + ], + [ + 'Left side of || is always false.', + 77, + ], + [ + 'Right side of || is always false.', + 79, + ], + [ + 'Left side of || is always true.', + 83, + ], + [ + 'Right side of || is always true.', + 85, + ], + ]); + } + + public function testRuleLogicalOrBleedingEdge(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->bleedingEdge = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/boolean-logical-or.php'], [ + [ + 'Left side of or is always true.', + 15, + ], + [ + 'Right side of or is always true.', + 19, + ], + [ + 'Left side of or is always false.', + 24, + ], + [ + 'Right side of or is always false.', + 27, + ], + [ + 'Right side of or is always true.', + 30, + ], + [ + 'Result of or is always true.', + 33, + ], + [ + 'Right side of or is always false.', + 36, + ], + [ + 'Right side of or is always false.', + 39, + ], + [ + 'Result of or is always true.', + 50, + $tipText, + ], + [ + 'Result of or is always true.', + 54, + $tipText, + ], + [ + 'Result of or is always true.', + 61, + ], + [ + 'Result of or is always true.', + 65, + ], + [ + 'Left side of or is always false.', + 77, + ], + [ + 'Right side of or is always false.', + 79, + ], + [ + 'Left side of or is always true.', + 83, + ], + [ + 'Right side of or is always true.', + 85, + ], ]); } @@ -204,4 +371,78 @@ public function testBug7881(): void $this->analyse([__DIR__ . '/data/bug-7881.php'], []); } + public function dataReportAlwaysTrueInLastCondition(): iterable + { + yield [false, [ + [ + 'Left side of || is always true.', + 23, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Right side of || is always true.', + 50, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Result of || is always true.', + 81, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]]; + yield [true, [ + [ + 'Left side of || is always true.', + 13, + ], + [ + 'Left side of || is always true.', + 23, + ], + [ + 'Right side of || is always true.', + 40, + ], + [ + 'Right side of || is always true.', + 50, + ], + [ + 'Result of || is always true.', + 69, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Result of || is always true.', + 81, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]]; + } + + /** + * @dataProvider dataReportAlwaysTrueInLastCondition + * @param list $expectedErrors + */ + public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/boolean-or-report-always-true-last-condition.php'], $expectedErrors); + } + + public function testBug6551(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = true; + $this->analyse([__DIR__ . '/data/bug-6551.php'], []); + } + + public function testBug4004(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = true; + $this->analyse([__DIR__ . '/data/bug-4004.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php index 2234e05ba9..f8a0b7d87d 100644 --- a/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -13,9 +14,13 @@ class ConstantLooseComparisonRuleTest extends RuleTestCase private bool $checkAlwaysTrueStrictComparison; + private bool $treatPhpDocTypesAsCertain = true; + + private bool $reportAlwaysTrueInLastCondition = false; + protected function getRule(): Rule { - return new ConstantLooseComparisonRule($this->checkAlwaysTrueStrictComparison); + return new ConstantLooseComparisonRule($this->checkAlwaysTrueStrictComparison, $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition); } public function testRule(): void @@ -26,6 +31,19 @@ public function testRule(): void "Loose comparison using == between 0 and '1' will always evaluate to false.", 20, ], + [ + "Loose comparison using == between 0 and '1' will always evaluate to false.", + 27, + ], + [ + "Loose comparison using == between 0 and '1' will always evaluate to false.", + 33, + ], + [ + 'Loose comparison using != between 3 and 3 will always evaluate to false.', + 48, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], ]); } @@ -41,7 +59,111 @@ public function testRuleAlwaysTrue(): void "Loose comparison using == between 0 and '1' will always evaluate to false.", 20, ], + [ + "Loose comparison using == between 0 and '1' will always evaluate to false.", + 27, + ], + [ + "Loose comparison using == between 0 and '1' will always evaluate to false.", + 33, + ], + [ + "Loose comparison using == between 0 and '0' will always evaluate to true.", + 35, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Loose comparison using != between 3 and 3 will always evaluate to false.', + 48, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], ]); } + public function testBug8485(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-8485.php'], [ + [ + 'Loose comparison using == between Bug8485\E::c and Bug8485\E::c will always evaluate to true.', + 21, + ], + [ + 'Loose comparison using == between Bug8485\F::c and Bug8485\E::c will always evaluate to false.', + 26, + ], + [ + 'Loose comparison using == between Bug8485\F::c and Bug8485\E::c will always evaluate to false.', + 31, + ], + [ + 'Loose comparison using == between Bug8485\F and Bug8485\E will always evaluate to false.', + 38, + ], + [ + 'Loose comparison using == between Bug8485\F and Bug8485\E::c will always evaluate to false.', + 43, + ], + ]); + } + + public function dataReportAlwaysTrueInLastCondition(): iterable + { + yield [false, [ + [ + 'Loose comparison using == between 1 and 1 will always evaluate to true.', + 21, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]]; + yield [true, [ + [ + 'Loose comparison using == between 1 and 1 will always evaluate to true.', + 12, + ], + [ + 'Loose comparison using == between 1 and 1 will always evaluate to true.', + 21, + ], + ]]; + } + + /** + * @dataProvider dataReportAlwaysTrueInLastCondition + * @param list $expectedErrors + */ + public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + $this->checkAlwaysTrueStrictComparison = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/loose-comparison-report-always-true-last-condition.php'], $expectedErrors); + } + + public function dataTreatPhpDocTypesAsCertain(): iterable + { + yield [false, []]; + yield [true, [ + [ + 'Loose comparison using == between 3 and 3 will always evaluate to true.', + 14, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]]; + } + + /** + * @dataProvider dataTreatPhpDocTypesAsCertain + * @param list $expectedErrors + */ + public function testTreatPhpDocTypesAsCertain(bool $treatPhpDocTypesAsCertain, array $expectedErrors): void + { + $this->checkAlwaysTrueStrictComparison = true; + $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; + $this->analyse([__DIR__ . '/data/loose-comparison-treat-phpdoc-types.php'], $expectedErrors); + } + } diff --git a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php index 77ce3dad2c..703c2e5a87 100644 --- a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php @@ -22,8 +22,10 @@ protected function getRule(): Rule $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, ); diff --git a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php index 343da11cbc..7d3b008d90 100644 --- a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php @@ -13,6 +13,8 @@ class ElseIfConstantConditionRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain; + private bool $reportAlwaysTrueInLastCondition = false; + protected function getRule(): Rule { return new ElseIfConstantConditionRule( @@ -22,10 +24,13 @@ protected function getRule(): Rule $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, ); } @@ -34,15 +39,57 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } - public function testRule(): void + public function dataRule(): iterable { - $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/elseif-condition.php'], [ + yield [false, [ + [ + 'Elseif condition is always true.', + 56, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Elseif condition is always false.', + 73, + ], + [ + 'Elseif condition is always false.', + 77, + ], + ]]; + + yield [true, [ [ 'Elseif condition is always true.', 18, ], - ]); + [ + 'Elseif condition is always true.', + 52, + ], + [ + 'Elseif condition is always true.', + 56, + ], + [ + 'Elseif condition is always false.', + 73, + ], + [ + 'Elseif condition is always false.', + 77, + ], + ]]; + } + + /** + * @dataProvider dataRule + * @param list $expectedErrors + */ + public function testRule(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/elseif-condition.php'], $expectedErrors); } public function testDoNotReportPhpDoc(): void @@ -51,7 +98,8 @@ public function testDoNotReportPhpDoc(): void $this->analyse([__DIR__ . '/data/elseif-condition-not-phpdoc.php'], [ [ 'Elseif condition is always true.', - 18, + 46, + 'Remove remaining cases below this one and this error will disappear too.', ], ]); } @@ -62,12 +110,13 @@ public function testReportPhpDoc(): void $this->analyse([__DIR__ . '/data/elseif-condition-not-phpdoc.php'], [ [ 'Elseif condition is always true.', - 18, + 46, + 'Remove remaining cases below this one and this error will disappear too.', ], [ 'Elseif condition is always true.', - 24, - 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + 56, + 'Remove remaining cases below this one and this error will disappear too.', ], ]); } diff --git a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php index 5bd1520ded..979af0dc12 100644 --- a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -22,8 +23,10 @@ protected function getRule(): Rule $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, ); @@ -50,6 +53,7 @@ public function testRule(): void [ 'If condition is always true.', 96, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'If condition is always true.', @@ -122,4 +126,40 @@ public function testBug5370(): void $this->analyse([__DIR__ . '/data/bug-5370.php'], []); } + public function testBug6902(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6902.php'], []); + } + + public function testBug8485(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->treatPhpDocTypesAsCertain = true; + + // reported by ConstantLooseComparisonRule instead + $this->analyse([__DIR__ . '/data/bug-8485.php'], []); + } + + public function testBug4302(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4302.php'], []); + } + + public function testBug7491(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7491.php'], []); + } + + public function testBug2499(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-2499.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 403f859487..361aa84bbb 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -5,6 +5,10 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use stdClass; +use function array_filter; +use function array_map; +use function array_values; +use function count; use const PHP_VERSION_ID; /** @@ -17,6 +21,8 @@ class ImpossibleCheckTypeFunctionCallRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain; + private bool $reportAlwaysTrueInLastCondition = false; + protected function getRule(): Rule { return new ImpossibleCheckTypeFunctionCallRule( @@ -25,9 +31,11 @@ protected function getRule(): Rule $this->getTypeSpecifier(), [stdClass::class], $this->treatPhpDocTypesAsCertain, + true, ), $this->checkAlwaysTrueCheckTypeFunctionCall, $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, ); } @@ -175,14 +183,17 @@ public function testImpossibleCheckTypeFunctionCall(): void [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'method\' will always evaluate to true.', 634, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'someAnother\' will always evaluate to true.', 637, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'unknown\' will always evaluate to false.', 640, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'method\' will always evaluate to true.', @@ -204,6 +215,7 @@ public function testImpossibleCheckTypeFunctionCall(): void [ 'Call to function assert() with true will always evaluate to true.', 693, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function is_numeric() with \'123\' will always evaluate to true.', @@ -212,6 +224,7 @@ public function testImpossibleCheckTypeFunctionCall(): void [ 'Call to function assert() with false will always evaluate to false.', 694, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function is_numeric() with \'blabla\' will always evaluate to false.', @@ -220,6 +233,7 @@ public function testImpossibleCheckTypeFunctionCall(): void [ 'Call to function assert() with true will always evaluate to true.', 701, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function is_numeric() with 123|float will always evaluate to true.', @@ -233,6 +247,20 @@ public function testImpossibleCheckTypeFunctionCall(): void 'Call to function property_exists() with CheckTypeFunctionCall\Bug2221 and \'foo\' will always evaluate to true.', 788, ], + [ + 'Call to function testIsInt() with int will always evaluate to true.', + 875, + ], + [ + 'Call to function is_int() with int will always evaluate to true.', + 889, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Call to function in_array() with arguments 1, array and true will always evaluate to false.', + 927, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], ], ); } @@ -323,6 +351,7 @@ public function testImpossibleCheckTypeFunctionCallWithoutAlwaysTrue(): void [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'unknown\' will always evaluate to false.', 640, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'unknown\' will always evaluate to false.', @@ -331,11 +360,17 @@ public function testImpossibleCheckTypeFunctionCallWithoutAlwaysTrue(): void [ 'Call to function assert() with false will always evaluate to false.', 694, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function is_numeric() with \'blabla\' will always evaluate to false.', 694, ], + [ + 'Call to function in_array() with arguments 1, array and true will always evaluate to false.', + 927, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], ], ); } @@ -366,6 +401,16 @@ public function testReportTypesFromPhpDocs(): void 19, 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], + [ + 'Call to function in_array() with arguments int, array and true will always evaluate to false.', + 27, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Call to function in_array() with arguments 1, array and true will always evaluate to false.', + 30, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], ]); } @@ -481,15 +526,22 @@ public function testBugInArrayDateFormat(): void [ 'Call to function in_array() with arguments \'a\', non-empty-array and true will always evaluate to true.', 39, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function in_array() with arguments \'b\', non-empty-array and true will always evaluate to false.', 43, + //'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to function in_array() with arguments int, array{} and true will always evaluate to false.', 47, ], + [ + 'Call to function in_array() with arguments int, array and true will always evaluate to false.', + 61, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], ]); } @@ -577,6 +629,10 @@ public function testConditionalTypesInference(): void 'Call to function testIsNotInt() with int will always evaluate to false.', 72, ], + [ + 'Call to function assertIsInt() with int will always evaluate to true.', + 78, + ], ]); } @@ -636,4 +692,367 @@ public function testBug7914(): void $this->analyse([__DIR__ . '/data/bug-7914.php'], []); } + public function testDocblockAssertEquality(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/docblock-assert-equality.php'], [ + [ + 'Call to function isAnInteger() with int will always evaluate to true.', + 42, + ], + ]); + } + + public function testBug8076(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8076.php'], []); + } + + public function testBug8562(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8562.php'], []); + } + + public function testBug6938(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-6938.php'], []); + } + + public function testBug8727(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8727.php'], []); + } + + public function testBug8474(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8474.php'], []); + } + + public function testBug5695(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-5695.php'], []); + } + + public function testBug8752(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/data/bug-8752.php'], []); + } + + public function testDiscussion9134(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/data/discussion-9134.php'], []); + } + + public function testImpossibleMethodExistOnGenericClassString(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/impossible-method-exists-on-generic-class-string.php'], [ + [ + "Call to function method_exists() with class-string&literal-string and 'staticAbc' will always evaluate to true.", + 18, + $tipText, + ], + [ + "Call to function method_exists() with class-string&literal-string and 'nonStaticAbc' will always evaluate to true.", + 23, + $tipText, + ], + [ + "Call to function method_exists() with class-string&literal-string and 'nonExistent' will always evaluate to false.", + 34, + $tipText, + ], + [ + "Call to function method_exists() with class-string&literal-string and 'staticAbc' will always evaluate to true.", + 39, + $tipText, + ], + [ + "Call to function method_exists() with class-string&literal-string and 'nonStaticAbc' will always evaluate to true.", + 44, + $tipText, + ], + + ]); + } + + public function dataReportAlwaysTrueInLastCondition(): iterable + { + yield [false, [ + [ + 'Call to function is_int() with int will always evaluate to true.', + 21, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]]; + yield [true, [ + [ + 'Call to function is_int() with int will always evaluate to true.', + 12, + ], + [ + 'Call to function is_int() with int will always evaluate to true.', + 21, + ], + ]]; + } + + /** + * @dataProvider dataReportAlwaysTrueInLastCondition + * @param list $expectedErrors + */ + public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/impossible-function-report-always-true-last-condition.php'], $expectedErrors); + } + + public function testObjectShapes(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/property-exists-object-shapes.php'], [ + [ + 'Call to function property_exists() with object{foo: int, bar?: string} and \'baz\' will always evaluate to false.', + 24, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + + /** @return list */ + private static function getLooseComparisonAgainsEnumsIssues(): array + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + return [ + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\\FooUnitEnum and array{\'A\'} will always evaluate to false.', + 21, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\\FooUnitEnum, array{\'A\'} and false will always evaluate to false.', + 24, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\\FooBackedEnum and array{\'A\'} will always evaluate to false.', + 27, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\\FooBackedEnum, array{\'A\'} and false will always evaluate to false.', + 30, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\\FooBackedEnum|LooseComparisonAgainstEnums\\FooUnitEnum, array{\'A\'} and false will always evaluate to false.', + 33, + ], + [ + 'Call to function in_array() with \'A\' and array{LooseComparisonAgainstEnums\\FooUnitEnum} will always evaluate to false.', + 39, + ], + [ + 'Call to function in_array() with arguments \'A\', array{LooseComparisonAgainstEnums\\FooUnitEnum} and false will always evaluate to false.', + 42, + ], + [ + 'Call to function in_array() with \'A\' and array{LooseComparisonAgainstEnums\\FooBackedEnum} will always evaluate to false.', + 45, + ], + [ + 'Call to function in_array() with arguments \'A\', array{LooseComparisonAgainstEnums\\FooBackedEnum} and false will always evaluate to false.', + 48, + ], + [ + 'Call to function in_array() with arguments \'A\', array{LooseComparisonAgainstEnums\\FooBackedEnum|LooseComparisonAgainstEnums\\FooUnitEnum} and false will always evaluate to false.', + 51, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum and array{bool} will always evaluate to false.', + 57, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum, array{bool} and false will always evaluate to false.', + 60, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooBackedEnum and array{bool} will always evaluate to false.', + 63, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooBackedEnum, array{bool} and false will always evaluate to false.', + 66, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooBackedEnum|LooseComparisonAgainstEnums\FooUnitEnum, array{bool} and false will always evaluate to false.', + 69, + ], + [ + 'Call to function in_array() with bool and array{LooseComparisonAgainstEnums\FooUnitEnum} will always evaluate to false.', + 75, + ], + [ + 'Call to function in_array() with arguments bool, array{LooseComparisonAgainstEnums\FooUnitEnum} and false will always evaluate to false.', + 78, + ], + [ + 'Call to function in_array() with bool and array{LooseComparisonAgainstEnums\FooBackedEnum} will always evaluate to false.', + 81, + ], + [ + 'Call to function in_array() with arguments bool, array{LooseComparisonAgainstEnums\FooBackedEnum} and false will always evaluate to false.', + 84, + ], + [ + 'Call to function in_array() with arguments bool, array{LooseComparisonAgainstEnums\FooBackedEnum|LooseComparisonAgainstEnums\FooUnitEnum} and false will always evaluate to false.', + 87, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum and array{null} will always evaluate to false.', + 93, + ], + [ + 'Call to function in_array() with null and array{LooseComparisonAgainstEnums\FooBackedEnum} will always evaluate to false.', + 96, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum and array will always evaluate to false.', + 125, + $tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum, array and false will always evaluate to false.', + 128, + $tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum, array and true will always evaluate to false.', + 131, + $tipText, + ], + [ + 'Call to function in_array() with string and array will always evaluate to false.', + 143, + $tipText, + ], + [ + 'Call to function in_array() with arguments string, array and false will always evaluate to false.', + 146, + $tipText, + ], + [ + 'Call to function in_array() with arguments string, array and true will always evaluate to false.', + 149, + $tipText, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum::B and non-empty-array will always evaluate to false.', + 159, + $tipText, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum::A and non-empty-array will always evaluate to true.', + 162, + $tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::A, non-empty-array and false will always evaluate to true.', + 165, + 'BUG', + //$tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::A, non-empty-array and true will always evaluate to true.', + 168, + 'BUG', + //$tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::B, non-empty-array and false will always evaluate to false.', + 171, + 'BUG', + //$tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::B, non-empty-array and true will always evaluate to false.', + 174, + 'BUG', + //$tipText, + ], + ]; + } + + public function testLooseComparisonAgainstEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $issues = array_map( + static function (array $i): array { + if (($i[2] ?? null) === 'BUG') { + unset($i[2]); + } + + return $i; + }, + self::getLooseComparisonAgainsEnumsIssues(), + ); + $this->analyse([__DIR__ . '/data/loose-comparison-against-enums.php'], $issues); + } + + public function testLooseComparisonAgainstEnumsNoPhpdoc(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = false; + $issues = self::getLooseComparisonAgainsEnumsIssues(); + $issues = array_values(array_filter($issues, static fn (array $i) => count($i) === 2)); + $this->analyse([__DIR__ . '/data/loose-comparison-against-enums.php'], $issues); + } + + public function testBug10502(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-10502.php'], [ + [ + "Call to function is_callable() with array{ArrayObject, 'count'} will always evaluate to true.", + 23, + ], + [ + "Call to function is_callable() with array{1: 'count', 0: ArrayObject} will always evaluate to true.", + 24, + $tipText, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php index 7d12e3f769..9be90b8054 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php @@ -19,9 +19,11 @@ public function getRule(): Rule $this->getTypeSpecifier(), [], true, + true, ), true, true, + false, ); } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php index 0607f5bbfb..3aa4bf0810 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php @@ -19,9 +19,11 @@ public function getRule(): Rule $this->getTypeSpecifier(), [], true, + true, ), true, true, + false, ); } @@ -71,10 +73,12 @@ public function testRule(): void [ 'Call to method ImpossibleMethodCall\Foo::isSame() with \'foo\' and \'foo\' will always evaluate to true.', 101, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to method ImpossibleMethodCall\Foo::isNotSame() with \'foo\' and \'foo\' will always evaluate to false.', 104, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to method ImpossibleMethodCall\Foo::isSame() with array{} and array{} will always evaluate to true.', @@ -116,6 +120,11 @@ public function testRule(): void 'Call to method ImpossibleMethodCall\Foo::isNotSame() with 2 and 2 will always evaluate to false.', 194, ], + [ + 'Call to method ImpossibleMethodCall\ConditionalAlwaysTrue::isInt() with int will always evaluate to true.', + 208, + 'Remove remaining cases below this one and this error will disappear too.', + ], ]); } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php index 5db54072a4..7a697e6ffa 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php @@ -13,6 +13,8 @@ class ImpossibleCheckTypeMethodCallRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain; + private bool $reportAlwaysTrueInLastCondition = false; + public function getRule(): Rule { return new ImpossibleCheckTypeMethodCallRule( @@ -21,9 +23,11 @@ public function getRule(): Rule $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, + true, ), true, $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, ); } @@ -79,10 +83,12 @@ public function testRule(): void [ 'Call to method ImpossibleMethodCall\Foo::isSame() with \'foo\' and \'foo\' will always evaluate to true.', 101, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to method ImpossibleMethodCall\Foo::isNotSame() with \'foo\' and \'foo\' will always evaluate to false.', 104, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to method ImpossibleMethodCall\Foo::isSame() with array{} and array{} will always evaluate to true.', @@ -148,6 +154,11 @@ public function testRule(): void 'Call to method ImpossibleMethodCall\Foo::isNotSame() with 2 and 2 will always evaluate to false.', 194, ], + [ + 'Call to method ImpossibleMethodCall\ConditionalAlwaysTrue::isInt() with int will always evaluate to true.', + 208, + 'Remove remaining cases below this one and this error will disappear too.', + ], ]); } @@ -186,6 +197,57 @@ public function testReportPhpDoc(): void ]); } + public function testBug8169(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8169.php'], [ + [ + 'Call to method Bug8169\HelloWorld::assertString() with string will always evaluate to true.', + 19, + ], + [ + 'Call to method Bug8169\HelloWorld::assertString() with string will always evaluate to true.', + 26, + ], + [ + 'Call to method Bug8169\HelloWorld::assertString() with int will always evaluate to false.', + 33, + ], + ]); + } + + public function dataReportAlwaysTrueInLastCondition(): iterable + { + yield [false, [ + [ + 'Call to method PHPStan\Tests\AssertionClass::assertString() with string will always evaluate to true.', + 25, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]]; + yield [true, [ + [ + 'Call to method PHPStan\Tests\AssertionClass::assertString() with string will always evaluate to true.', + 15, + ], + [ + 'Call to method PHPStan\Tests\AssertionClass::assertString() with string will always evaluate to true.', + 25, + ], + ]]; + } + + /** + * @dataProvider dataReportAlwaysTrueInLastCondition + * @param list $expectedErrors + */ + public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/impossible-method-report-always-true-last-condition.php'], $expectedErrors); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php index 9115a5f9ce..a93147ab83 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php @@ -13,6 +13,8 @@ class ImpossibleCheckTypeStaticMethodCallRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain; + private bool $reportAlwaysTrueInLastCondition = false; + public function getRule(): Rule { return new ImpossibleCheckTypeStaticMethodCallRule( @@ -21,9 +23,11 @@ public function getRule(): Rule $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, + true, ), true, $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, ); } @@ -60,6 +64,11 @@ public function testRule(): void 'Call to static method PHPStan\Tests\AssertionClass::assertInt() with arguments 1, 2 and 3 will always evaluate to true.', 34, ], + [ + 'Call to static method ImpossibleStaticMethodCall\ConditionalAlwaysTrue::isInt() with int will always evaluate to true.', + 66, + 'Remove remaining cases below this one and this error will disappear too.', + ], ]); } @@ -98,6 +107,44 @@ public function testReportPhpDocs(): void ]); } + public function testAssertUnresolvedGeneric(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/assert-unresolved-generic.php'], []); + } + + public function dataReportAlwaysTrueInLastCondition(): iterable + { + yield [false, [ + [ + 'Call to static method PHPStan\Tests\AssertionClass::assertInt() with int will always evaluate to true.', + 23, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]]; + yield [true, [ + [ + 'Call to static method PHPStan\Tests\AssertionClass::assertInt() with int will always evaluate to true.', + 14, + ], + [ + 'Call to static method PHPStan\Tests\AssertionClass::assertInt() with int will always evaluate to true.', + 23, + ], + ]]; + } + + /** + * @dataProvider dataReportAlwaysTrueInLastCondition + * @param list $expectedErrors + */ + public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/impossible-static-method-report-always-true-last-condition.php'], $expectedErrors); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php new file mode 100644 index 0000000000..24080f2e00 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php @@ -0,0 +1,79 @@ + + */ +class LogicalXorConstantConditionRuleTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain; + + private bool $reportAlwaysTrueInLastCondition = false; + + protected function getRule(): TRule + { + return new LogicalXorConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + $this->createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + true, + ), + $this->treatPhpDocTypesAsCertain, + true, + ), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + ); + } + + public function testRule(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/logical-xor.php'], [ + [ + 'Left side of xor is always true.', + 14, + ], + [ + 'Right side of xor is always false.', + 14, + ], + [ + 'Left side of xor is always false.', + 17, + ], + [ + 'Right side of xor is always true.', + 17, + ], + [ + 'Left side of xor is always true.', + 20, + $tipText, + ], + [ + 'Right side of xor is always true.', + 20, + $tipText, + ], + [ + 'Left side of xor is always true.', + 24, + ], + [ + 'Right side of xor is always false.', + 24, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionDoNotRememberPossiblyImpureValuesRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionDoNotRememberPossiblyImpureValuesRuleTest.php new file mode 100644 index 0000000000..b52785c6f5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionDoNotRememberPossiblyImpureValuesRuleTest.php @@ -0,0 +1,49 @@ + + */ +class MatchExpressionDoNotRememberPossiblyImpureValuesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(MatchExpressionRule::class); + } + + public function testBug9357(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9357.php'], []); + } + + public function testBug9007(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9007.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/doNotRememberPossiblyImpureValues.neon', + ], + ); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php index 76a2687457..b267bfc3e1 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -14,9 +14,29 @@ class MatchExpressionRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain = true; + private bool $reportAlwaysTrueInLastCondition = false; + + private bool $disableUnreachable = false; + protected function getRule(): Rule { - return new MatchExpressionRule(true); + return new MatchExpressionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + $this->createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + true, + ), + $this->treatPhpDocTypesAsCertain, + true, + ), + true, + $this->disableUnreachable, + $this->reportAlwaysTrueInLastCondition, + $this->treatPhpDocTypesAsCertain, + ); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -26,6 +46,7 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool public function testRule(): void { + $tipText = 'Remove remaining cases below this one and this error will disappear too.'; $this->analyse([__DIR__ . '/data/match-expr.php'], [ [ 'Match arm comparison between 1|2|3 and \'foo\' is always false.', @@ -38,6 +59,7 @@ public function testRule(): void [ 'Match arm comparison between 3 and 3 is always true.', 28, + $tipText, ], [ 'Match arm is unreachable because previous comparison is always true.', @@ -46,6 +68,7 @@ public function testRule(): void [ 'Match arm comparison between 3 and 3 is always true.', 35, + $tipText, ], [ 'Match arm is unreachable because previous comparison is always true.', @@ -54,6 +77,7 @@ public function testRule(): void [ 'Match arm comparison between 1 and 1 is always true.', 40, + $tipText, ], [ 'Match arm is unreachable because previous comparison is always true.', @@ -66,6 +90,7 @@ public function testRule(): void [ 'Match arm comparison between 1 and 1 is always true.', 46, + $tipText, ], [ 'Match arm is unreachable because previous comparison is always true.', @@ -79,22 +104,10 @@ public function testRule(): void 'Match arm comparison between 1|2 and 3 is always false.', 61, ], - [ - 'Match arm comparison between 1 and 1 is always true.', - 66, - ], [ 'Match expression does not handle remaining values: 1|2|3', 78, ], - [ - 'Match arm comparison between true and false is always false.', - 86, - ], - [ - 'Match arm comparison between true and false is always false.', - 92, - ], [ 'Match expression does not handle remaining value: true', 90, @@ -153,7 +166,12 @@ public function testEnums(): void 56, ], [ - 'Match arm comparison between *NEVER* and MatchEnums\Foo is always false.', + 'Match arm comparison between MatchEnums\Foo::THREE and MatchEnums\Foo::THREE is always true.', + 76, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Match arm is unreachable because previous comparison is always true.', 77, ], ]); @@ -242,4 +260,287 @@ public function testBug7746(): void $this->analyse([__DIR__ . '/data/bug-7746.php'], []); } + public function testBug8240(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8240.php'], [ + [ + 'Match arm comparison between Bug8240\Foo and Bug8240\Foo::BAR is always true.', + 13, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Match arm is unreachable because previous comparison is always true.', + 14, + ], + [ + 'Match arm comparison between Bug8240\Foo2::BAZ and Bug8240\Foo2::BAZ is always true.', + 28, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Match arm is unreachable because previous comparison is always true.', + 29, + ], + ]); + } + + public function testLastArmAlwaysTrue(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $this->treatPhpDocTypesAsCertain = true; + $tipText = 'Remove remaining cases below this one and this error will disappear too.'; + $this->analyse([__DIR__ . '/data/last-match-arm-always-true.php'], [ + [ + 'Match arm comparison between $this(LastMatchArmAlwaysTrue\Foo)&LastMatchArmAlwaysTrue\Foo::TWO and LastMatchArmAlwaysTrue\Foo::TWO is always true.', + 22, + $tipText, + ], + [ + 'Match arm is unreachable because previous comparison is always true.', + 23, + ], + [ + 'Match arm comparison between $this(LastMatchArmAlwaysTrue\Foo)&LastMatchArmAlwaysTrue\Foo::TWO and LastMatchArmAlwaysTrue\Foo::TWO is always true.', + 31, + $tipText, + ], + [ + 'Match arm is unreachable because previous comparison is always true.', + 32, + ], + [ + 'Match arm comparison between $this(LastMatchArmAlwaysTrue\Foo)&LastMatchArmAlwaysTrue\Foo::TWO and LastMatchArmAlwaysTrue\Foo::TWO is always true.', + 40, + $tipText, + ], + [ + 'Match arm is unreachable because previous comparison is always true.', + 41, + ], + [ + 'Match arm is unreachable because previous comparison is always true.', + 42, + ], + [ + 'Match arm comparison between $this(LastMatchArmAlwaysTrue\Bar) and LastMatchArmAlwaysTrue\Bar::ONE is always true.', + 62, + $tipText, + ], + [ + 'Match arm is unreachable because previous comparison is always true.', + 63, + ], + [ + 'Match arm comparison between 1 and 0 is always false.', + 70, + ], + [ + 'Match expression does not handle remaining value: 1', + 69, + ], + ]); + } + + public function dataReportAlwaysTrueInLastCondition(): iterable + { + yield [false, false, [ + [ + 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', + 23, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Match arm is unreachable because previous comparison is always true.', + 24, + ], + [ + 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', + 49, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Match arm is unreachable because previous comparison is always true.', + 50, + ], + ]]; + yield [true, false, [ + [ + 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', + 15, + ], + [ + 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', + 23, + ], + [ + 'Match arm is unreachable because previous comparison is always true.', + 24, + ], + [ + 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', + 45, + ], + [ + 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', + 49, + ], + [ + 'Match arm is unreachable because previous comparison is always true.', + 50, + ], + ]]; + yield [false, true, [ + [ + 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', + 23, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', + 49, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]]; + yield [true, true, [ + [ + 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', + 15, + ], + [ + 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', + 23, + ], + [ + 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', + 45, + ], + [ + 'Match arm comparison between $this(MatchAlwaysTrueLastArm\Foo)&MatchAlwaysTrueLastArm\Foo::BAR and MatchAlwaysTrueLastArm\Foo::BAR is always true.', + 49, + ], + ]]; + } + + /** + * @dataProvider dataReportAlwaysTrueInLastCondition + * @param list $expectedErrors + */ + public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, bool $disableUnreachable, array $expectedErrors): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->disableUnreachable = $disableUnreachable; + $this->analyse([__DIR__ . '/data/match-always-true-last-arm.php'], $expectedErrors); + } + + public function testBug8932(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-8932.php'], []); + } + + public function testBug8937(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-8937.php'], []); + } + + public function testBug8900(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-8900.php'], []); + } + + public function testBug4451(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-4451.php'], []); + } + + public function testBug9007(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9007.php'], []); + } + + public function testBug9457(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9457.php'], []); + } + + public function testBug8614(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-8614.php'], []); + } + + public function testBug8536(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-8536.php'], []); + } + + public function testBug9499(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9499.php'], []); + } + + public function testBug6407(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-6407.php'], []); + } + + public function testBugUnhandledTrueWithComplexCondition(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-unhandled-true-with-complex-condition.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index 4f7448cd51..78cef84972 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -12,9 +12,16 @@ class NumberComparisonOperatorsConstantConditionRuleTest extends RuleTestCase { + private bool $treatPhpDocTypesAsCertain = true; + protected function getRule(): Rule { - return new NumberComparisonOperatorsConstantConditionRule(); + return new NumberComparisonOperatorsConstantConditionRule($this->treatPhpDocTypesAsCertain); + } + + public function testBug8277(): void + { + $this->analyse([__DIR__ . '/data/bug-8277.php'], []); } public function testRule(): void @@ -150,4 +157,78 @@ public function testBug2851(): void $this->analyse([__DIR__ . '/data/bug-2851.php'], []); } + public function testBug8643(): void + { + $this->analyse([__DIR__ . '/data/bug-8643.php'], []); + } + + public function dataTreatPhpDocTypesAsCertain(): iterable + { + yield [ + false, + [], + ]; + yield [ + true, + [ + [ + 'Comparison operation ">=" between int<1, max> and 0 is always true.', + 11, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Comparison operation "<" between int<1, max> and 0 is always false.', + 18, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ], + ]; + } + + /** + * @dataProvider dataTreatPhpDocTypesAsCertain + * @param list $expectedErrors + */ + public function testTreatPhpDocTypesAsCertain(bool $treatPhpDocTypesAsCertain, array $expectedErrors): void + { + $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; + $this->analyse([__DIR__ . '/data/number-comparison-treat.php'], $expectedErrors); + } + + public function testBug6776(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-6776.php'], []); + } + + public function testBug7075(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-7075.php'], []); + } + + public function testBug8803(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/data/bug-8803.php'], []); + } + + public function testBug8938(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8938.php'], []); + } + + public function testBug5005(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-5005.php'], []); + } + + public function testBug6467(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6467.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index 972225e3a6..d8397ed533 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -15,14 +15,24 @@ class StrictComparisonOfDifferentTypesRuleTest extends RuleTestCase private bool $checkAlwaysTrueStrictComparison; + private bool $reportAlwaysTrueInLastCondition = false; + + private bool $treatPhpDocTypesAsCertain = true; + protected function getRule(): Rule { - return new StrictComparisonOfDifferentTypesRule($this->checkAlwaysTrueStrictComparison); + return new StrictComparisonOfDifferentTypesRule($this->checkAlwaysTrueStrictComparison, $this->treatPhpDocTypesAsCertain, $this->reportAlwaysTrueInLastCondition); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; } public function testStrictComparison(): void { $this->checkAlwaysTrueStrictComparison = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse( [__DIR__ . '/data/strict-comparison.php'], [ @@ -49,6 +59,7 @@ public function testStrictComparison(): void [ 'Strict comparison using === between 1 and array|bool|StrictComparison\Collection will always evaluate to false.', 19, + $tipText, ], [ 'Strict comparison using === between true and false will always evaluate to false.', @@ -101,10 +112,12 @@ public function testStrictComparison(): void [ 'Strict comparison using !== between StrictComparison\Node|null and false will always evaluate to true.', 212, + $tipText, ], [ 'Strict comparison using !== between StrictComparison\Node|null and false will always evaluate to true.', 255, + $tipText, ], [ 'Strict comparison using !== between stdClass and null will always evaluate to true.', @@ -173,6 +186,7 @@ public function testStrictComparison(): void [ 'Strict comparison using === between int<0, 1> and 100 will always evaluate to false.', 622, + $tipText, ], [ 'Strict comparison using === between 100 and \'foo\' will always evaluate to false.', @@ -182,10 +196,6 @@ public function testStrictComparison(): void 'Strict comparison using === between int<10, max> and \'foo\' will always evaluate to false.', 635, ], - [ - 'Strict comparison using === between \'foofoofoofoofoofoof…\' and \'foofoofoofoofoofoof…\' will always evaluate to true.', - 654, - ], [ 'Strict comparison using === between string|null and 1 will always evaluate to false.', 685, @@ -250,6 +260,11 @@ public function testStrictComparison(): void 'Strict comparison using !== between NAN and NAN will always evaluate to true.', 983, ], + [ + 'Strict comparison using === between \'foofoofoofoofoofoof…\' and \'foofoofoofoofoofoof…\' will always evaluate to true.', + 996, + 'Remove remaining cases below this one and this error will disappear too.', + ], ], ); } @@ -257,6 +272,7 @@ public function testStrictComparison(): void public function testStrictComparisonWithoutAlwaysTrue(): void { $this->checkAlwaysTrueStrictComparison = false; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse( [__DIR__ . '/data/strict-comparison.php'], [ @@ -275,6 +291,7 @@ public function testStrictComparisonWithoutAlwaysTrue(): void [ 'Strict comparison using === between 1 and array|bool|StrictComparison\Collection will always evaluate to false.', 19, + $tipText, ], [ 'Strict comparison using === between true and false will always evaluate to false.', @@ -367,6 +384,7 @@ public function testStrictComparisonWithoutAlwaysTrue(): void [ 'Strict comparison using === between int<0, 1> and 100 will always evaluate to false.', 622, + $tipText, ], [ 'Strict comparison using === between 100 and \'foo\' will always evaluate to false.', @@ -562,6 +580,7 @@ public function testBug7555(): void [ 'Strict comparison using === between 2 and 2 will always evaluate to true.', 11, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], ]); } @@ -597,8 +616,16 @@ public function testBug6181(): void public function testBug2851b(): void { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->checkAlwaysTrueStrictComparison = true; - $this->analyse([__DIR__ . '/data/bug-2851b.php'], []); + $this->analyse([__DIR__ . '/data/bug-2851b.php'], [ + [ + 'Strict comparison using === between 0 and 0 will always evaluate to true.', + 21, + $tipText, + ], + ]); } public function testBug8158(): void @@ -607,4 +634,386 @@ public function testBug8158(): void $this->analyse([__DIR__ . '/data/bug-8158.php'], []); } + public function testBug8485(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-8485.php'], [ + [ + 'Strict comparison using === between Bug8485\E::c and Bug8485\E::c will always evaluate to true.', + 19, + 'Use match expression instead. PHPStan will report unhandled enum cases.', + ], + [ + 'Strict comparison using === between Bug8485\F::c and Bug8485\E::c will always evaluate to false.', + 24, + ], + [ + 'Strict comparison using === between Bug8485\F::c and Bug8485\E::c will always evaluate to false.', + 29, + ], + [ + 'Strict comparison using === between Bug8485\F and Bug8485\E will always evaluate to false.', + 36, + ], + [ + 'Strict comparison using === between Bug8485\F and Bug8485\E::c will always evaluate to false.', + 41, + ], + [ + 'Strict comparison using === between Bug8485\FooEnum::C and Bug8485\FooEnum::C will always evaluate to true.', + 67, + "• Remove remaining cases below this one and this error will disappear too.\n• Use match expression instead. PHPStan will report unhandled enum cases.", + ], + [ + 'Strict comparison using === between Bug8485\FooEnum::C and Bug8485\FooEnum::C will always evaluate to true.', + 74, + "• Remove remaining cases below this one and this error will disappear too.\n• Use match expression instead. PHPStan will report unhandled enum cases.", + ], + ]); + } + + public function testBug8516(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-8516.php'], []); + } + + public function testPhpUnitIntegration(): void + { + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/../../Analyser/data/phpunit-integration.php'], []); + } + + public function testBug8586(): void + { + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-8586.php'], []); + } + + public function testBug4242(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-4242.php'], []); + } + + public function testBug3633(): void + { + $this->checkAlwaysTrueStrictComparison = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/bug-3633.php'], [ + [ + 'Strict comparison using === between class-string and \'Bug3633\\\OtherClass\' will always evaluate to false.', + 37, + $tipText, + ], + [ + 'Strict comparison using === between \'Bug3633\\\HelloWorld\' and \'Bug3633\\\HelloWorld\' will always evaluate to true.', + 41, + $tipText, + ], + [ + 'Strict comparison using === between \'Bug3633\\\HelloWorld\' and \'Bug3633\\\OtherClass\' will always evaluate to false.', + 44, + ], + [ + 'Strict comparison using === between class-string and \'Bug3633\\\HelloWorld\' will always evaluate to false.', + 64, + $tipText, + ], + [ + 'Strict comparison using === between \'Bug3633\\\OtherClass\' and \'Bug3633\\\HelloWorld\' will always evaluate to false.', + 71, + $tipText, + ], + [ + 'Strict comparison using === between \'Bug3633\\\OtherClass\' and \'Bug3633\\\OtherClass\' will always evaluate to true.', + 74, + $tipText, + ], + [ + 'Strict comparison using === between class-string and \'Bug3633\\\HelloWorld\' will always evaluate to false.', + 93, + $tipText, + ], + [ + 'Strict comparison using === between class-string and \'Bug3633\\\OtherClass\' will always evaluate to false.', + 96, + $tipText, + ], + [ + 'Strict comparison using === between \'Bug3633\\\FinalClass\' and \'Bug3633\\\FinalClass\' will always evaluate to true.', + 102, + $tipText, + ], + [ + 'Strict comparison using === between \'Bug3633\\\FinalClass\' and \'Bug3633\\\HelloWorld\' will always evaluate to false.', + 106, + $tipText, + ], + [ + 'Strict comparison using === between \'Bug3633\\\FinalClass\' and \'Bug3633\\\OtherClass\' will always evaluate to false.', + 109, + $tipText, + ], + [ + 'Strict comparison using !== between \'Bug3633\\\FinalClass\' and \'Bug3633\\\FinalClass\' will always evaluate to false.', + 112, + $tipText, + ], + [ + 'Strict comparison using === between \'Bug3633\\\FinalClass\' and \'Bug3633\\\FinalClass\' will always evaluate to true.', + 115, + ], + ]); + } + + public function testLastConditionAlwaysTrue(): void + { + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/strict-comparison-last-condition-always-true.php'], [ + [ + 'Strict comparison using === between \'bar\' and \'bar\' will always evaluate to true.', + 15, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testBug3019(): void + { + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/../../Analyser/data/bug-3019.php'], []); + } + + public function testBug7578(): void + { + $this->checkAlwaysTrueStrictComparison = true; + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-7578.php'], []); + } + + public function testBug6260(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkAlwaysTrueStrictComparison = true; + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-6260.php'], []); + } + + public function testBug8736(): void + { + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-8736.php'], []); + } + + public function dataLastMatchArm(): iterable + { + yield [false, [ + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 36, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + "Strict comparison using === between *NEVER* and 'ccc' will always evaluate to false.", + 38, + ], + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 46, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 62, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 79, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]]; + yield [true, [ + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 17, + ], + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 30, + ], + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 36, + ], + [ + "Strict comparison using === between *NEVER* and 'ccc' will always evaluate to false.", + 38, + ], + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 46, + ], + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 62, + ], + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 75, + ], + [ + "Strict comparison using === between 'bbb' and 'bbb' will always evaluate to true.", + 79, + ], + ]]; + } + + /** + * @dataProvider dataLastMatchArm + * @param list $expectedErrors + */ + public function testLastMatchArm(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkAlwaysTrueStrictComparison = true; + $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; + $this->analyse([__DIR__ . '/data/strict-comparison-last-match-arm.php'], $expectedErrors); + } + + public function testBug8776Part1(): void + { + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-8776-1.php'], []); + } + + public function testBug8776Part2(): void + { + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-8776-2.php'], []); + } + + public function testBug5978(): void + { + if (PHP_VERSION_ID >= 80000) { + $expectedErrors = [ + [ + 'Strict comparison using === between non-empty-string and false will always evaluate to false.', + 7, + ], + [ + 'Strict comparison using === between non-empty-string and null will always evaluate to false.', + 7, + ], + ]; + } else { + $expectedErrors = []; + } + + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-5978.php'], $expectedErrors); + } + + public function testBug9104(): void + { + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-9104.php'], [ + [ + 'Strict comparison using === between int<1, max> and 0 will always evaluate to false.', + 12, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + + public function testEnumTips(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/strict-comparison-enum-tips.php'], [ + [ + 'Strict comparison using === between StrictComparisonEnumTips\SomeEnum::Two and StrictComparisonEnumTips\SomeEnum::Two will always evaluate to true.', + 52, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testBug9142(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-9142.php'], [ + [ + 'Strict comparison using === between $this(Bug9142\MyEnum) and Bug9142\MyEnum::Three will always evaluate to false.', + 18, + ], + [ + 'Strict comparison using === between Bug9142\MyEnum and Bug9142\MyEnum::Three will always evaluate to false.', + 31, + ], + ]); + } + + public function testBug4061(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-4061.php'], []); + } + + public function testBug9723(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-9723.php'], []); + } + + public function testBug9723b(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-9723b.php'], []); + } + + public function testBug8366(): void + { + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/../../Analyser/data/bug-8366.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php index d54153136c..71fce9241d 100644 --- a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php @@ -22,8 +22,10 @@ protected function getRule(): Rule $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, ); @@ -95,16 +97,13 @@ public function testReportPhpDoc(): void public function testBug7580(): void { $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-7580.php'], [ - [ - 'Ternary operator condition is always false.', - 6, - ], - [ - 'Ternary operator condition is always true.', - 9, - ], - ]); + $this->analyse([__DIR__ . '/data/bug-7580.php'], []); + } + + public function testBug3370(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-3370.php'], []); } } diff --git a/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php b/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php index 0792b75591..7843308281 100644 --- a/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php @@ -22,10 +22,13 @@ protected function getRule(): Rule $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, + false, ); } @@ -121,4 +124,16 @@ public function testBug8076(): void $this->analyse([__DIR__ . '/data/bug-8076.php'], []); } + public function testBug8562(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8562.php'], []); + } + + public function testBug7491(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7491.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php b/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php index abfca34a8f..317f5f98aa 100644 --- a/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php @@ -22,10 +22,13 @@ protected function getRule(): Rule $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, + false, ); } @@ -61,6 +64,10 @@ public function testDoNotReportPhpDoc(): void 'Else branch is unreachable because ternary operator condition is always true.', 17, ], + [ + 'Else branch is unreachable because ternary operator condition is always true.', + 20, + ], ]); } @@ -85,9 +92,20 @@ public function testReportPhpDoc(): void [ 'Else branch is unreachable because ternary operator condition is always true.', 20, - $tipText, ], ]); } + public function testBug3019(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/data/bug-3019.php'], []); + } + + public function testBug7686(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7686.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php index a85dae7241..42675f9d8a 100644 --- a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php @@ -22,8 +22,10 @@ protected function getRule(): Rule $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, ); diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php index 54c9c1be40..e6f158f91a 100644 --- a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php @@ -22,8 +22,10 @@ protected function getRule(): Rule $this->getTypeSpecifier(), [], $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, + true, ), $this->treatPhpDocTypesAsCertain, ); diff --git a/tests/PHPStan/Rules/Comparison/data/assert-unresolved-generic.php b/tests/PHPStan/Rules/Comparison/data/assert-unresolved-generic.php new file mode 100644 index 0000000000..57be8639e5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/assert-unresolved-generic.php @@ -0,0 +1,27 @@ +foo !== null and $this->bar !== null) { + + } + } + +} + +class StringInIsset +{ + + public function doFoo(string $s, string $t) + { + if (isset($s[1]) and isset($t[1])) { + + } + } + +} + +class IssetBug +{ + + public function doFoo(string $alias, array $options = []) + { + list($name, $p) = explode('.', $alias); + if (isset($options['c']) and !\strpos($options['c'], '\\')) { + // ... + } + + if (!isset($options['c']) and \strpos($p, 'X') === 0) { + // ? + } + } + +} + +class IntegerRangeType +{ + + public function doFoo(int $i, float $f) + { + if ($i < 3 and $i > 5) { // can never happen + } + + if ($f > 0 and $f < 1) { + } + } + +} + +class AndInIfCondition +{ + public function andInIfCondition($mixed, int $i): void + { + if (!$mixed) { + if ($mixed and $i) { + } + if ($i and $mixed) { + } + } + if ($mixed) { + if ($mixed and $i) { + } + if ($i and $mixed) { + } + } + } +} + +function getMaybeArray() : ?array { + if (rand(0, 1)) { return [1, 2, 3]; } + return null; +} + +function bug1924() { + $arr = [ + 'a' => getMaybeArray(), + 'b' => getMaybeArray(), + ]; + + if (isset($arr['a']) and isset($arr['b'])) { + } +} + +class Foo +{ + +} + +class Bar +{ + +} + +interface Lorem +{ + +} + +interface Ipsum +{ + +} diff --git a/tests/PHPStan/Rules/Comparison/data/boolean-logical-or.php b/tests/PHPStan/Rules/Comparison/data/boolean-logical-or.php new file mode 100644 index 0000000000..2373f02a4d --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/boolean-logical-or.php @@ -0,0 +1,89 @@ + $x */ +function doFoo(?ArrayObject $x):void { + $callable1 = [$x, 'count']; + $callable2 = array_reverse($callable1, true); + + var_dump( + is_callable($callable1), + is_callable($callable2) + ); +} + +function doBar():void { + $callable1 = [new ArrayObject([0]), 'count']; + $callable2 = array_reverse($callable1, true); + + var_dump( + is_callable($callable1), + is_callable($callable2) + ); +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-2499.php b/tests/PHPStan/Rules/Comparison/data/bug-2499.php new file mode 100644 index 0000000000..3581c62254 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-2499.php @@ -0,0 +1,11 @@ + 'A', + 'b' => 'B', + 'c' => 'C', + ]; + + public function get($value) + { + return array_keys(self::MAP, $value) ?: [$value]; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-3633.php b/tests/PHPStan/Rules/Comparison/data/bug-3633.php new file mode 100644 index 0000000000..95aedbcf12 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-3633.php @@ -0,0 +1,128 @@ +test(); + } +} + +class OtherClass { + use Foo; + + public function bar($obj): void { + if (get_class($this) === HelloWorld::class) { + echo "OK"; + } + if (get_class($this) === OtherClass::class) { + echo "OK"; + } + + if (get_class() === HelloWorld::class) { + echo "OK"; + } + if (get_class() === OtherClass::class) { + echo "OK"; + } + + if (get_class($obj) === HelloWorld::class) { + echo "OK"; + } + if (get_class($obj) === OtherClass::class) { + echo "OK"; + } + + $this->test(); + } +} + +final class FinalClass { + use Foo; + + public function bar($obj): void { + if (get_class($this) === HelloWorld::class) { + echo "OK"; + } + if (get_class($this) === OtherClass::class) { + echo "OK"; + } + if (get_class($this) !== FinalClass::class) { + echo "OK"; + } + if (get_class($this) === FinalClass::class) { + echo "OK"; + } + + if (get_class() === HelloWorld::class) { + echo "OK"; + } + if (get_class() === OtherClass::class) { + echo "OK"; + } + if (get_class() !== FinalClass::class) { + echo "OK"; + } + if (get_class() === FinalClass::class) { + echo "OK"; + } + + if (get_class($obj) === HelloWorld::class) { + echo "OK"; + } + if (get_class($obj) === OtherClass::class) { + echo "OK"; + } + + $this->test(); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-4004.php b/tests/PHPStan/Rules/Comparison/data/bug-4004.php new file mode 100644 index 0000000000..c6ab715335 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-4004.php @@ -0,0 +1,20 @@ += 8.1 + +namespace Bug4242; + +class Enum +{ + public const TYPE_A = 1; + public const TYPE_B = 2; + public const TYPE_C = 3; + public const TYPE_D = 4; +} + +class Data +{ + private int $type; + private int $someLoad; + public function __construct(int $type) + { + $this->type=$type; + } + public function getType(): int + { + return $this->type; + } + public function someLoad(int $type): self + { + $this->someLoad=$type; + return $this; + } + public function getSomeLoad(): int + { + return $this->someLoad; + } +} + +class HelloWorld +{ + public function case1(): void + { + $data=(new Data(Enum::TYPE_A)); + if($data->getType()===Enum::TYPE_A){ + $data->someLoad(4); + }elseif(\in_array($data->getType(), [7,8,9,100], true)){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_B){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_C){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_D){ + $data->someLoad(6); + }else{ + return; + } + + + if($data->getType()===Enum::TYPE_A){ + $data->someLoad(4); + }elseif(\in_array($data->getType(), [7,8,9,100], true)){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_B){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_C){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_D){ // expected to work without an error + $data->someLoad(6); + } + + } + + public function case2(): void + { + $data=(new Data(Enum::TYPE_A)); + if($data->getType()===Enum::TYPE_A){ + $data->someLoad(4); + }elseif(\in_array($data->getType(), [7,8,9,100], true)){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_B){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_C){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_D){ + $data->someLoad(6); + }else{ + return; + } + + // code above is the same as in case1. code bellow with sorted elseif's + if($data->getType()===Enum::TYPE_A){ + $data->someLoad(4); + }elseif($data->getType()===Enum::TYPE_B){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_C){ + $data->someLoad(6); + }elseif($data->getType()===Enum::TYPE_D){ + $data->someLoad(6); + }elseif(\in_array($data->getType(), [7,8,9,100], true)){ + $data->someLoad(6); + } + + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-4302.php b/tests/PHPStan/Rules/Comparison/data/bug-4302.php new file mode 100644 index 0000000000..825bec72b4 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-4302.php @@ -0,0 +1,14 @@ += 8.1 + +namespace Bug4451; + +class HelloWorld +{ + public function sayHello(): int + { + $verified = fn(): bool => rand() === 1; + + return match([$verified(), $verified()]) { + [true, true] => 1, + [true, false] => 2, + [false, true] => 3, + [false, false] => 4, + }; + + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-4969.php b/tests/PHPStan/Rules/Comparison/data/bug-4969.php new file mode 100644 index 0000000000..658c311cf0 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-4969.php @@ -0,0 +1,19 @@ + $requiredRatio) { + + } elseif ($srcRatio < $requiredRatio) { + + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5365.php b/tests/PHPStan/Rules/Comparison/data/bug-5365.php new file mode 100644 index 0000000000..54f2263446 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5365.php @@ -0,0 +1,22 @@ +\d+)$#i'; + $subject = 'C 1234567890'; + + $found = (bool)preg_match( $pattern, $subject, $matches ) && isset( $matches['productId'] ); + assertType('bool', $found); +}; + +function (): void { + $matches = []; + $pattern = '#^C\s+(?\d+)$#i'; + $subject = 'C 1234567890'; + + assertType('bool', preg_match( $pattern, $subject, $matches ) ? isset( $matches['productId'] ) : false); +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5695.php b/tests/PHPStan/Rules/Comparison/data/bug-5695.php new file mode 100644 index 0000000000..17d33153a5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5695.php @@ -0,0 +1,21 @@ + 'up' }; }; + +function (): void { + $result = match(rand(1, 3)) { + 1 => 'foo', + 2 => 'bar', + 3 => 'baz' + }; +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6260.php b/tests/PHPStan/Rules/Comparison/data/bug-6260.php new file mode 100644 index 0000000000..dd5d1eb1f0 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6260.php @@ -0,0 +1,14 @@ += 8.0 + +namespace Bug6260; + +class Foo{ + public function __construct( + /** @var non-empty-array */ + private array $array + ){ + if(count($array) === 0){ + throw new \InvalidArgumentException(); + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6407.php b/tests/PHPStan/Rules/Comparison/data/bug-6407.php new file mode 100644 index 0000000000..99f4ead9b3 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6407.php @@ -0,0 +1,65 @@ += 8.0 + +namespace Bug6407; + +class BookEditPacket +{ + public const TYPE_REPLACE_PAGE = 0; + public const TYPE_ADD_PAGE = 1; + public const TYPE_DELETE_PAGE = 2; + public const TYPE_SWAP_PAGES = 3; + public const TYPE_SIGN_BOOK = 4; + + public int $type; +} + + +class PlayerEditBookEvent +{ + public const ACTION_REPLACE_PAGE = 0; + public const ACTION_ADD_PAGE = 1; + public const ACTION_DELETE_PAGE = 2; + public const ACTION_SWAP_PAGES = 3; + public const ACTION_SIGN_BOOK = 4; +} + +class HelloWorld +{ + private BookEditPacket $packet; + + private function iAmImpure(): void + { + $this->packet->type = 999; + } + + public function sayHello(BookEditPacket $packet): bool + { + $this->packet = $packet; + switch ($packet->type) { + case BookEditPacket::TYPE_REPLACE_PAGE: + $this->iAmImpure(); + break; + case BookEditPacket::TYPE_ADD_PAGE: + break; + case BookEditPacket::TYPE_DELETE_PAGE: + break; + case BookEditPacket::TYPE_SWAP_PAGES: + break; + case BookEditPacket::TYPE_SIGN_BOOK: + break; + default: + return false; + } + + //for redundancy, in case of protocol changes, we don't want to pass these directly + $action = match ($packet->type) { + BookEditPacket::TYPE_REPLACE_PAGE => PlayerEditBookEvent::ACTION_REPLACE_PAGE, + BookEditPacket::TYPE_ADD_PAGE => PlayerEditBookEvent::ACTION_ADD_PAGE, + BookEditPacket::TYPE_DELETE_PAGE => PlayerEditBookEvent::ACTION_DELETE_PAGE, + BookEditPacket::TYPE_SWAP_PAGES => PlayerEditBookEvent::ACTION_SWAP_PAGES, + BookEditPacket::TYPE_SIGN_BOOK => PlayerEditBookEvent::ACTION_SIGN_BOOK, + default => throw new \Error("We already filtered unknown types in the switch above") + }; + return true; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6467.php b/tests/PHPStan/Rules/Comparison/data/bug-6467.php new file mode 100644 index 0000000000..722d952afd --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6467.php @@ -0,0 +1,16 @@ + $null) { + $success = $values[$index] < $expected; + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6551.php b/tests/PHPStan/Rules/Comparison/data/bug-6551.php new file mode 100644 index 0000000000..3b3e9574a6 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6551.php @@ -0,0 +1,63 @@ + 12, + 'rasd' => 13, + 'c34' => 15, + ]; + + foreach ($data as $key => $value) { + $match = []; + if (false === preg_match('/^c(\d+)$/', $key, $match) || empty($match)) { + continue; + } + var_dump($key); + var_dump($value); + } +}; + +function (): void { + $data = [ + 'c1' => 12, + 'rasd' => 13, + 'c34' => 15, + ]; + + foreach ($data as $key => $value) { + if (false === preg_match('/^c(\d+)$/', $key, $match) || empty($match)) { + continue; + } + var_dump($key); + var_dump($value); + } +}; + +function (): void { + $data = [ + 'c1' => 12, + 'rasd' => 13, + 'c34' => 15, + ]; + + foreach ($data as $key => $value) { + $match = []; + assertType('bool', preg_match('/^c(\d+)$/', $key, $match) || empty($match)); + } +}; + +function (): void { + $data = [ + 'c1' => 12, + 'rasd' => 13, + 'c34' => 15, + ]; + + foreach ($data as $key => $value) { + assertType('bool', preg_match('/^c(\d+)$/', $key, $match) || empty($match)); + } +}; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6776.php b/tests/PHPStan/Rules/Comparison/data/bug-6776.php new file mode 100644 index 0000000000..f05a9ad1ee --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6776.php @@ -0,0 +1,16 @@ + 1, 'b' => 2]; + /** @var array * */ + $array2 = ['a' => 1]; + + $check = function (string $key) use (&$array1, &$array2): bool { + if (!isset($array1[$key], $array2[$key])) { + return false; + } + // ... more conditions here ... + return true; + }; + + if ($check('a')) { + // ... + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6938.php b/tests/PHPStan/Rules/Comparison/data/bug-6938.php new file mode 100644 index 0000000000..1f14920582 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6938.php @@ -0,0 +1,22 @@ + $value + */ +function myFunction($value): void +{ + if (is_string($value)) { + $value = [$value]; + } elseif (is_array($value)) { + // If given an array, filter out anything that isn't a string. + $value = array_filter($value, 'is_string'); + } + + if (! is_array($value)) { + throw new \DomainException('Invalid argument type for $value'); + } + + // Now we know that $value is either a string or an array of strings. +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7075.php b/tests/PHPStan/Rules/Comparison/data/bug-7075.php new file mode 100644 index 0000000000..b4311d4303 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7075.php @@ -0,0 +1,18 @@ + $b */ +function foo(int $b): void { + if ($b > 100) throw new \Exception("bad"); + print "ok"; +} + +/** + * @param int<1,max> $number + */ +function foo2(int $number): void { + if ($number < 1) { + throw new \Exception('Number cannot be less than 1'); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7271.php b/tests/PHPStan/Rules/Comparison/data/bug-7270.php similarity index 94% rename from tests/PHPStan/Rules/Comparison/data/bug-7271.php rename to tests/PHPStan/Rules/Comparison/data/bug-7270.php index 91358218dd..746659180f 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-7271.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-7270.php @@ -1,6 +1,6 @@ $array + */ + public function foo(array $array): void + { + if ([] === $array) { + throw new \InvalidArgumentException(); + } + } + + /** + * @param non-empty-array $array + */ + public function foo2(array $array): void + { + if (0 === count($array)) { + throw new \InvalidArgumentException(); + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7686.php b/tests/PHPStan/Rules/Comparison/data/bug-7686.php new file mode 100644 index 0000000000..be05325779 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7686.php @@ -0,0 +1,25 @@ + $input + * @return array<'return'|int, string> + */ + public static function test(array $input): array + { + $output = []; + foreach($input as $match) { + if (array_key_exists($match['name'], $output) == false) { + $output[$match['name']] = ''; + } + if (($match['type'] === '') || (in_array($match['type'], explode('|', $output[$match['name']]), true) === true)) { + continue; + } + $output[$match['name']] = ($output[$match['name']] === '' ? $match['type'] : $output[$match['name']] . '|' . $match['type']); + } + return $output; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8169.php b/tests/PHPStan/Rules/Comparison/data/bug-8169.php new file mode 100644 index 0000000000..a6c4d33025 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8169.php @@ -0,0 +1,36 @@ +assertString($foo); + assertType('string', $foo); + $this->assertString($foo); // should report as always evaluating to true? + assertType('string', $foo); + } + + public function test2(string $foo): void + { + assertType('string', $foo); + $this->assertString($foo); // should report as always evaluating to true? + assertType('string', $foo); + } + + public function test3(int $foo): void + { + assertType('int', $foo); + $this->assertString($foo); // should report as always evaluating to false? + assertType('*NEVER*', $foo); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8240.php b/tests/PHPStan/Rules/Comparison/data/bug-8240.php new file mode 100644 index 0000000000..d54f6244b6 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8240.php @@ -0,0 +1,31 @@ += 8.1 + +namespace Bug8240; + +enum Foo +{ + case BAR; +} + +function doFoo(Foo $foo): int +{ + return match ($foo) { + Foo::BAR => 5, + default => throw new \Exception('This will not be executed') + }; +} + +enum Foo2 +{ + case BAR; + case BAZ; +} + +function doFoo2(Foo2 $foo): int +{ + return match ($foo) { + Foo2::BAR => 5, + Foo2::BAZ => 15, + default => throw new \Exception('This will not be executed') + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8277.php b/tests/PHPStan/Rules/Comparison/data/bug-8277.php new file mode 100644 index 0000000000..3be373d818 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8277.php @@ -0,0 +1,36 @@ + $stream + * @param positive-int $width + * + * @return Generator + */ +function swindow(iterable $stream, int $width): Generator +{ + $window = []; + foreach ($stream as $value) { + $window[] = $value; + $count = count($window); + + assertType('int<1, max>', $count); + + switch (true) { + case $count > $width: + array_shift($window); + // no break + case $count === $width: + yield $window; + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8474.php b/tests/PHPStan/Rules/Comparison/data/bug-8474.php new file mode 100644 index 0000000000..d6510c8ad6 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8474.php @@ -0,0 +1,32 @@ += 7.4 + +namespace Bug8474; + +class World {} +class HelloWorld extends World { + public string $hello = 'world'; +} + +function hello(World $world): bool { + return property_exists($world, 'hello'); +} + +class Alpha +{ + public function __construct() + { + if (property_exists($this, 'data')) { + $this->data = 'Hello'; + } + } +} + +class Beta extends Alpha +{ + /** @var string|null */ + public $data = null; +} + +class Delta extends Alpha +{ +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8485.php b/tests/PHPStan/Rules/Comparison/data/bug-8485.php new file mode 100644 index 0000000000..ce7cc5fa3c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8485.php @@ -0,0 +1,78 @@ += 8.1 + +namespace Bug8485; + +use function PHPStan\Testing\assertType; + +enum E { + case c; +} + +enum F { + case c; +} + +function shouldError():void { + $e = E::c; + $f = F::c; + + if ($e === E::c) { + } + if ($e == E::c) { + } + + if ($f === $e) { + } + if ($f == $e) { + } + + if ($f === E::c) { + } + if ($f == E::c) { + } +} + +function allGood(E $e, F $f):void { + if ($f === $e) { + } + if ($f == $e) { + } + + if ($f === E::c) { + } + if ($f == E::c) { + } +} + +enum FooEnum +{ + case A; + case B; + case C; +} +function dooFoo(FooEnum $s):void { + if ($s === FooEnum::A) { + } elseif ($s === FooEnum::B) { + } elseif ($s === FooEnum::C) { + } + + if ($s === FooEnum::A) { + } elseif ($s === FooEnum::B) { + } else { + assertType('Bug8485\FooEnum::C', $s); + } + + if ($s === FooEnum::A) { + } elseif ($s === FooEnum::B) { + } elseif ($s === FooEnum::C) { + } else { + assertType('*NEVER*', $s); + } + + if ($s === FooEnum::A) { + } elseif ($s === FooEnum::B) { + } elseif ($s === FooEnum::C) { + } elseif (rand(0, 1)) { + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8516.php b/tests/PHPStan/Rules/Comparison/data/bug-8516.php new file mode 100644 index 0000000000..ca0202d6be --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8516.php @@ -0,0 +1,18 @@ += 7.4 + +namespace Bug8516; + +function validate($value, array $options = null): bool +{ + if (is_int($value)) { + $options ??= ['options' => ['min_range' => 0]]; + if (filter_var($value, FILTER_VALIDATE_INT, $options) === false) { + return false; + } + // ... + } + if (is_string($value)) { + // ... + } + return true; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8536.php b/tests/PHPStan/Rules/Comparison/data/bug-8536.php new file mode 100644 index 0000000000..9ac6e588d9 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8536.php @@ -0,0 +1,23 @@ += 8.1 + +namespace Bug8536; + +final class A { + public function __construct(public readonly string $id) {} +} +final class B { + public function __construct(public readonly string $name) {} +} + +class Foo +{ + + public function getValue(A|B $obj): string + { + return match(get_class($obj)) { + A::class => $obj->id, + B::class => $obj->name, + }; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8562.php b/tests/PHPStan/Rules/Comparison/data/bug-8562.php new file mode 100644 index 0000000000..9deeaa6a0d --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8562.php @@ -0,0 +1,18 @@ + $a + */ +function a(array $a): void { + $l = (string) array_key_last($a); + $s = substr($l, 0, 2); + if ($s === '') { + ; + } else { + var_dump($s); + } +} + + diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8586.php b/tests/PHPStan/Rules/Comparison/data/bug-8586.php new file mode 100644 index 0000000000..2b004a9f56 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8586.php @@ -0,0 +1,38 @@ +getString() === null); + $em->refreshFromAnnotation($foo); + \assert($foo->getString() !== null); + } + + public function sayHello2(Foo $foo, EntityManager $em): void + { + \assert($foo->getString() === null); + $em->refresh($foo); + \assert($foo->getString() !== null); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8614.php b/tests/PHPStan/Rules/Comparison/data/bug-8614.php new file mode 100644 index 0000000000..6eedd79f2a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8614.php @@ -0,0 +1,14 @@ += 8.1 + +namespace Bug8614; + +/** + * @param int|float|bool|string|object|mixed[] $value + */ +function stringify(int|float|bool|string|object|array $value): string +{ + return match (gettype($value)) { + 'integer', 'double', 'boolean', 'string' => (string) $value, + 'object', 'array' => var_export($value, true), + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8643.php b/tests/PHPStan/Rules/Comparison/data/bug-8643.php new file mode 100644 index 0000000000..722c4ef4a3 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8643.php @@ -0,0 +1,19 @@ += 7.4 + +namespace Bug8727; + +abstract class Foo +{ + abstract public function hello(): void; + + protected function message(): string + { + if (property_exists($this, 'lala')) { + return 'Lala!'; + } + + return 'Hello!'; + } +} + +class Bar extends Foo { + protected bool $lala = true; + + public function hello(): void + { + echo $this->message(); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8736.php b/tests/PHPStan/Rules/Comparison/data/bug-8736.php new file mode 100644 index 0000000000..ac6e79e59c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8736.php @@ -0,0 +1,15 @@ + ['min_range' => $minimum]]; + $filtered = filter_var($value, FILTER_VALIDATE_INT, $options); + if ($filtered === false) { + return compact('minimum', 'value'); + } + } + if (isset($schema['maximum'])) { + $maximum = $schema['maximum']; + if (filter_var($maximum, FILTER_VALIDATE_INT) === false) { + throw new LogicException('`maximum` must be `int`'); + } + $options = ['options' => ['max_range' => $maximum]]; + /** @var int|false */ + $filtered = filter_var($value, FILTER_VALIDATE_INT, $options); + if ($filtered === false) { + return compact('maximum', 'value'); + } + } + // ... + } + if (is_string($value)) { + // ... + } + return true; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8776-2.php b/tests/PHPStan/Rules/Comparison/data/bug-8776-2.php new file mode 100644 index 0000000000..50b69fb7cd --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8776-2.php @@ -0,0 +1,24 @@ + ['min_range' => $minimum]]; + $filtered = filter_var($value, FILTER_VALIDATE_INT, $options); + if ($filtered === false) { + return; + } + } + + public function sayWorld(int $value): void + { + $options = ['options' => ['min_range' => 17]]; + $filtered = filter_var($value, FILTER_VALIDATE_INT, $options); + if ($filtered === false) { + return; + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8797.php b/tests/PHPStan/Rules/Comparison/data/bug-8797.php new file mode 100644 index 0000000000..853c5ed347 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8797.php @@ -0,0 +1,13 @@ += 8.0 + +namespace Bug8900; + +class Foo +{ + + public function doFoo(): void + { + $test_array = []; + for($index = 0; $index++; $index < random_int(1,100)) { + $test_array[] = 'entry'; + } + + foreach($test_array as $key => $value) { + $key_mod_4 = match($key % 4) { + 0 => '0', + 1 => '1', + 2 => '2', + 3 => '3', + }; + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8932.php b/tests/PHPStan/Rules/Comparison/data/bug-8932.php new file mode 100644 index 0000000000..b52a425157 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8932.php @@ -0,0 +1,18 @@ += 8.0 + +namespace Bug8932; + +class HelloWorld +{ + /** + * @param 'A'|'B' $string + */ + public function sayHello(string $string): int + { + return match ($string) { + 'A' => 1, + 'B' => 2, + default => throw new \LogicException(), + }; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8937.php b/tests/PHPStan/Rules/Comparison/data/bug-8937.php new file mode 100644 index 0000000000..161fdc59e7 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8937.php @@ -0,0 +1,26 @@ += 8.0 + +namespace Bug8937; + +/** + * @param 'A'|'B' $string + */ +function sayHello(string $string): int +{ + return match ($string) { + 'A' => 1, + 'B' => 2, + }; +} + +/** + * @param array|string $v + */ +function foo(array|string $v): string +{ + return match(true) { + is_string($v) => 'string', + is_array($v) && \array_is_list($v) => 'list', + is_array($v) => 'array', + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8938.php b/tests/PHPStan/Rules/Comparison/data/bug-8938.php new file mode 100644 index 0000000000..8d354b0d81 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8938.php @@ -0,0 +1,17 @@ + 0) { + $firstChar = substr($data, 0, 1); + $data = substr($data, 1); + $returnValue = $returnValue . $firstChar; + } + return $returnValue; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9007.php b/tests/PHPStan/Rules/Comparison/data/bug-9007.php new file mode 100644 index 0000000000..ae2fa03d6c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9007.php @@ -0,0 +1,20 @@ += 8.1 + +namespace Bug9007; + +use function PHPStan\Testing\assertType; + +enum Country: string { + case Usa = 'USA'; + case Canada = 'CAN'; + case Mexico = 'MEX'; +} + +function doStuff(string $countryString): int { + assertType(Country::class, Country::from($countryString)); + return match (Country::from($countryString)) { + Country::Usa => 1, + Country::Canada => 2, + Country::Mexico => 3, + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9104.php b/tests/PHPStan/Rules/Comparison/data/bug-9104.php new file mode 100644 index 0000000000..e701ca020b --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9104.php @@ -0,0 +1,18 @@ + $list + */ + public function getFirst(array $list): int + { + if (count($list) === 0) { + throw new \LogicException('empty array'); + } + + return $list[0]; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9142.php b/tests/PHPStan/Rules/Comparison/data/bug-9142.php new file mode 100644 index 0000000000..9c149242d7 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9142.php @@ -0,0 +1,38 @@ += 8.1 + +namespace Bug9142; + +enum MyEnum: string +{ + + case One = 'one'; + case Two = 'two'; + case Three = 'three'; + + public function thisTypeWithSubtractedEnumCase(): int + { + if ($this === self::Three) { + return -1; + } + + if ($this === self::Three) { + return 0; + } + + return 1; + } + + public function enumTypeWithSubtractedEnumCase(self $self): int + { + if ($self === self::Three) { + return -1; + } + + if ($self === self::Three) { + return 0; + } + + return 1; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9357.php b/tests/PHPStan/Rules/Comparison/data/bug-9357.php new file mode 100644 index 0000000000..c0bae16688 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9357.php @@ -0,0 +1,23 @@ += 8.1 + +namespace Bug9357; + +enum MyEnum: string { + case A = 'a'; + case B = 'b'; +} + +class My { + /** @phpstan-impure */ + public function getType(): MyEnum { + echo "called!"; + return rand() > 0.5 ? MyEnum::A : MyEnum::B; + } +} + +function test(My $m): void { + echo match ($m->getType()) { + MyEnum::A => 1, + MyEnum::B => 2, + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9457.php b/tests/PHPStan/Rules/Comparison/data/bug-9457.php new file mode 100644 index 0000000000..f3a456c14f --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9457.php @@ -0,0 +1,31 @@ += 8.1 + +namespace Bug9457; + +class Randomizer { + function bool(): bool { + return rand(0, 1) === 1; + } + + public function doFoo(?self $randomizer): void + { + // Correct + echo match ($randomizer?->bool()) { + true => 'true', + false => 'false', + null => 'null', + }; + + // Correct + echo match ($randomizer?->bool()) { + true => 'true', + false, null => 'false or null', + }; + + // Unexpected error + echo match ($randomizer?->bool()) { + false => 'false', + true, null => 'true or null', + }; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9499.php b/tests/PHPStan/Rules/Comparison/data/bug-9499.php new file mode 100644 index 0000000000..0a833ad59d --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9499.php @@ -0,0 +1,52 @@ += 8.1 + +namespace Bug9499; + +use function PHPStan\Testing\assertType; + +enum FooEnum +{ + case A; + case B; + case C; + case D; +} + +class Foo +{ + public function __construct(public readonly FooEnum $f) + { + } +} + +function test(FooEnum $f, Foo $foo): void +{ + $arr = ['f' => $f]; + match ($arr['f']) { + FooEnum::A, FooEnum::B => match ($arr['f']) { + FooEnum::A => 'a', + FooEnum::B => 'b', + }, + default => '', + }; + match ($foo->f) { + FooEnum::A, FooEnum::B => match ($foo->f) { + FooEnum::A => 'a', + FooEnum::B => 'b', + }, + default => '', + }; +} + +function test2(FooEnum $f, Foo $foo): void +{ + $arr = ['f' => $f]; + match ($arr['f']) { + FooEnum::A, FooEnum::B => assertType(FooEnum::class . '::A|' . FooEnum::class . '::B', $arr['f']), + default => '', + }; + match ($foo->f) { + FooEnum::A, FooEnum::B => assertType(FooEnum::class . '::A|' . FooEnum::class . '::B', $foo->f), + default => '', + }; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9723.php b/tests/PHPStan/Rules/Comparison/data/bug-9723.php new file mode 100644 index 0000000000..eba7197793 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9723.php @@ -0,0 +1,39 @@ += 8.1 + +namespace Bug9723; + +enum State : int +{ + case StateZero = 0; + case StateOne = 1; +} + +function doFoo() { + $state = rand(0,5); + +// First time checking +// $state is 0|1|2|3|4|5 + if ( $state === State::StateZero->value ) + { + echo "No phpstan errors so far!"; + } + + switch ( $state ) + { + case State::StateZero->value: + case State::StateOne->value: + break; + default: + throw new Exception("Error"); + } + +// Second time checking +// $state is State::StateZero->value|State::StateOne->value +// ... or equivalently, $state is 0|1 +// ... but phpstan thinks $state is definitely 0 + if ( $state === State::StateZero->value ) + { + echo "What's changed?"; + } +} + diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9723b.php b/tests/PHPStan/Rules/Comparison/data/bug-9723b.php new file mode 100644 index 0000000000..acbd20b08e --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9723b.php @@ -0,0 +1,48 @@ += 8.1 + +namespace Bug9723b; + +enum State : int +{ + case StateZero = 0; + case StateOne = 1; + case StateTwo = 2; +} + +function doFoo() { + $state = rand(0,5); + +// First time checking +// $state is 0|1|2|3|4|5 + if ( + $state === State::StateZero->value + || $state === State::StateTwo->value + ) + { + echo "No phpstan errors so far!"; + } + + switch ( $state ) + { + case State::StateZero->value: + case State::StateOne->value: + case State::StateTwo->value: + break; + default: + throw new Exception("Error"); + } + +// Second time checking +// $state is State::StateZero->value|State::StateOne->value|State::StateTwo->value +// ... or equivalently, $state is 0|1|2 +// ... but phpstan thinks $state is definitely 0 +// ... and that is is being compared against 0 and 1, not 0 and 2??? + if ( + $state === State::StateZero->value + || $state === State::StateTwo->value + ) + { + echo "What's changed?"; + } +} + diff --git a/tests/PHPStan/Rules/Comparison/data/bug-unhandled-true-with-complex-condition.php b/tests/PHPStan/Rules/Comparison/data/bug-unhandled-true-with-complex-condition.php new file mode 100644 index 0000000000..60e777703c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-unhandled-true-with-complex-condition.php @@ -0,0 +1,34 @@ += 8.1 + +namespace MatchUnhandledTrueWithComplexCondition; + +enum Bar +{ + + case ONE; + case TWO; + case THREE; + +} + +class Foo +{ + + public Bar $type; + + public function getRand(): int + { + return rand(0, 10); + } + + public function getPriority(): int + { + return match (true) { + $this->type === Bar::ONE => 0, + $this->type === Bar::TWO && $this->getRand() !== 8 => 1, + $this->type === BAR::THREE => 2, + $this->type === BAR::TWO => 3, + }; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/check-type-function-call-not-phpdoc.php b/tests/PHPStan/Rules/Comparison/data/check-type-function-call-not-phpdoc.php index 3321e8929b..0d248e6c8e 100644 --- a/tests/PHPStan/Rules/Comparison/data/check-type-function-call-not-phpdoc.php +++ b/tests/PHPStan/Rules/Comparison/data/check-type-function-call-not-phpdoc.php @@ -21,4 +21,14 @@ public function doFoo( } } + /** @param array $strings */ + public function checkInArray(int $i, array $strings): void + { + if (in_array($i, $strings, true)) { + } + + if (in_array(1, $strings, true)) { + } + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php b/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php index 3b9dee70db..d9fb0b0845 100644 --- a/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php +++ b/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php @@ -862,3 +862,108 @@ public function doFoo(int $i, array $is): void } } + +/** + * @phpstan-assert-if-true int $value + */ +function testIsInt(mixed $value): bool +{ + return is_int($value); +} + +function (int $int) { + if (testIsInt($int)) { + + } +}; + +class ConditionalAlwaysTrue +{ + public function sayHello(?int $date): void + { + if ($date === null) { + } elseif (is_int($date)) { // always-true should not be reported because last condition + } + + if ($date === null) { + } elseif (is_int($date)) { // always-true should be reported, because another condition below + } elseif (rand(0,1)) { + } + } +} + +class InArray3 +{ + /** + * @param non-empty-array $nonEmptyInts + * @param array $strings + */ + public function doFoo(int $i, string $s, array $nonEmptyInts, array $strings): void + { + if (in_array($i, $strings)) { + } + + if (in_array($i, $strings, false)) { + } + + if (in_array(5, $strings)) { + } + + if (in_array(5, $strings, false)) { + } + + if (in_array($s, $nonEmptyInts)) { + } + + if (in_array($s, $nonEmptyInts, false)) { + } + + if (in_array('5', $nonEmptyInts)) { + } + + if (in_array('5', $nonEmptyInts, false)) { + } + + if (in_array(1, $strings, true)) { + } + } +} + +function checkSuperGlobals(): void +{ + foreach ($GLOBALS as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_SERVER as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_GET as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_POST as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_FILES as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_COOKIE as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_SESSION as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_REQUEST as $k => $v) { + if (is_int($k)) {} + } + + foreach ($_ENV as $k => $v) { + if (is_int($k)) {} + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/docblock-assert-equality.php b/tests/PHPStan/Rules/Comparison/data/docblock-assert-equality.php new file mode 100644 index 0000000000..761ac48b47 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/docblock-assert-equality.php @@ -0,0 +1,47 @@ +isInt($date)) { // always-true should not be reported because last condition + } + + if ($date === null) { + } elseif ($this->isInt($date)) { // always-true should be reported, because another condition below + } elseif (rand(0,1)) { + } + } + + /** + * @phpstan-assert-if-true int $value + */ + public function isInt($value): bool { + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/impossible-method-exists-on-generic-class-string.php b/tests/PHPStan/Rules/Comparison/data/impossible-method-exists-on-generic-class-string.php new file mode 100644 index 0000000000..1fcd8d344c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/impossible-method-exists-on-generic-class-string.php @@ -0,0 +1,61 @@ +&literal-string $s + */ + public function sayGenericHello(string $s): void + { + // no erros on non-final class + if (method_exists($s, 'nonExistent')) { + $s->nonExistent(); + $s::nonExistent(); + } + + if (method_exists($s, 'staticAbc')) { + $s::staticAbc(); + $s->staticAbc(); + } + + if (method_exists($s, 'nonStaticAbc')) { + $s::nonStaticAbc(); + $s->nonStaticAbc(); + } + } + + /** + * @param class-string&literal-string $s + */ + public function sayFinalGenericHello(string $s): void + { + if (method_exists($s, 'nonExistent')) { + $s->nonExistent(); + $s::nonExistent(); + } + + if (method_exists($s, 'staticAbc')) { + $s::staticAbc(); + $s->staticAbc(); + } + + if (method_exists($s, 'nonStaticAbc')) { + $s::nonStaticAbc(); + $s->nonStaticAbc(); + } + } +} + +class S { + public static function staticAbc():void {} + + public function nonStaticAbc():void {} +} + +final class FinalS { + public static function staticAbc():void {} + + public function nonStaticAbc():void {} +} diff --git a/tests/PHPStan/Rules/Comparison/data/impossible-method-report-always-true-last-condition.php b/tests/PHPStan/Rules/Comparison/data/impossible-method-report-always-true-last-condition.php new file mode 100644 index 0000000000..6e1b47da7c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/impossible-method-report-always-true-last-condition.php @@ -0,0 +1,32 @@ +assertString($s)) { + + } + } + + public function doBar(string $s) + { + $assertion = new AssertionClass; + if (rand(0, 1)) { + + } elseif ($assertion->assertString($s)) { + + } else { + + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/impossible-static-method-call.php b/tests/PHPStan/Rules/Comparison/data/impossible-static-method-call.php index 01a0125a69..3103493358 100644 --- a/tests/PHPStan/Rules/Comparison/data/impossible-static-method-call.php +++ b/tests/PHPStan/Rules/Comparison/data/impossible-static-method-call.php @@ -53,3 +53,24 @@ public function nullableInt(): ?int } } + +class ConditionalAlwaysTrue +{ + public function sayHello(?int $date): void + { + if ($date === null) { + } elseif (self::isInt($date)) { // always-true should not be reported because last condition + } + + if ($date === null) { + } elseif (self::isInt($date)) { // always-true should be reported, because another condition below + } elseif (rand(0,1)) { + } + } + + /** + * @phpstan-assert-if-true int $value + */ + static public function isInt($value): bool { + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/impossible-static-method-report-always-true-last-condition.php b/tests/PHPStan/Rules/Comparison/data/impossible-static-method-report-always-true-last-condition.php new file mode 100644 index 0000000000..c9beac09ff --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/impossible-static-method-report-always-true-last-condition.php @@ -0,0 +1,30 @@ += 8.1 + +namespace LastMatchArmAlwaysTrue; + +enum Foo { + + case ONE; + case TWO; + + public function doFoo(): void + { + match ($this) { + self::ONE => 'test', + self::TWO => 'two', + }; + } + + public function doBar(): void + { + match ($this) { + self::ONE => 'test', + self::TWO => 'two', + default => 'three', + }; + } + + public function doBaz(): void + { + match ($this) { + self::ONE => 'test', + self::TWO => 'two', + self::TWO => 'three', + }; + } + + public function doBaz2(): void + { + match ($this) { + self::ONE => 'test', + self::TWO => 'two', + self::TWO => 'three', + default => 'four', + }; + } + +} + +enum Bar { + + case ONE; + + public function doFoo(): void + { + match ($this) { + self::ONE => 'test', + }; + } + + public function doBar(): void + { + match ($this) { + self::ONE => 'test', + default => 'test2', + }; + } + + public function doBaz(): void + { + match (1) { + 0 => 'test', + }; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/logical-xor.php b/tests/PHPStan/Rules/Comparison/data/logical-xor.php new file mode 100644 index 0000000000..fe63eb640b --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/logical-xor.php @@ -0,0 +1,27 @@ += 8.1 + +namespace LooseComparisonAgainstEnums; + +enum FooUnitEnum +{ + case A; + case B; +} + +enum FooBackedEnum: string +{ + case A = 'A'; + case B = 'B'; +} + +class InArrayTest +{ + public function enumVsString(FooUnitEnum $u, FooBackedEnum $b): void + { + if (in_array($u, ['A'])) { + } + + if (in_array($u, ['A'], false)) { + } + + if (in_array($b, ['A'])) { + } + + if (in_array($b, ['A'], false)) { + } + + if (in_array(rand() ? $u : $b, ['A'], false)) { + } + } + + public function stringVsEnum(FooUnitEnum $u, FooBackedEnum $b): void + { + if (in_array('A', [$u])) { + } + + if (in_array('A', [$u], false)) { + } + + if (in_array('A', [$b])) { + } + + if (in_array('A', [$b], false)) { + } + + if (in_array('A', [rand() ? $u : $b], false)) { + } + } + + public function enumVsBool(FooUnitEnum $u, FooBackedEnum $b, bool $bl): void + { + if (in_array($u, [$bl])) { + } + + if (in_array($u, [$bl], false)) { + } + + if (in_array($b, [$bl])) { + } + + if (in_array($b, [$bl], false)) { + } + + if (in_array(rand() ? $u : $b, [$bl], false)) { + } + } + + public function boolVsEnum(FooUnitEnum $u, FooBackedEnum $b, bool $bl): void + { + if (in_array($bl, [$u])) { + } + + if (in_array($bl, [$u], false)) { + } + + if (in_array($bl, [$b])) { + } + + if (in_array($bl, [$b], false)) { + } + + if (in_array($bl, [rand() ? $u : $b], false)) { + } + } + + public function null(FooUnitEnum $u, FooBackedEnum $b): void + { + if (in_array($u, [null])) { + } + + if (in_array(null, [$b])) { + } + } + + public function nullableEnum(?FooUnitEnum $u, string $s): void + { + // null == "" + if (in_array($u, [$s])) { + } + + if (in_array($s, [$u])) { + } + } + + /** + * @param array $strings + * @param array $unitEnums + */ + public function dynamicValues(FooUnitEnum $u, string $s, array $strings, array $unitEnums): void + { + if (in_array($u, $unitEnums)) { + } + + if (in_array($u, $unitEnums, false)) { + } + + if (in_array($u, $unitEnums, true)) { + } + + if (in_array($u, $strings)) { + } + + if (in_array($u, $strings, false)) { + } + + if (in_array($u, $strings, true)) { + } + + if (in_array($s, $strings)) { + } + + if (in_array($s, $strings, false)) { + } + + if (in_array($s, $strings, true)) { + } + + if (in_array($s, $unitEnums)) { + } + + if (in_array($s, $unitEnums, false)) { + } + + if (in_array($s, $unitEnums, true)) { + } + } + + /** + * @param non-empty-array $nonEmptyA + * @return void + */ + public function nonEmptyArray(array $nonEmptyA): void + { + if (in_array(FooUnitEnum::B, $nonEmptyA)) { + } + + if (in_array(FooUnitEnum::A, $nonEmptyA)) { + } + + if (in_array(FooUnitEnum::A, $nonEmptyA, false)) { + } + + if (in_array(FooUnitEnum::A, $nonEmptyA, true)) { + } + + if (in_array(FooUnitEnum::B, $nonEmptyA, false)) { + } + + if (in_array(FooUnitEnum::B, $nonEmptyA, true)) { + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/loose-comparison-report-always-true-last-condition.php b/tests/PHPStan/Rules/Comparison/data/loose-comparison-report-always-true-last-condition.php new file mode 100644 index 0000000000..f7e964231c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/loose-comparison-report-always-true-last-condition.php @@ -0,0 +1,28 @@ += 8.1 + +namespace MatchAlwaysTrueLastArm; + +enum Foo +{ + + case FOO; + case BAR; + + public function doFoo(): void + { + match ($this) { + self::FOO => 1, + self::BAR => 2, + }; + } + + public function doBar(): void + { + match ($this) { + self::FOO => 1, + self::BAR => 2, + default => 3, + }; + } + + public function doBaz(): void + { + $a = 'aaa'; + if (rand(0, 1)) { + $a = 'bbb'; + } + + // reported by StrictComparisonOfDifferentTypesRule + match (true) { + $a === 'aaa' => 1, + $a === 'bbb' => 2, + }; + } + + public function doMoreConditionsInLastArm(): void + { + match ($this) { + self::FOO, self::BAR => 1, + }; + + match ($this) { + self::FOO, self::BAR => 1, + default => 2, + }; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/match-expr.php b/tests/PHPStan/Rules/Comparison/data/match-expr.php index fb3c8c28ca..37d5e01c1e 100644 --- a/tests/PHPStan/Rules/Comparison/data/match-expr.php +++ b/tests/PHPStan/Rules/Comparison/data/match-expr.php @@ -63,7 +63,7 @@ public function doFoo(int $i): void }; match (1) { - 1 => 1, // always true - report with strict-rules + 1 => 1, }; match ($i) { @@ -83,13 +83,13 @@ public function doFoo(int $i): void public function doBar(\Exception $e): void { match (true) { - $e instanceof \InvalidArgumentException, $e instanceof \InvalidArgumentException => true, + $e instanceof \InvalidArgumentException, $e instanceof \InvalidArgumentException => true, // reported by ImpossibleInstanceOfRule default => null, }; match (true) { $e instanceof \InvalidArgumentException => true, - $e instanceof \InvalidArgumentException => true, + $e instanceof \InvalidArgumentException => true, // reported by ImpossibleInstanceOfRule }; } @@ -180,3 +180,26 @@ function (): string { -1 => 'down', }; }; + +final class FinalFoo +{ + +} + +final class FinalBar +{ + +} + +class TestGetClass +{ + + public function doMatch(FinalFoo|FinalBar $class): void + { + match (get_class($class)) { + FinalFoo::class => 1, + FinalBar::class => 2, + }; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/number-comparison-treat.php b/tests/PHPStan/Rules/Comparison/data/number-comparison-treat.php new file mode 100644 index 0000000000..46c959aafc --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/number-comparison-treat.php @@ -0,0 +1,22 @@ += 0) { + } + } + + /** @param positive-int $i */ + public function sayHello2(int $i): void + { + if ($i < 0) { + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/property-exists-object-shapes.php b/tests/PHPStan/Rules/Comparison/data/property-exists-object-shapes.php new file mode 100644 index 0000000000..33d55d212a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/property-exists-object-shapes.php @@ -0,0 +1,29 @@ += 8.1 + +namespace StrictComparisonEnumTips; + +enum SomeEnum +{ + + case One; + case Two; + + public function exhaustiveWithSafetyCheck(): int + { + // not reported by this rule at all + if ($this === self::One) { + return -1; + } elseif ($this === self::Two) { + return 0; + } else { + throw new \LogicException('New case added, handling missing'); + } + } + + + public function exhaustiveWithSafetyCheck2(): int + { + // not reported by this rule at all + if ($this === self::One) { + return -1; + } + + if ($this === self::Two) { + return 0; + } + + throw new \LogicException('New case added, handling missing'); + } + + public function exhaustiveWithSafetyCheckInMatchAlready(): int + { + // not reported by this rule at all + return match ($this) { + self::One => -1, + self::Two => 0, + default => throw new \LogicException('New case added, handling missing'), + }; + } + + public function exhaustiveWithSafetyCheckInMatchAlready2(self $self): int + { + return match (true) { + $self === self::One => -1, + $self === self::Two => 0, + default => throw new \LogicException('New case added, handling missing'), + }; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/strict-comparison-last-condition-always-true.php b/tests/PHPStan/Rules/Comparison/data/strict-comparison-last-condition-always-true.php new file mode 100644 index 0000000000..201c9ce0b2 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/strict-comparison-last-condition-always-true.php @@ -0,0 +1,34 @@ += 7.4 + +namespace StrictComparisonLastConditionAlwaysTrue; + +class Foo +{ + + public function doFoo(): void + { + $a = rand() ? 'foo' : 'bar'; + if (rand()) { + + } elseif ($a === 'foo') { + + } elseif ($a === 'bar') { + + } else { + + } + } + + public function doFoo2(): void + { + $a = rand() ? 'foo' : 'bar'; + if (rand()) { + + } elseif ($a === 'foo') { + + } elseif ($a === 'bar') { + + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/strict-comparison-last-match-arm.php b/tests/PHPStan/Rules/Comparison/data/strict-comparison-last-match-arm.php new file mode 100644 index 0000000000..d601468e1c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/strict-comparison-last-match-arm.php @@ -0,0 +1,84 @@ += 8.1 + +namespace StrictComparisonLastMatchArm; + +class Foo +{ + + public function doBaz(): void + { + $a = 'aaa'; + if (rand(0, 1)) { + $a = 'bbb'; + } + + match (true) { + $a === 'aaa' => 1, + $a === 'bbb' => 2, + }; + } + + public function doFoo(): void + { + $a = 'aaa'; + if (rand(0, 1)) { + $a = 'bbb'; + } + + if ($a === 'aaa') { + + } elseif ($a === 'bbb') { + + } + + if ($a === 'aaa') { + + } elseif ($a === 'bbb') { + + } elseif ($a === 'ccc') { + + } else { + + } + + if ($a === 'aaa') { + + } elseif ($a === 'bbb') { + + } else { + + } + } + + public function doIpsum(): void + { + $a = 'aaa'; + if (rand(0, 1)) { + $a = 'bbb'; + } + + match (true) { + $a === 'aaa' => 1, + $a === 'bbb' => 2, + default => new \Exception(), + }; + } + + public function doMoreConditionsInLastArm(): void + { + $a = 'aaa'; + if (rand(0, 1)) { + $a = 'bbb'; + } + + match (true) { + $a === 'aaa', $a === 'bbb' => 1, + }; + + match (true) { + $a === 'aaa', $a === 'bbb' => 1, + default => new \Exception(), + }; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/strict-comparison.php b/tests/PHPStan/Rules/Comparison/data/strict-comparison.php index 8010ddcb9a..9719e9c133 100644 --- a/tests/PHPStan/Rules/Comparison/data/strict-comparison.php +++ b/tests/PHPStan/Rules/Comparison/data/strict-comparison.php @@ -982,3 +982,23 @@ function () { INF !== INF; NAN !== NAN; }; + +class ArrayWithLongStrings2 +{ + + public function doFoo() + { + $array = ['foofoofoofoofoofoofoo','foofoofoofoofoofoofob']; + + foreach ($array as $value) { + if ('foofoofoofoofoofoofoo' === $value) { + echo 'nope'; + } elseif ('foofoofoofoofoofoofob' === $value) { + echo 'nop nope'; + } elseif (rand(0, 1) === 0) { + echo 'nope'; + } + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/ternary.php b/tests/PHPStan/Rules/Comparison/data/ternary.php index e2c3048ec4..b693f44656 100644 --- a/tests/PHPStan/Rules/Comparison/data/ternary.php +++ b/tests/PHPStan/Rules/Comparison/data/ternary.php @@ -1,6 +1,6 @@ analyse([__DIR__ . '/data/const-equals-no-namespace.php'], []); } + public function testDefinedScopeMerge(): void + { + $this->analyse([__DIR__ . '/data/defined-scope-merge.php'], [ + [ + 'Constant TEST not found.', + 8, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + + [ + 'Constant TEST not found.', + 11, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Constants/DynamicClassConstantFetchRuleTest.php b/tests/PHPStan/Rules/Constants/DynamicClassConstantFetchRuleTest.php new file mode 100644 index 0000000000..2806935226 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/DynamicClassConstantFetchRuleTest.php @@ -0,0 +1,70 @@ + + */ +class DynamicClassConstantFetchRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new DynamicClassConstantFetchRule( + self::getContainer()->getByType(PhpVersion::class), + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false), + ); + } + + public function testRule(): void + { + $errors = []; + if (PHP_VERSION_ID < 80300) { + $errors = [ + [ + 'Fetching class constants with a dynamic name is supported only on PHP 8.3 and later.', + 15, + ], + [ + 'Fetching class constants with a dynamic name is supported only on PHP 8.3 and later.', + 16, + ], + [ + 'Fetching class constants with a dynamic name is supported only on PHP 8.3 and later.', + 18, + ], + [ + 'Fetching class constants with a dynamic name is supported only on PHP 8.3 and later.', + 19, + ], + [ + 'Fetching class constants with a dynamic name is supported only on PHP 8.3 and later.', + 20, + ], + ]; + } else { + $errors = [ + [ + 'Class constant name in dynamic fetch can only be a string, int given.', + 18, + ], + [ + 'Class constant name in dynamic fetch can only be a string, int|string given.', + 19, + ], + [ + 'Class constant name in dynamic fetch can only be a string, string|null given.', + 20, + ], + ]; + } + $this->analyse([__DIR__ . '/data/dynamic-class-constant-fetch.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Constants/FinalConstantRuleTest.php b/tests/PHPStan/Rules/Constants/FinalConstantRuleTest.php index e54f0973b2..1fac7d6798 100644 --- a/tests/PHPStan/Rules/Constants/FinalConstantRuleTest.php +++ b/tests/PHPStan/Rules/Constants/FinalConstantRuleTest.php @@ -40,7 +40,7 @@ public function dataRule(): array /** * @dataProvider dataRule - * @param mixed[] $errors + * @param list $errors */ public function testRule(int $phpVersionId, array $errors): void { diff --git a/tests/PHPStan/Rules/Constants/MagicConstantContextRuleTest.php b/tests/PHPStan/Rules/Constants/MagicConstantContextRuleTest.php new file mode 100644 index 0000000000..2f598d78b3 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/MagicConstantContextRuleTest.php @@ -0,0 +1,167 @@ + + */ +class MagicConstantContextRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MagicConstantContextRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/magic-constant.php'], [ + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 5, + ], + [ + 'Magic constant __FUNCTION__ is always empty outside a function.', + 6, + ], + [ + 'Magic constant __METHOD__ is always empty outside a function.', + 7, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 9, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 17, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 22, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 26, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 59, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 64, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 78, + ], + [ + 'Magic constant __METHOD__ is always empty outside a function.', + 91, + ], + [ + 'Magic constant __FUNCTION__ is always empty outside a function.', + 92, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 93, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 97, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 101, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 105, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 109, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 115, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 120, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 133, + ], + ]); + } + + public function testGlobalNamespace(): void + { + $this->analyse([__DIR__ . '/data/magic-constant-global-ns.php'], [ + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 5, + ], + [ + 'Magic constant __FUNCTION__ is always empty outside a function.', + 6, + ], + [ + 'Magic constant __METHOD__ is always empty outside a function.', + 7, + ], + [ + 'Magic constant __NAMESPACE__ is always empty in global namespace.', + 8, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 9, + ], + [ + 'Magic constant __NAMESPACE__ is always empty in global namespace.', + 16, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 17, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 22, + ], + [ + 'Magic constant __NAMESPACE__ is always empty in global namespace.', + 25, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 26, + ], + [ + 'Magic constant __NAMESPACE__ is always empty in global namespace.', + 34, + ], + [ + 'Magic constant __CLASS__ is always empty outside a class.', + 46, + ], + [ + 'Magic constant __NAMESPACE__ is always empty in global namespace.', + 48, + ], + [ + 'Magic constant __TRAIT__ is always empty outside a trait.', + 51, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php b/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php index 3822349ced..5951d8fe71 100644 --- a/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -14,8 +15,7 @@ class MissingClassConstantTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - $reflectionProvider = $this->createReflectionProvider(); - return new MissingClassConstantTypehintRule(new MissingTypehintCheck($reflectionProvider, true, true, true, true, [])); + return new MissingClassConstantTypehintRule(new MissingTypehintCheck(true, true, true, true, [])); } public function testRule(): void @@ -38,4 +38,32 @@ public function testRule(): void ]); } + public function testBug8957(): void + { + if (PHP_VERSION_ID < 80200) { + $this->markTestSkipped('This test needs PHP 8.2'); + } + $this->analyse([__DIR__ . '/data/bug-8957.php'], []); + } + + public function testRuleShouldNotApplyToNativeTypes(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('This test needs PHP 8.3'); + } + + $this->analyse([__DIR__ . '/data/class-constant-native-type.php'], [ + [ + 'Constant ClassConstantNativeTypeForMissingTypehintRule\Foo::B type has no value type specified in iterable type array.', + 19, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type', + ], + [ + 'Constant ClassConstantNativeTypeForMissingTypehintRule\Foo::D with generic class ClassConstantNativeTypeForMissingTypehintRule\Bar does not specify its types: T', + 24, + 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Constants/NativeTypedClassConstantRuleTest.php b/tests/PHPStan/Rules/Constants/NativeTypedClassConstantRuleTest.php new file mode 100644 index 0000000000..7e0e4e6103 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/NativeTypedClassConstantRuleTest.php @@ -0,0 +1,36 @@ + + */ +class NativeTypedClassConstantRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new NativeTypedClassConstantRule(new PhpVersion(PHP_VERSION_ID)); + } + + public function testRule(): void + { + $errors = []; + if (PHP_VERSION_ID < 80300) { + $errors = [ + [ + 'Class constants with native types are supported only on PHP 8.3 and later.', + 10, + ], + ]; + } + + $this->analyse([__DIR__ . '/data/native-typed-class-constant.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php b/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php index 13e57adda4..6ce51b0297 100644 --- a/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php +++ b/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php @@ -90,4 +90,30 @@ public function testFinal(): void $this->analyse([__DIR__ . '/data/overriding-final-constant.php'], $errors); } + public function testNativeTypes(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->analyse([__DIR__ . '/data/overriding-constant-native-types.php'], [ + [ + 'Native type int|string of constant OverridingConstantNativeTypes\Bar::D is not covariant with native type int of constant OverridingConstantNativeTypes\Foo::D.', + 21, + ], + [ + 'Constant OverridingConstantNativeTypes\Ipsum::B overriding constant OverridingConstantNativeTypes\Lorem::B (int) should also have native type int.', + 37, + ], + [ + 'Constant OverridingConstantNativeTypes\PharChild::BZ2 overriding constant Phar::BZ2 (int) should also have native type int.', + 44, + ], + [ + 'Native type int|string of constant OverridingConstantNativeTypes\PharChild::NONE is not covariant with native type int of constant Phar::NONE.', + 48, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.php b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.php new file mode 100644 index 0000000000..7942506ca1 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.php @@ -0,0 +1,103 @@ + + */ +class ValueAssignedToClassConstantRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new ValueAssignedToClassConstantRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/value-assigned-to-class-constant.php'], [ + [ + 'PHPDoc tag @var for constant ValueAssignedToClassConstant\Foo::BAZ with type string is incompatible with value 1.', + 14, + ], + [ + 'PHPDoc tag @var for constant ValueAssignedToClassConstant\Foo::DOLOR with type ValueAssignedToClassConstant\Foo is incompatible with value 1.', + 23, + ], + [ + 'PHPDoc tag @var for constant ValueAssignedToClassConstant\Bar::BAZ with type string is incompatible with value 2.', + 32, + ], + ]); + } + + public function testBug7352(): void + { + $this->analyse([__DIR__ . '/data/bug-7352.php'], []); + } + + public function testBug7352WithSubNamespace(): void + { + $this->analyse([__DIR__ . '/data/bug-7352-with-sub-namespace.php'], []); + } + + public function testBug7273(): void + { + $this->analyse([__DIR__ . '/data/bug-7273.php'], []); + } + + public function testBug7273b(): void + { + $this->analyse([__DIR__ . '/data/bug-7273b.php'], []); + } + + public function testBug5655(): void + { + $this->analyse([__DIR__ . '/data/bug-5655.php'], []); + } + + public function testNativeType(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->analyse([__DIR__ . '/data/value-assigned-to-class-constant-native-type.php'], [ + [ + 'Constant ValueAssignedToClassConstantNativeType\Foo::BAR (int) does not accept value \'bar\'.', + 10, + ], + [ + 'Constant ValueAssignedToClassConstantNativeType\Bar::BAR (int<1, max>) does not accept value 0.', + 21, + ], + [ + 'Constant ValueAssignedToClassConstantNativeType\Floats::BAR (int) does not accept value 1.0.', + 30, + ], + [ + 'PHPDoc tag @var for constant ValueAssignedToClassConstantNativeType\Floats::BAZ with type float is incompatible with value 1.', + 33, + ], + ]); + } + + public function testBug10212(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->analyse([__DIR__ . '/data/bug-10212.php'], [ + [ + 'Constant Bug10212\HelloWorld::B (Bug10212\X\Foo) does not accept value Bug10212\Foo::Bar.', + 15, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Constants/data/bug-10212.php b/tests/PHPStan/Rules/Constants/data/bug-10212.php new file mode 100644 index 0000000000..40fda3454c --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/bug-10212.php @@ -0,0 +1,22 @@ += 8.3 + +namespace Bug10212; + +use function PHPStan\Testing\assertType; + +enum Foo +{ + case Bar; +} + +class HelloWorld +{ + public const string A = 'foo'; + public const X\Foo B = Foo::Bar; + public const Foo C = Foo::Bar; +} + +function(HelloWorld $hw): void { + assertType(X\Foo::class, $hw::B); + assertType(Foo::class, $hw::C); +}; diff --git a/tests/PHPStan/Rules/Constants/data/bug-5655.php b/tests/PHPStan/Rules/Constants/data/bug-5655.php new file mode 100644 index 0000000000..058d25136f --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/bug-5655.php @@ -0,0 +1,14 @@ + '', + ]; +} diff --git a/tests/PHPStan/Rules/Constants/data/bug-7273.php b/tests/PHPStan/Rules/Constants/data/bug-7273.php new file mode 100644 index 0000000000..b21ffc9d58 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/bug-7273.php @@ -0,0 +1,75 @@ + + */ +abstract class SomeValueProcessor implements ConfigValueProcessorInterface +{ +} + +interface ConfigKey +{ + public const ACTIVITY__EXPORT__TYPE = 'activity.export.type'; + public const ACTIVITY__TAGS__MULTI = 'activity.tags.multi'; + // ... +} + +interface Module +{ + public const ABSENCEREQUEST = 'absencerequest'; + public const ACTIVITY = 'activity'; + // ... +} + +interface ConfigRepositoryInterface +{ + /** + * @var array>, mixed>, + * }> + */ + public const CONFIGURATIONS = [ + ConfigKey::ACTIVITY__EXPORT__TYPE => [ + 'type' => "'SomeExport'|'SomeOtherExport'|null", + 'default' => null, + 'acl' => ['superadmin'], + 'linked_module' => Module::ACTIVITY, + ], + ConfigKey::ACTIVITY__TAGS__MULTI => [ + 'default' => false, + 'acl' => ['admin'], + 'linked_module' => Module::ACTIVITY, + 'value_processors' => [ + SomeValueProcessor::class => ['someOption' => true], + ], + ], + // ... + ]; +} diff --git a/tests/PHPStan/Rules/Constants/data/bug-7273b.php b/tests/PHPStan/Rules/Constants/data/bug-7273b.php new file mode 100644 index 0000000000..136efa9a9e --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/bug-7273b.php @@ -0,0 +1,51 @@ + */ + public const FIRST_EXAMPLE = [ + 'image.png' => [ + 'url' => '/first-link', + 'title' => 'First Link', + ], + 'directory/image.jpg' => [ + 'url' => '/second-link', + 'title' => 'Second Link', + ], + 'another-image.jpg' => [ + 'title' => 'An Image', + ], + ]; + + /** @var array */ + public const SECOND_EXAMPLE = [ + 'image.png' => [ + 'url' => '/first-link', + 'title' => 'First Link', + ], + 'directory/image.jpg' => [ + 'url' => '/second-link', + 'title' => 'Second Link', + ], + 'another-image.jpg' => [ + 'title' => 'An Image', + ], + ]; + + /** @var array{url?: string, title: string}[] */ + public const THIRD_EXAMPLE = [ + 'image.png' => [ + 'url' => '/first-link', + 'title' => 'First Link', + ], + 'directory/image.jpg' => [ + 'url' => '/second-link', + 'title' => 'Second Link', + ], + 'another-image.jpg' => [ + 'title' => 'An Image', + ], + ]; +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-7352-with-sub-namespace.php b/tests/PHPStan/Rules/Constants/data/bug-7352-with-sub-namespace.php similarity index 100% rename from tests/PHPStan/Rules/PhpDoc/data/bug-7352-with-sub-namespace.php rename to tests/PHPStan/Rules/Constants/data/bug-7352-with-sub-namespace.php diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-7352.php b/tests/PHPStan/Rules/Constants/data/bug-7352.php similarity index 100% rename from tests/PHPStan/Rules/PhpDoc/data/bug-7352.php rename to tests/PHPStan/Rules/Constants/data/bug-7352.php diff --git a/tests/PHPStan/Rules/Constants/data/bug-8957.php b/tests/PHPStan/Rules/Constants/data/bug-8957.php new file mode 100644 index 0000000000..683249c9ad --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/bug-8957.php @@ -0,0 +1,23 @@ += 8.2 + +namespace Bug8957; + +use function PHPStan\Testing\assertType; + +enum A: string +{ + case X = 'x'; + case Y = 'y'; +} + +class B { + public const A = [ + A::X->value, + A::Y->value, + ]; + + public function doFoo(): void + { + assertType('array{\'x\', \'y\'}', self::A); + } +} diff --git a/tests/PHPStan/Rules/Constants/data/class-constant-native-type.php b/tests/PHPStan/Rules/Constants/data/class-constant-native-type.php new file mode 100644 index 0000000000..fb3b694d57 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/class-constant-native-type.php @@ -0,0 +1,25 @@ += 8.3 + +namespace ClassConstantNativeTypeForMissingTypehintRule; + +/** @template T */ +class Bar +{ + +} + +const ConstantWithObjectInstanceForNativeType = new Bar(); + +class Foo +{ + + public const array A = []; + + /** @var array */ + public const array B = []; + + public const Bar C = ConstantWithObjectInstanceForNativeType; + + /** @var Bar */ + public const Bar D = ConstantWithObjectInstanceForNativeType; +} diff --git a/tests/PHPStan/Rules/Constants/data/defined-scope-merge.php b/tests/PHPStan/Rules/Constants/data/defined-scope-merge.php new file mode 100644 index 0000000000..9209ddf238 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/defined-scope-merge.php @@ -0,0 +1,11 @@ += 8.3 + +namespace NativeTypedClassConstant; + +class Foo +{ + + public const TEST = 1; + + public const int LOREM = 2; + +} diff --git a/tests/PHPStan/Rules/Constants/data/overriding-constant-native-types.php b/tests/PHPStan/Rules/Constants/data/overriding-constant-native-types.php new file mode 100644 index 0000000000..9b8afbbcad --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/overriding-constant-native-types.php @@ -0,0 +1,50 @@ += 8.3 + +namespace OverridingConstantNativeTypes; + +class Foo +{ + + public const int A = 1; + public const int|string B = 1; + public const int|string C = 1; + public const int D = 1; + +} + +class Bar extends Foo +{ + + public const int A = 2; + public const int|string B = 'foo'; + public const int C = 1; + public const int|string D = 1; + +} + +class Lorem +{ + + public const A = 1; + public const int B = 1; + +} + +class Ipsum extends Lorem +{ + + public const int A = 1; + public const B = 1; + +} + +class PharChild extends \Phar +{ + + const BZ2 = 'foo'; // error + + const int GZ = 1; // OK + + const int|string NONE = 1; // error + +} diff --git a/tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant-native-type.php b/tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant-native-type.php new file mode 100644 index 0000000000..5f287c1c4f --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant-native-type.php @@ -0,0 +1,38 @@ += 8.3 + +namespace ValueAssignedToClassConstantNativeType; + +class Foo +{ + + public const int FOO = 1; + + public const int BAR = 'bar'; + +} + +class Bar +{ + + /** @var int<1, max> */ + public const int FOO = 1; + + /** @var int<1, max> */ + public const int BAR = 0; + +} + +class Floats +{ + + public const float FOO = 1; + + public const int BAR = 1.0; + + /** @var float */ + public const BAZ = 1; + + /** @var float */ + public const float LOREM = 1; + +} diff --git a/tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant.php b/tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant.php new file mode 100644 index 0000000000..bdcf81838c --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant.php @@ -0,0 +1,49 @@ + */ + const DOLOR = 1; + +} + +class Bar extends Foo +{ + + const BAR = 2; + + const BAZ = 2; + +} + +class Baz +{ + + /** @var string */ + private const BAZ = 'foo'; + +} + +class Lorem extends Baz +{ + + private const BAZ = 1; + +} diff --git a/tests/PHPStan/Rules/DateTimeInstantiationRuleTest.php b/tests/PHPStan/Rules/DateTimeInstantiationRuleTest.php index b88771ce6f..40711affec 100644 --- a/tests/PHPStan/Rules/DateTimeInstantiationRuleTest.php +++ b/tests/PHPStan/Rules/DateTimeInstantiationRuleTest.php @@ -44,6 +44,14 @@ public function test(): void 'Instantiating DateTime with 2020-04-31 produces a warning: The parsed date was invalid', 20, ],*/ + [ + 'Instantiating DateTime with 2020.11.17 produces an error: Double time specification', + 22, + ], + [ + 'Instantiating DateTimeImmutable with 2020.11.17 produces an error: Double time specification', + 23, + ], ], ); } diff --git a/tests/PHPStan/Rules/DeadCode/BetterNoopRuleTest.php b/tests/PHPStan/Rules/DeadCode/BetterNoopRuleTest.php new file mode 100644 index 0000000000..dd472321ea --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/BetterNoopRuleTest.php @@ -0,0 +1,146 @@ + + */ +class BetterNoopRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new BetterNoopRule(new ExprPrinter(new Printer())); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/noop.php'], [ + [ + 'Expression "$arr" on a separate line does not do anything.', + 9, + ], + [ + 'Expression "$arr[\'test\']" on a separate line does not do anything.', + 10, + ], + [ + 'Expression "$foo::$test" on a separate line does not do anything.', + 11, + ], + [ + 'Expression "$foo->test" on a separate line does not do anything.', + 12, + ], + [ + 'Expression "\'foo\'" on a separate line does not do anything.', + 14, + ], + [ + 'Expression "1" on a separate line does not do anything.', + 15, + ], + [ + 'Expression "@\'foo\'" on a separate line does not do anything.', + 17, + ], + [ + 'Expression "+1" on a separate line does not do anything.', + 18, + ], + [ + 'Expression "-1" on a separate line does not do anything.', + 19, + ], + [ + 'Expression "isset($test)" on a separate line does not do anything.', + 25, + ], + [ + 'Expression "empty($test)" on a separate line does not do anything.', + 26, + ], + [ + 'Expression "true" on a separate line does not do anything.', + 27, + ], + [ + 'Expression "\DeadCodeNoop\Foo::TEST" on a separate line does not do anything.', + 28, + ], + [ + 'Expression "(string) 1" on a separate line does not do anything.', + 30, + ], + [ + 'Unused result of "xor" operator.', + 32, + 'This operator has unexpected precedence, try disambiguating the logic with parentheses ().', + ], + [ + 'Unused result of "and" operator.', + 35, + 'This operator has unexpected precedence, try disambiguating the logic with parentheses ().', + ], + [ + 'Unused result of "or" operator.', + 38, + 'This operator has unexpected precedence, try disambiguating the logic with parentheses ().', + ], + [ + 'Unused result of ternary operator.', + 40, + ], + [ + 'Unused result of ternary operator.', + 41, + ], + [ + 'Unused result of "||" operator.', + 46, + ], + [ + 'Unused result of "&&" operator.', + 49, + ], + ]); + } + + public function testNullsafe(): void + { + $this->analyse([__DIR__ . '/data/nullsafe-property-fetch-noop.php'], [ + [ + 'Expression "$ref?->name" on a separate line does not do anything.', + 10, + ], + ]); + } + + public function testRuleImpurePoints(): void + { + $this->analyse([__DIR__ . '/data/noop-impure-points.php'], [ + [ + 'Unused result of "&&" operator.', + 12, + ], + [ + 'Expression "$b()" on a separate line does not do anything.', + 59, + ], + [ + 'Expression "new class…" on a separate line does not do anything.', + 98, + ], + [ + 'Expression "new class…" on a separate line does not do anything.', + 104, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php new file mode 100644 index 0000000000..ac630c4880 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/CallToConstructorStatementWithoutImpurePointsRuleTest.php @@ -0,0 +1,37 @@ + + */ +class CallToConstructorStatementWithoutImpurePointsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToConstructorStatementWithoutImpurePointsRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/call-to-constructor-without-impure-points.php'], [ + [ + 'Call to new CallToConstructorWithoutImpurePoints\Foo() on a separate line has no effect.', + 15, + ], + ]); + } + + protected function getCollectors(): array + { + return [ + new PossiblyPureNewCollector($this->createReflectionProvider()), + new ConstructorWithoutImpurePointsCollector(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php new file mode 100644 index 0000000000..3c5fe7a2b9 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/CallToFunctionStatementWithoutImpurePointsRuleTest.php @@ -0,0 +1,37 @@ + + */ +class CallToFunctionStatementWithoutImpurePointsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToFunctionStatementWithoutImpurePointsRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/call-to-function-without-impure-points.php'], [ + [ + 'Call to function CallToFunctionWithoutImpurePoints\myFunc() on a separate line has no effect.', + 29, + ], + ]); + } + + protected function getCollectors(): array + { + return [ + new PossiblyPureFuncCallCollector($this->createReflectionProvider()), + new FunctionWithoutImpurePointsCollector(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php new file mode 100644 index 0000000000..4b93edaba2 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php @@ -0,0 +1,69 @@ + + */ +class CallToMethodStatementWithoutImpurePointsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToMethodStatementWithoutImpurePointsRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/call-to-method-without-impure-points.php'], [ + [ + 'Call to method CallToMethodWithoutImpurePoints\finalX::myFunc() on a separate line has no effect.', + 7, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\finalX::myFunc() on a separate line has no effect.', + 8, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\foo::finalFunc() on a separate line has no effect.', + 30, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\y::myFinalBaseFunc() on a separate line has no effect.', + 36, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 39, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\finalSubSubY::mySubSubFunc() on a separate line has no effect.', + 40, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\y::myFinalBaseFunc() on a separate line has no effect.', + 41, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\AbstractFoo::myFunc() on a separate line has no effect.', + 119, + ], + [ + 'Call to method CallToMethodWithoutImpurePoints\CallsPrivateMethodWithoutImpurePoints::doBar() on a separate line has no effect.', + 127, + ], + ]); + } + + protected function getCollectors(): array + { + return [ + new PossiblyPureMethodCallCollector(), + new MethodWithoutImpurePointsCollector(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRuleTest.php new file mode 100644 index 0000000000..74258d25a1 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/CallToStaticMethodStatementWithoutImpurePointsRuleTest.php @@ -0,0 +1,69 @@ + + */ +class CallToStaticMethodStatementWithoutImpurePointsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToStaticMethodStatementWithoutImpurePointsRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/call-to-static-method-without-impure-points.php'], [ + [ + 'Call to CallToStaticMethodWithoutImpurePoints\X::myFunc() on a separate line has no effect.', + 6, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\X::myFunc() on a separate line has no effect.', + 7, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\X::myFunc() on a separate line has no effect.', + 16, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 18, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 20, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\SubSubY::mySubSubFunc() on a separate line has no effect.', + 21, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 48, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 53, + ], + [ + 'Call to CallToStaticMethodWithoutImpurePoints\y::myFunc() on a separate line has no effect.', + 58, + ], + ]); + } + + protected function getCollectors(): array + { + return [ + new PossiblyPureStaticCallCollector(), + new MethodWithoutImpurePointsCollector(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php b/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php index d9f5358b8f..edffaa5b9a 100644 --- a/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php @@ -15,7 +15,7 @@ class NoopRuleTest extends RuleTestCase protected function getRule(): Rule { - return new NoopRule(new ExprPrinter(new Printer())); + return new NoopRule(new ExprPrinter(new Printer()), false); } public function testRule(): void diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index 4cf373673f..0efdb81138 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -41,7 +41,19 @@ public function testRule(): void ], [ 'Unreachable statement - code above always terminates.', - 71, + 44, + ], + [ + 'Unreachable statement - code above always terminates.', + 58, + ], + [ + 'Unreachable statement - code above always terminates.', + 93, + ], + [ + 'Unreachable statement - code above always terminates.', + 157, ], ]); } @@ -131,4 +143,79 @@ public function testBug7188(): void ]); } + public function testBug8620(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8620.php'], []); + } + + public function testBug4002(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002.php'], []); + } + + public function testBug4002Two(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002-2.php'], []); + } + + public function testBug4002Three(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002-3.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 13, + ], + ]); + } + + public function testBug4002Four(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002-4.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 9, + ], + ]); + } + + public function testBug4002Class(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002_class.php'], []); + } + + public function testBug4002Interface(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002_interface.php'], []); + } + + public function testBug4002Trait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4002_trait.php'], []); + } + + public function testBug8319(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8319.php'], []); + } + + public function testBug8966(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8966.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 8, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php index 08d6825213..24bde42de8 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php @@ -83,4 +83,33 @@ public function testBug8204(): void $this->analyse([__DIR__ . '/data/bug-8204.php'], []); } + public function testBug9005(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9005.php'], []); + } + + public function testBug9765(): void + { + $this->analyse([__DIR__ . '/data/bug-9765.php'], []); + } + + public function testDynamicConstantFetch(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->analyse([__DIR__ . '/data/unused-private-constant-dynamic-fetch.php'], [ + [ + 'Constant UnusedPrivateConstantDynamicFetch\Baz::FOO is unused.', + 32, + 'See: https://phpstan.org/developing-extensions/always-used-class-constants', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php index 60fe27b782..246237f97f 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php @@ -2,6 +2,9 @@ namespace PHPStan\Rules\DeadCode; +use PHPStan\Reflection\MethodReflection; +use PHPStan\Rules\Methods\AlwaysUsedMethodExtension; +use PHPStan\Rules\Methods\DirectAlwaysUsedMethodExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -14,7 +17,19 @@ class UnusedPrivateMethodRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UnusedPrivateMethodRule(); + return new UnusedPrivateMethodRule( + new DirectAlwaysUsedMethodExtensionProvider([ + new class() implements AlwaysUsedMethodExtension { + + public function isAlwaysUsed(MethodReflection $methodReflection): bool + { + return $methodReflection->getDeclaringClass()->is('UnusedPrivateMethod\IgnoredByExtension') + && $methodReflection->getName() === 'foo'; + } + + }, + ]), + ); } public function testRule(): void @@ -38,7 +53,11 @@ public function testRule(): void ], [ 'Method UnusedPrivateMethod\Lorem::doBaz() is unused.', - 97, + 99, + ], + [ + 'Method UnusedPrivateMethod\IgnoredByExtension::bar() is unused.', + 181, ], ]); } @@ -90,4 +109,28 @@ public function testBug7389(): void ]); } + public function testBug8346(): void + { + $this->analyse([__DIR__ . '/data/bug-8346.php'], []); + } + + public function testFalsePositiveWithTraitUse(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/unused-method-false-positive-with-trait.php'], []); + } + + public function testBug6039(): void + { + $this->analyse([__DIR__ . '/data/bug-6039.php'], []); + } + + public function testBug9765(): void + { + $this->analyse([__DIR__ . '/data/bug-9765.php'], []); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php index dfb6eae2ea..e17c06a69d 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php @@ -218,6 +218,13 @@ public function testNullsafe(): void $this->analyse([__DIR__ . '/data/nullsafe-unused-private-property.php'], []); } + public function testBug3654(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-3654.php'], []); + } + public function testBug5935(): void { $this->alwaysWrittenTags = []; @@ -265,4 +272,47 @@ public function testBug8204(): void $this->analyse([__DIR__ . '/data/bug-8204.php'], []); } + public function testBug8850(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-8850.php'], []); + } + + public function testBug9409(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-9409.php'], []); + } + + public function testBug9765(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-9765.php'], []); + } + + public function testBug10059(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-10059.php'], []); + } + + public function testBug10628(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-10628.php'], []); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-10059.php b/tests/PHPStan/Rules/DeadCode/data/bug-10059.php new file mode 100644 index 0000000000..fdc6335e27 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-10059.php @@ -0,0 +1,20 @@ += 8.1 + +namespace Bug10059; + +use DateTimeImmutable; + +class Foo +{ + public function __construct( + private readonly DateTimeImmutable $startDateTime + ) { + } + + public function bar(): void + { + declare(ticks=5) { + echo $this->startDateTime->format('Y-m-d H:i:s.u'), PHP_EOL; + } + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-10628.php b/tests/PHPStan/Rules/DeadCode/data/bug-10628.php new file mode 100644 index 0000000000..3fee75ba1e --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-10628.php @@ -0,0 +1,32 @@ += 8.0 + +namespace Bug10628; + +use stdClass; + +interface Bar +{ + + public function bazName(): string; + +} + +final class Foo +{ + public function __construct( + private Bar $bar, + ) { + } + + public function __invoke(): stdClass + { + return $this->getMixed()->get( + name: $this->bar->bazName(), + ); + } + + public function getMixed(): mixed + { + + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-3654.php b/tests/PHPStan/Rules/DeadCode/data/bug-3654.php new file mode 100644 index 0000000000..d6e182a023 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-3654.php @@ -0,0 +1,42 @@ +id = $id; + } + + public function jsonSerialize(): array + { + return \get_object_vars($this); + } +} + +class Bar implements \JsonSerializable +{ + + /** + * @var int + */ + private $id; + + public function __construct(int $id) + { + $this->id = $id; + } + + public function jsonSerialize(): void + { + \array_walk($this, static function ($key, $value) { + }); + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-4002-2.php b/tests/PHPStan/Rules/DeadCode/data/bug-4002-2.php new file mode 100644 index 0000000000..f16716af43 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-4002-2.php @@ -0,0 +1,11 @@ += 8.0 + +namespace Bug6039; + +trait Foo +{ + public function showFoo(): void + { + echo 'foo' . self::postFoo(); + } + + abstract private static function postFoo(): string; +} + +class UseFoo +{ + use Foo { + showFoo as showFooTrait; + } + + public function showFoo(): void + { + echo 'fooz'; + $this->showFooTrait(); + } + + private static function postFoo(): string + { + return 'postFoo'; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-8319.php b/tests/PHPStan/Rules/DeadCode/data/bug-8319.php new file mode 100644 index 0000000000..f8e767d47f --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-8319.php @@ -0,0 +1,11 @@ +sayhello('world'); + } + + private function sayHello(string $name): string + { + return 'Hello ' . $name; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-8620.php b/tests/PHPStan/Rules/DeadCode/data/bug-8620.php new file mode 100644 index 0000000000..cad6d811c2 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-8620.php @@ -0,0 +1,33 @@ +security = $security; + } +} + +trait QueryBuilderHelperTrait +{ + use OrganisationExtensionHelperTrait; +} + +trait OrganisationExtensionHelperTrait +{ + use UserHelperTrait; + + public function getOrganisationIds(): void + { + $user = $this->getUser(); + } +} + +trait UserHelperTrait +{ + public function getUser(): string + { + $user = $this->security->getUser(); + + return 'foo'; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-8966.php b/tests/PHPStan/Rules/DeadCode/data/bug-8966.php new file mode 100644 index 0000000000..a50e2b1c2e --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-8966.php @@ -0,0 +1,8 @@ += 8.1 + +namespace Bug9005; + +enum Test: string +{ + private const PREFIX = 'my-stuff-'; + + case TESTING = self::PREFIX . 'test'; + + case TESTING2 = self::PREFIX . 'test2'; +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-9409.php b/tests/PHPStan/Rules/DeadCode/data/bug-9409.php new file mode 100644 index 0000000000..849ef0af2d --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-9409.php @@ -0,0 +1,22 @@ + $tempDir */ + private static $tempDir = []; + + public function getTempDir(string $name): ?string + { + if (isset($this::$tempDir[$name])) { + return $this::$tempDir[$name]; + } + + $path = ''; + + $this::$tempDir[$name] = $path; + + return $path; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-9765.php b/tests/PHPStan/Rules/DeadCode/data/bug-9765.php new file mode 100644 index 0000000000..10fbbcf45d --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-9765.php @@ -0,0 +1,71 @@ +add($arg); + }; + } + + public function do(): void + { + $c = self::runner(); + print $c->bindTo($this)(5); + } + + private function add(int $a): int + { + return $a + 1; + } + +} + +class HelloWorld2 +{ + + /** @var int */ + private $foo; + + public static function runner(): \Closure + { + return function (int $arg) { + if ($arg > 0) { + $this->foo = $arg; + } else { + echo $this->foo; + } + }; + } + + public function do(): void + { + $c = self::runner(); + print $c->bindTo($this)(5); + } + +} + +class HelloWorld3 +{ + + private const FOO = 1; + + public static function runner(): \Closure + { + return function (int $arg) { + echo $this::FOO; + }; + } + + public function do(): void + { + $c = self::runner(); + print $c->bindTo($this)(5); + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/call-to-constructor-without-impure-points.php b/tests/PHPStan/Rules/DeadCode/data/call-to-constructor-without-impure-points.php new file mode 100644 index 0000000000..10b9622a2d --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/call-to-constructor-without-impure-points.php @@ -0,0 +1,16 @@ +myFunc(); + $x->myFUNC(); + $x->throwingFUNC(); + $x->throwingFunc(); + $x->funcWithRef(); + $x->impureFunc(); + $x->callingImpureFunc(); + + $a = $x->myFunc(); + + $xy = new y(); + if (rand(0,1)) { + $xy = new finalX(); + } + $xy->myFunc(); + + $xy = new Y(); // case-insensitive class name + if (rand(0,1)) { + $xy = new finalX(); + } + $xy->myFunc(); + + $foo = new Foo(); + $foo->finalFunc(); + $foo->finalThrowingFunc(); + $foo->throwingFunc(); + + $subY = new subY(); + $subY->myFunc(); + $subY->myFinalBaseFunc(); + + $subSubY = new finalSubSubY(); + $subSubY->myFunc(); + $subSubY->mySubSubFunc(); + $subSubY->myFinalBaseFunc(); +}; + +class y +{ + function myFunc() + { + } + final function myFinalBaseFunc() + { + } +} + +class subY extends y { +} + +final class finalSubSubY extends subY { + function mySubSubFunc() + { + } +} + +final class finalX { + function myFunc() + { + } + + function throwingFunc() + { + throw new \Exception(); + } + + function funcWithRef(&$a) + { + } + + /** @phpstan-impure */ + function impureFunc() + { + } + + function callingImpureFunc() + { + $this->impureFunc(); + } +} + +class foo +{ + final function finalFunc() + { + } + + final function finalThrowingFunc() + { + throw new \Exception(); + } + + function throwingFunc() + { + throw new \Exception(); + } +} + +abstract class AbstractFoo +{ + + function myFunc() + { + } + +} +final class FinalFoo extends AbstractFoo +{ + +} + +function (FinalFoo $foo): void { + $foo->myFunc(); +}; + +class CallsPrivateMethodWithoutImpurePoints +{ + + public function doFoo(): void + { + $this->doBar(); + } + + private function doBar(): int + { + return 1; + } + +} + +class TestIgnoring +{ + + public function doFoo(): void + { + $this->doBar(); // @phpstan-ignore method.resultUnused + } + + private function doBar(): int + { + return 1; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/call-to-static-method-without-impure-points.php b/tests/PHPStan/Rules/DeadCode/data/call-to-static-method-without-impure-points.php new file mode 100644 index 0000000000..3dfcff73d4 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/call-to-static-method-without-impure-points.php @@ -0,0 +1,122 @@ +i = 1; + } +} + +class ChildOfParentWithConstructor extends ParentWithConstructor +{ + public function __construct() + { + parent::__construct(); + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/noop-impure-points.php b/tests/PHPStan/Rules/DeadCode/data/noop-impure-points.php new file mode 100644 index 0000000000..cf52aa513f --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/noop-impure-points.php @@ -0,0 +1,117 @@ +doBar(); + $b && $this->doBaz(); + $b && $this->doLorem(); + } + + /** + * @phpstan-pure + */ + public function doBar(): bool + { + return true; + } + + /** + * @phpstan-impure + */ + public function doBaz(): bool + { + return true; + } + + public function doLorem(): bool + { + return true; + } + + public function doExit(): void + { + exit(1); + } + + public function doAssign(bool $b): void + { + $b ? $a = 1 : ''; + $b ? $this->foo = 1 : ''; + } + + public function doClosures(int $i): void + { + $a = static function () { + echo '1'; + }; + $a(); + + $b = static function () { + return 1 + 1; + }; + $b(); + + $ref = 1; + $c = static function () use (&$ref) { + $ref++; + }; + $c(); + + $d = function () { + self::$foo = 1; + }; + $d(); + + $e = function () { + self::$staticProp = 1; + }; + $e(); + + $i(); + } + + public function doFunctionWithByRef(bool $b, array $a): void + { + $func = $b ? 'array_unshift' : 'array_push'; + $func($a, 1); + } + + public function anonymousClassWithSideEffect(): void + { + new class () { + public function __construct() + { + echo '1'; + } + }; + } + + public function anonymousClassWithoutConstructor(): void + { + new class () { + }; + } + + public function anonymousClassWithPureConstructor(): void + { + new class () { + + /** @var int */ + private $i; + + public function __construct() + { + $this->i = 1; + } + + }; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/noop.php b/tests/PHPStan/Rules/DeadCode/data/noop.php index c025831720..5eb7d42d46 100644 --- a/tests/PHPStan/Rules/DeadCode/data/noop.php +++ b/tests/PHPStan/Rules/DeadCode/data/noop.php @@ -2,7 +2,7 @@ namespace DeadCodeNoop; -function (stdClass $foo) { +function (stdClass $foo, bool $a, bool $b) { $foo->foo(); $arr = []; @@ -28,4 +28,24 @@ function (stdClass $foo) { Foo::TEST; (string) 1; + + $r = $a xor $b; + + $s = $a and doFoo(); + $t = $a and $b; + + $s = $a or doFoo(); + $t = $a or $b; + + $a ? $b : $s; + $a ?: $b; + $a ? doFoo() : $s; + $a ? $b : doFoo(); + $a ? doFoo() : doBar(); + + $a || $b; + $a || doFoo(); + + $a && $b; + $a && doFoo(); }; diff --git a/tests/PHPStan/Rules/DeadCode/data/unreachable.php b/tests/PHPStan/Rules/DeadCode/data/unreachable.php index 69b6c3c8bc..48f39153f1 100644 --- a/tests/PHPStan/Rules/DeadCode/data/unreachable.php +++ b/tests/PHPStan/Rules/DeadCode/data/unreachable.php @@ -36,6 +36,28 @@ public function doLorem() // this is why... } + public function doLorem2(string $foo) + { + return; + // this is why... + + echo $foo; + } + + public function doLorem3() + { + return; + ; + } + + public function doLorem4(string $foo) + { + return; + ; + + echo $foo; + } + /** * @param \stdClass[] $all */ @@ -114,3 +136,25 @@ private function somethingAboutDateTime(\DateTime $dt): bool } } + +class LastElseIf +{ + + /** + * @param 'a'|'b'|'c' $s + * @return void + */ + public function doFoo(string $s): void + { + if ($s === 'a') { + return; + } elseif ($s === 'b') { + return; + } elseif ($s === 'c') { + return; + } + + echo "test"; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-method-false-positive-with-trait.php b/tests/PHPStan/Rules/DeadCode/data/unused-method-false-positive-with-trait.php new file mode 100644 index 0000000000..24e2c3256d --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-method-false-positive-with-trait.php @@ -0,0 +1,72 @@ += 8.1 + +namespace UnusedMethodFalsePositiveWithTrait; + +use ReflectionEnum; + +enum LocalOnlineReservationTime: string +{ + + use LabeledEnumTrait; + + case MORNING = 'morning'; + case AFTERNOON = 'afternoon'; + case EVENING = 'evening'; + + public static function getPeriodForHour(string $hour): self + { + $hour = self::hourToNumber($hour); + + throw new \Exception('Internal error'); + } + + private static function hourToNumber(string $hour): int + { + return (int) str_replace(':', '', $hour); + } + +} + +trait LabeledEnumTrait +{ + + use EnumTrait; + +} + +trait EnumTrait +{ + + /** + * @return list + */ + public static function getDeprecatedEnums(): array + { + static $cache = []; + if ($cache === []) { + $reflection = new ReflectionEnum(self::class); + $cases = $reflection->getCases(); + + foreach ($cases as $case) { + $docComment = $case->getDocComment(); + if ($docComment === false || !str_contains($docComment, '@deprecated')) { + continue; + } + $cache[] = self::from($case->getBackingValue()); + } + } + + return $cache; + } + + public function isDeprecated(): bool + { + return $this->equalsAny(self::getDeprecatedEnums()); + } + + public function equalsAny(...$that): bool + { + return in_array($this, $that, true); + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-private-constant-dynamic-fetch.php b/tests/PHPStan/Rules/DeadCode/data/unused-private-constant-dynamic-fetch.php new file mode 100644 index 0000000000..089efcb892 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-constant-dynamic-fetch.php @@ -0,0 +1,39 @@ += 8.3 + +namespace UnusedPrivateConstantDynamicFetch; + +class Foo +{ + + private const FOO = 1; + + public function doFoo(string $s): void + { + echo self::{$s}; + } + +} + +class Bar +{ + + private const FOO = 1; + + public function doFoo(self $a, string $s): void + { + echo $a::{$s}; + } + +} + +class Baz +{ + + private const FOO = 1; + + public function doFoo(\stdClass $a, string $s): void + { + echo $a::{$s}; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-private-method.php b/tests/PHPStan/Rules/DeadCode/data/unused-private-method.php index be6f11bdd0..ce43e40a90 100644 --- a/tests/PHPStan/Rules/DeadCode/data/unused-private-method.php +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-method.php @@ -80,8 +80,10 @@ private function doFoo() public function doBar(string $name) { - $cb = [$this, $name]; - $cb(); + if ($name === 'doFoo') { + $cb = [$this, $name]; + $cb(); + } } } @@ -154,3 +156,29 @@ private function doLorem() } } + +class StaticMethod +{ + + private static function doFoo(): void + { + + } + + public function doTest(): void + { + $this::doFoo(); + } + +} + +class IgnoredByExtension +{ + private function foo(): void + { + } + + private function bar(): void + { + } +} diff --git a/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php b/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php index bbd36941eb..acc2a2c8c1 100644 --- a/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php +++ b/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php @@ -23,10 +23,6 @@ public function testRuleInPhpStanNamespace(): void 'Dumped type: non-empty-array', 10, ], - [ - 'Missing argument for PHPStan\dumpType() function call.', - 11, - ], ]); } @@ -72,4 +68,18 @@ public function testBug7803(): void ]); } + public function testBug10377(): void + { + $this->analyse([__DIR__ . '/data/bug-10377.php'], [ + [ + 'Dumped type: array', + 22, + ], + [ + 'Dumped type: array', + 34, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Debug/data/bug-10377.php b/tests/PHPStan/Rules/Debug/data/bug-10377.php new file mode 100644 index 0000000000..b9850376e4 --- /dev/null +++ b/tests/PHPStan/Rules/Debug/data/bug-10377.php @@ -0,0 +1,37 @@ + $additionalProperties + */ + public function addAdditionalProperties(array $additionalProperties): void + { + \PHPStan\dumpType($additionalProperties); + } +} + +trait RequestParameters +{ + + /** + * @param array $additionalProperties + */ + public function addAdditionalProperties(array $additionalProperties): void + { + \PHPStan\dumpType($additionalProperties); + } + +} diff --git a/tests/PHPStan/Rules/DirectRegistryTest.php b/tests/PHPStan/Rules/DirectRegistryTest.php index 2eb284a52a..c1ac9fc109 100644 --- a/tests/PHPStan/Rules/DirectRegistryTest.php +++ b/tests/PHPStan/Rules/DirectRegistryTest.php @@ -26,8 +26,12 @@ public function testGetRules(): void public function testGetRulesWithTwoDifferentInstances(): void { - $fooRule = new UniversalRule(Node\Expr\FuncCall::class, static fn (Node\Expr\FuncCall $node, Scope $scope): array => ['Foo error']); - $barRule = new UniversalRule(Node\Expr\FuncCall::class, static fn (Node\Expr\FuncCall $node, Scope $scope): array => ['Bar error']); + $fooRule = new UniversalRule(Node\Expr\FuncCall::class, static fn (Node\Expr\FuncCall $node, Scope $scope): array => [ + RuleErrorBuilder::message('Foo error')->identifier('tests.fooRule')->build(), + ]); + $barRule = new UniversalRule(Node\Expr\FuncCall::class, static fn (Node\Expr\FuncCall $node, Scope $scope): array => [ + RuleErrorBuilder::message('Bar error')->identifier('tests.barRule')->build(), + ]); $registry = new DirectRegistry([ $fooRule, diff --git a/tests/PHPStan/Rules/DummyCollectorRule.php b/tests/PHPStan/Rules/DummyCollectorRule.php index ca11b930e6..22b66a7809 100644 --- a/tests/PHPStan/Rules/DummyCollectorRule.php +++ b/tests/PHPStan/Rules/DummyCollectorRule.php @@ -42,6 +42,7 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(implode(', ', $parts)) ->file(__DIR__ . '/data/dummy-collector.php') ->line(5) + ->identifier('tests.dummyCollector') ->build(), ]; } diff --git a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php index 5213052e97..26643e506c 100644 --- a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php +++ b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -26,7 +28,7 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), new NullsafeCheck(), new PhpVersion(80100), new UnresolvableTypeHelper(), @@ -37,7 +39,11 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, ), ); } diff --git a/tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php b/tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php new file mode 100644 index 0000000000..08f4885067 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php @@ -0,0 +1,44 @@ + + */ +class AbilityToDisableImplicitThrowsTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( + $this->createReflectionProvider(), + [], + [], + [], + [], + ), true); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/ability-to-disable-implicit-throws.php'], [ + [ + 'Dead catch - Throwable is never thrown in the try block.', + 17, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [__DIR__ . '/data/ability-to-disable-implicit-throws.neon'], + ); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleStubsTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleStubsTest.php new file mode 100644 index 0000000000..6b32128600 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleStubsTest.php @@ -0,0 +1,46 @@ + + */ +class CatchWithUnthrownExceptionRuleStubsTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( + $this->createReflectionProvider(), + [], + [], + [], + [], + ), true); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/catch-with-unthrown-exception-stubs.php'], [ + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 44, + ], + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 55, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/catch-with-unthrown-exception-stubs.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index ac6a7d01ca..83943ccbb2 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -2,6 +2,8 @@ namespace PHPStan\Rules\Exceptions; +use Error; +use InvalidArgumentException; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -12,9 +14,20 @@ class CatchWithUnthrownExceptionRuleTest extends RuleTestCase { + private bool $reportUncheckedExceptionDeadCatch = true; + + /** @var string[] */ + private array $uncheckedExceptionClasses = []; + protected function getRule(): Rule { - return new CatchWithUnthrownExceptionRule(); + return new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( + $this->createReflectionProvider(), + [], + $this->uncheckedExceptionClasses, + [], + [], + ), $this->reportUncheckedExceptionDeadCatch); } public function testRule(): void @@ -108,6 +121,144 @@ public function testRule(): void 'Dead catch - Exception is never thrown in the try block.', 532, ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 555, + ], + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 629, + ], + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 647, + ], + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 741, + ], + ]); + } + + public function testRuleWithoutReportingUncheckedException(): void + { + $this->reportUncheckedExceptionDeadCatch = false; + $this->uncheckedExceptionClasses = [ + InvalidArgumentException::class, + Error::class, + ]; + + $this->analyse([__DIR__ . '/data/unthrown-exception.php'], [ + [ + 'Dead catch - Throwable is never thrown in the try block.', + 12, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 21, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 38, + ], + [ + 'Dead catch - RuntimeException is never thrown in the try block.', + 49, + ], + [ + 'Dead catch - Throwable is never thrown in the try block.', + 71, + ], + [ + 'Dead catch - DomainException is never thrown in the try block.', + 117, + ], + [ + 'Dead catch - Throwable is never thrown in the try block.', + 119, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 171, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 180, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 224, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 312, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 344, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 375, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 380, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 398, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 432, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 437, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 485, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 532, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 555, + ], + ]); + } + + public function testMultiCatch(): void + { + $this->analyse([__DIR__ . '/data/unthrown-exception-multi.php'], [ + [ + 'Dead catch - LogicException is never thrown in the try block.', + 12, + ], + [ + 'Dead catch - OverflowException is never thrown in the try block.', + 36, + ], + [ + 'Dead catch - JsonException is never thrown in the try block.', + 58, + ], + [ + 'Dead catch - RuntimeException is never thrown in the try block.', + 120, + ], + [ + 'Dead catch - InvalidArgumentException is already caught above.', + 145, + ], + [ + 'Dead catch - InvalidArgumentException is already caught above.', + 156, + ], ]); } @@ -167,6 +318,16 @@ public function testBug4814(): void ]); } + public function testBug9066(): void + { + $this->analyse([__DIR__ . '/data/bug-9066.php'], [ + [ + 'Dead catch - OutOfBoundsException is never thrown in the try block.', + 28, + ], + ]); + } + public function testThrowExpression(): void { $this->analyse([__DIR__ . '/data/dead-catch-throw-expr.php'], [ @@ -421,4 +582,46 @@ public function testBug6349(): void ]); } + public function testMagicMethods(): void + { + $this->analyse([__DIR__ . '/data/dead-catch-magic-methods.php'], [ + [ + 'Dead catch - Exception is never thrown in the try block.', + 22, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 65, + ], + ]); + } + + public function testBug9406(): void + { + $this->analyse([__DIR__ . '/data/bug-9406.php'], []); + } + + public function testBug5650(): void + { + $this->analyse([__DIR__ . '/data/bug-5650.php'], [ + [ + 'Dead catch - RuntimeException is never thrown in the try block.', + 24, + ], + [ + 'Dead catch - RuntimeException is never thrown in the try block.', + 32, + ], + ]); + } + + public function testBug9568(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-9568.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleWithDisabledMultiCatchTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleWithDisabledMultiCatchTest.php new file mode 100644 index 0000000000..debb459f03 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleWithDisabledMultiCatchTest.php @@ -0,0 +1,48 @@ + + */ +class CatchWithUnthrownExceptionRuleWithDisabledMultiCatchTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( + $this->createReflectionProvider(), + [], + [], + [], + [], + ), true); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [__DIR__ . '/disable-detect-multi-catch.neon'], + ); + } + + public function testMultiCatchBackwardCompatible(): void + { + $this->analyse([__DIR__ . '/data/unthrown-exception-multi.php'], [ + [ + 'Dead catch - InvalidArgumentException is already caught above.', + 145, + ], + [ + 'Dead catch - InvalidArgumentException is already caught above.', + 156, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php b/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php index 6af5ae9a2c..36346ad255 100644 --- a/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Exceptions; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -14,10 +16,13 @@ class CaughtExceptionExistenceRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new CaughtExceptionExistenceRule( - $broker, - new ClassCaseSensitivityCheck($broker, true), + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), true, ); } @@ -52,4 +57,17 @@ public function testBug3690(): void $this->analyse([__DIR__ . '/data/bug-3690.php'], []); } + public function testPhpstanInternalClass(): void + { + $tip = 'This is most likely unintentional. Did you mean to type \PrefixedRuntimeException?'; + + $this->analyse([__DIR__ . '/../Classes/data/phpstan-internal-class.php'], [ + [ + 'Referencing prefixed PHPStan class: _PHPStan_156ee64ba\PrefixedRuntimeException.', + 19, + $tip, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php index 9f195bb503..d085543b84 100644 --- a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php @@ -40,6 +40,10 @@ public function testRule(): void 'Method MissingExceptionMethodThrows\Foo::doLorem2() throws checked exception InvalidArgumentException but it\'s missing from the PHPDoc @throws tag.', 34, ], + [ + 'Method MissingExceptionMethodThrows\Foo::dateTimeZoneDoesThrows() throws checked exception Exception but it\'s missing from the PHPDoc @throws tag.', + 95, + ], ]); } diff --git a/tests/PHPStan/Rules/Exceptions/NoncapturingCatchRuleTest.php b/tests/PHPStan/Rules/Exceptions/NoncapturingCatchRuleTest.php new file mode 100644 index 0000000000..9c8181f4a3 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/NoncapturingCatchRuleTest.php @@ -0,0 +1,60 @@ + + */ +class NoncapturingCatchRuleTest extends RuleTestCase +{ + + private PhpVersion $phpVersion; + + protected function getRule(): Rule + { + return new NoncapturingCatchRule($this->phpVersion); + } + + public function dataRule(): array + { + return [ + [ + 70400, + [ + [ + 'Non-capturing catch is supported only on PHP 8.0 and later.', + 12, + ], + [ + 'Non-capturing catch is supported only on PHP 8.0 and later.', + 21, + ], + ], + ], + [ + 80000, + [], + ], + ]; + } + + /** + * @dataProvider dataRule + * + * @param list $expectedErrors + */ + public function testRule(int $phpVersion, array $expectedErrors): void + { + $this->phpVersion = new PhpVersion($phpVersion); + + $this->analyse([ + __DIR__ . '/data/noncapturing-catch.php', + __DIR__ . '/data/bug-8663.php', + ], $expectedErrors); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/ThrowExprTypeRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowExprTypeRuleTest.php new file mode 100644 index 0000000000..8ef6277fe1 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/ThrowExprTypeRuleTest.php @@ -0,0 +1,74 @@ + + */ +class ThrowExprTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ThrowExprTypeRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false)); + } + + public function testRule(): void + { + $this->analyse( + [__DIR__ . '/data/throw-values.php'], + [ + /*[ + 'Invalid type int to throw.', + 29, + ], + [ + 'Invalid type ThrowValues\InvalidException to throw.', + 32, + ], + [ + 'Invalid type ThrowValues\InvalidInterfaceException to throw.', + 35, + ], + [ + 'Invalid type Exception|null to throw.', + 38, + ], + [ + 'Throwing object of an unknown class ThrowValues\NonexistentClass.', + 44, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ],*/ + [ + 'Invalid type int to throw.', + 65, + ], + ], + ); + } + + public function testClassExists(): void + { + $this->analyse([__DIR__ . '/data/throw-class-exists.php'], []); + } + + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/throw-values-nullsafe.php'], [ + /*[ + 'Invalid type Exception|null to throw.', + 17, + ],*/ + ]); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/ThrowExpressionRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowExpressionRuleTest.php index 19d096a91b..1f32de8fe4 100644 --- a/tests/PHPStan/Rules/Exceptions/ThrowExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/ThrowExpressionRuleTest.php @@ -40,7 +40,7 @@ public function dataRule(): array /** * @dataProvider dataRule - * @param mixed[] $expectedErrors + * @param list $expectedErrors */ public function testRule(int $phpVersion, array $expectedErrors): void { diff --git a/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php index 5b78999ddd..ff6c4416a6 100644 --- a/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php @@ -87,7 +87,7 @@ public function dataRule(): array /** * @dataProvider dataRule * @param string[] $checkedExceptionClasses - * @param mixed[] $errors + * @param list $errors */ public function testRule(bool $missingCheckedExceptionInThrows, array $checkedExceptionClasses, array $errors): void { diff --git a/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php index 5f0940bf9a..5a2dcb0429 100644 --- a/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php @@ -89,7 +89,7 @@ public function dataRule(): array /** * @dataProvider dataRule * @param string[] $checkedExceptionClasses - * @param mixed[] $errors + * @param list $errors */ public function testRule(bool $missingCheckedExceptionInThrows, array $checkedExceptionClasses, array $errors): void { diff --git a/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php b/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php index eddbd31bf6..98c3737887 100644 --- a/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -40,7 +41,39 @@ public function testRule(): void 'Method TooWideThrowsMethod\ParentClass::doFoo() has LogicException in PHPDoc @throws tag but it\'s not thrown.', 77, ], + [ + 'Method TooWideThrowsMethod\ImmediatelyCalledCallback::doFoo2() has InvalidArgumentException in PHPDoc @throws tag but it\'s not thrown.', + 167, + ], + ]); + } + + public function testBug6233(): void + { + $this->analyse([__DIR__ . '/data/bug-6233.php'], []); + } + + public function testImmediatelyCalledArrowFunction(): void + { + if (PHP_VERSION_ID < 70400) { + self::markTestSkipped('Test requires PHP 7.4.'); + } + + $this->analyse([__DIR__ . '/data/immediately-called-arrow-function.php'], [ + [ + 'Method ImmediatelyCalledArrowFunction\ImmediatelyCalledCallback::doFoo2() has InvalidArgumentException in PHPDoc @throws tag but it\'s not thrown.', + 19, + ], ]); } + public function testFirstClassCallable(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/immediately-called-fcc.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/catch-with-unthrown-exception-stubs.neon b/tests/PHPStan/Rules/Exceptions/catch-with-unthrown-exception-stubs.neon new file mode 100644 index 0000000000..f21387ab6a --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/catch-with-unthrown-exception-stubs.neon @@ -0,0 +1,5 @@ +parameters: + exceptions: + implicitThrows: false + stubFiles: + - data/catch-with-unthrown-exception-stubs.stub diff --git a/tests/PHPStan/Rules/Exceptions/data/ability-to-disable-implicit-throws.neon b/tests/PHPStan/Rules/Exceptions/data/ability-to-disable-implicit-throws.neon new file mode 100644 index 0000000000..790db39dd6 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/ability-to-disable-implicit-throws.neon @@ -0,0 +1,3 @@ +parameters: + exceptions: + implicitThrows: false diff --git a/tests/PHPStan/Rules/Exceptions/data/ability-to-disable-implicit-throws.php b/tests/PHPStan/Rules/Exceptions/data/ability-to-disable-implicit-throws.php new file mode 100644 index 0000000000..0c5d4c1c26 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/ability-to-disable-implicit-throws.php @@ -0,0 +1,25 @@ +method(); + } catch (\Throwable $e) { // Dead catch - Throwable is never thrown in the try block. + + } + } + + public function method(): void + { + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-5650.php b/tests/PHPStan/Rules/Exceptions/data/bug-5650.php new file mode 100644 index 0000000000..9a9be87c4a --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-5650.php @@ -0,0 +1,35 @@ += 8.0 + +namespace Bug8663; + +/** + * Provides example to demonstrate an issue with PHPStan. + */ +class StanExample2 +{ + + /** + * An exception is caught but not captured. + * + * That's OK for PHP 8 but not for 7.4 - PHPStan does not report the issue. + */ + public function catchExceptionsWithoutCapturing(): void + { + try { + print 'Lets do something nasty here.'; + throw new \Exception('This is nasty'); + } catch (\Exception) { + print 'Exception occured'; + } + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-9066.php b/tests/PHPStan/Rules/Exceptions/data/bug-9066.php new file mode 100644 index 0000000000..60990327ac --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-9066.php @@ -0,0 +1,32 @@ +get('1'); + } catch (\OutOfBoundsException $e) { + + } + } + public function removeMayThrow() + { $map = new \Ds\Map(); + try { + $map->remove('1'); + } catch (\OutOfBoundsException $e) { + + } + } + public function neverThrows() + { $map = new \Ds\Map(); + try { + $map->get('1', null); + $map->remove('1', null); + } catch (\OutOfBoundsException $e) { + + } + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-9406.php b/tests/PHPStan/Rules/Exceptions/data/bug-9406.php new file mode 100644 index 0000000000..0bf40f3c92 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-9406.php @@ -0,0 +1,37 @@ += 8.0 + +namespace Bug9568; + +class Json +{ + public function decode( + string $jsonString, + bool $associative = false, + int $flags = JSON_THROW_ON_ERROR, + callable $onDecodeFail = null + ): mixed { + try { + return json_decode( + json: $jsonString, + associative: $associative, + flags: $flags, + ); + } catch (\Throwable $exception) { + if (isset($onDecodeFail)) { + return $onDecodeFail($exception); + } + } + + return null; + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/catch-with-unthrown-exception-stubs.php b/tests/PHPStan/Rules/Exceptions/data/catch-with-unthrown-exception-stubs.php new file mode 100644 index 0000000000..e9e6908560 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/catch-with-unthrown-exception-stubs.php @@ -0,0 +1,72 @@ +transactional(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + public function doFoo2(): void + { + try { + \MyFunction\doFoo(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + public function doFoo3(array $a): void + { + try { + uksort($a, function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + public function doFoo4(\Ds\Deque $deque): void + { + try { + $deque->filter(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/catch-with-unthrown-exception-stubs.stub b/tests/PHPStan/Rules/Exceptions/data/catch-with-unthrown-exception-stubs.stub new file mode 100644 index 0000000000..44993928da --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/catch-with-unthrown-exception-stubs.stub @@ -0,0 +1,49 @@ + + * @param-immediately-invoked-callable $callback + */ + public function filter(callable $callback = null): Deque + { + } + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/dead-catch-magic-methods.php b/tests/PHPStan/Rules/Exceptions/data/dead-catch-magic-methods.php new file mode 100644 index 0000000000..930e862583 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/dead-catch-magic-methods.php @@ -0,0 +1,69 @@ +magicMethod1(); + } catch (\Exception $e) { + + } + + try { + ClassWithMagicMethod::staticMagicMethod1(); + } catch (\Exception $e) { + // No error since `implicitThrows: true` is used by default + } + } +} + +/** + * @method void magicMethod2(); + * @method static void staticMagicMethod2(); + */ +class ClassWithMagicMethod2 +{ + /** + * @throws \Exception + */ + public function __call($name, $arguments) + { + throw new \Exception(); + } + + /** + * @throws void + */ + public static function __callStatic($name, $arguments) + { + } + + public function test() + { + try { + (new ClassWithMagicMethod2())->magicMethod2(); + } catch (\Exception $e) { + + } + + try { + ClassWithMagicMethod2::staticMagicMethod2(); + } catch (\Exception $e) { + + } + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/immediately-called-arrow-function.php b/tests/PHPStan/Rules/Exceptions/data/immediately-called-arrow-function.php new file mode 100644 index 0000000000..307639c826 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/immediately-called-arrow-function.php @@ -0,0 +1,41 @@ += 8.0 + +namespace ImmediatelyCalledArrowFunction; + +class ImmediatelyCalledCallback +{ + + /** + * @throws \InvalidArgumentException + */ + public function doFoo(array $a): void + { + array_map(fn () => throw new \InvalidArgumentException(), $a); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo2(array $a): void + { + $cb = fn () => throw new \InvalidArgumentException(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo3(array $a): void + { + $f = fn () => throw new \InvalidArgumentException(); + $f(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo4(array $a): void + { + (fn () => throw new \InvalidArgumentException())(); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/immediately-called-fcc.php b/tests/PHPStan/Rules/Exceptions/data/immediately-called-fcc.php new file mode 100644 index 0000000000..35d36afcac --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/immediately-called-fcc.php @@ -0,0 +1,82 @@ += 8.1 + +namespace ImmediatelyCalledFcc; + +class Foo +{ + + /** + * @throws \InvalidArgumentException + */ + public function doFoo(): void + { + $f = function () { + throw new \InvalidArgumentException(); + }; + $g = $f(...); + $g(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo2(): void + { + $f = fn () => throw new \InvalidArgumentException(); + $g = $f(...); + $g(); + } + + /** + * @throws \InvalidArgumentException + */ + public function throwsInvalidArgumentException() + { + throw new \InvalidArgumentException(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo3(): void + { + $f = $this->throwsInvalidArgumentException(...); + $f(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo4(): void + { + $f = alsoThrowsInvalidArgumentException(...); + $f(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo5(): void + { + $f = [$this, 'throwsInvalidArgumentException']; + $f(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo6(): void + { + $f = 'ImmediatelyCalledFcc\\alsoThrowsInvalidArgumentException'; + $f(); + } + +} + +/** + * @throws \InvalidArgumentException + */ +function alsoThrowsInvalidArgumentException() +{ + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php b/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php index 8914d535d9..7df5ea9915 100644 --- a/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php +++ b/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php @@ -85,4 +85,19 @@ private function throwsInterface(): void } + public function dateTimeZoneDoesNotThrow(): void + { + new \DateTimeZone('UTC'); + } + + public function dateTimeZoneDoesThrows(string $tz): void + { + new \DateTimeZone($tz); + } + + public function dateTimeZoneDoesNotThrowCaseInsensitive(): void + { + new \DaTetImezOnE('UTC'); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/data/noncapturing-catch.php b/tests/PHPStan/Rules/Exceptions/data/noncapturing-catch.php new file mode 100644 index 0000000000..a9e8a482cb --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/noncapturing-catch.php @@ -0,0 +1,17 @@ += 8.0 + +namespace NoncapturingCatch; + +class HelloWorld +{ + + public function hello(): void + { + try { + throw new \Exception('Hello'); + } catch (\Exception) { + echo 'Hi!'; + } + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/throw-class-exists.php b/tests/PHPStan/Rules/Exceptions/data/throw-class-exists.php new file mode 100644 index 0000000000..39c9dd13dc --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/throw-class-exists.php @@ -0,0 +1,19 @@ += 8.0 + +namespace ThrowExprValuesNullsafe; + +class Bar +{ + + function doException(): \Exception + { + return new \Exception(); + } + +} + +function doFoo(?Bar $bar) +{ + throw $bar?->doException(); +} diff --git a/tests/PHPStan/Rules/Exceptions/data/throw-values.php b/tests/PHPStan/Rules/Exceptions/data/throw-values.php new file mode 100644 index 0000000000..39d51de3ca --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/throw-values.php @@ -0,0 +1,66 @@ += 8.0 + +namespace ThrowExprValues; + +class InvalidException {}; +interface InvalidInterfaceException {}; +interface ValidInterfaceException extends \Throwable {}; + +/** + * @template T of \Exception + * @param class-string $genericExceptionClassName + * @param T $genericException + */ +function test($genericExceptionClassName, $genericException) { + /** @var ValidInterfaceException $validInterface */ + $validInterface = new \Exception(); + /** @var InvalidInterfaceException $invalidInterface */ + $invalidInterface = new \Exception(); + /** @var \Exception|null $nullableException */ + $nullableException = new \Exception(); + + if (rand(0, 1)) { + throw new \Exception(); + } + if (rand(0, 1)) { + throw $validInterface; + } + if (rand(0, 1)) { + throw 123; + } + if (rand(0, 1)) { + throw new InvalidException(); + } + if (rand(0, 1)) { + throw $invalidInterface; + } + if (rand(0, 1)) { + throw $nullableException; + } + if (rand(0, 1)) { + throw foo(); + } + if (rand(0, 1)) { + throw new NonexistentClass(); + } + if (rand(0, 1)) { + throw new $genericExceptionClassName; + } + if (rand(0, 1)) { + throw $genericException; + } +} + +function (\stdClass $foo) { + /** @var \Exception $foo */ + throw $foo; +}; + +function (\stdClass $foo) { + /** @var \Exception */ + throw $foo; +}; + +function (?\stdClass $foo) { + echo $foo ?? throw 1; +}; diff --git a/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-method.php b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-method.php index 819d89c87b..a5c79a5ec8 100644 --- a/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-method.php +++ b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-method.php @@ -147,3 +147,49 @@ public function doBaz(): void } } + +class ImmediatelyCalledCallback +{ + + /** + * @throws \InvalidArgumentException + */ + public function doFoo(array $a): void + { + array_map(function () { + throw new \InvalidArgumentException(); + }, $a); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo2(array $a): void + { + $cb = function () { + throw new \InvalidArgumentException(); + }; + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo3(array $a): void + { + $f = function () { + throw new \InvalidArgumentException(); + }; + $f(); + } + + /** + * @throws \InvalidArgumentException + */ + public function doFoo4(array $a): void + { + (function () { + throw new \InvalidArgumentException(); + })(); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-multi.php b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-multi.php new file mode 100644 index 0000000000..c0893a8ba1 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception-multi.php @@ -0,0 +1,187 @@ +throwLogicRangeJsonExceptions(); + + } catch (\JsonException $t) { + + } catch (\RangeException | \LogicException $t) { + + } + } + + public function doBaz() + { + try { + $this->throwLogicRangeJsonExceptions(); + + } catch (\JsonException $t) { + + } catch (\RangeException | \LogicException | \OverflowException $t) { // overflow not thrown + + } + } + + public function doBag() + { + try { + $this->throwLogicRangeJsonExceptions(); + + } catch (\RuntimeException $t) { + + } catch (\LogicException | \JsonException $t) { + + } + } + + public function doZag() + { + try { + throw new \RangeException(); + + } catch (\RuntimeException | \JsonException $t) { // json not thrown + + } + } + + public function doBal() + { + try { + $this->throwLogicRangeJsonExceptions(); + + } catch (\RuntimeException | \JsonException $t) { + + } catch (\InvalidArgumentException $t) { + + } + } + + public function doBap() + { + try { + $this->throwLogicRangeJsonExceptions(); + + } catch (\InvalidArgumentException $t) { + + } catch (\RuntimeException | \JsonException $t) { + + } + } + + public function doZaz() + { + try { + \ThrowPoints\Helpers\maybeThrows(); + } catch (\LogicException $e) { + + } + } + + public function doZab() + { + try { + \ThrowPoints\Helpers\maybeThrows(); + } catch (\InvalidArgumentException | \LogicException $e) { + + } + } + + public function someThrowableTest(): void + { + try { + $this->throwIae(); + } catch (\InvalidArgumentException | \Exception $e) { + + } catch (\Throwable $e) { + + } + } + + public function someThrowableTest2(): void + { + try { + $this->throwIae(); + } catch (\RuntimeException | \Throwable $e) { + // IAE is not runtime, dead + } catch (\Throwable $e) { + + } + } + + public function someThrowableTest3(): void + { + try { + $this->throwIae(); + } catch (\Throwable $e) { + + } catch (\Throwable $e) { + + } + } + + + public function someThrowableTest4(): void + { + try { + $this->throwIae(); + } catch (\Throwable $e) { + + } catch (\InvalidArgumentException $e) { + + } + } + + public function someThrowableTest5(): void + { + try { + $this->throwIae(); + } catch (\InvalidArgumentException $e) { + + } catch (\InvalidArgumentException $e) { + + } + } + + public function someThrowableTest6(): void + { + try { + $this->throwIae(); + } catch (\InvalidArgumentException | \Exception $e) { + // catch can be simplified, this is not reported + } + } + + /** + * @throws \RangeException + * @throws \LogicException + * @throws \JsonException + */ + private function throwLogicRangeJsonExceptions(): void + { + + } + + /** @throws \InvalidArgumentException */ + public function throwIae(): void + { + + } + + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php index 78236201ac..d501e1cffc 100644 --- a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php +++ b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php @@ -544,3 +544,203 @@ public function doBar(string $s) } } + +class TestCaseInsensitiveClassNames +{ + + public function doFoo(): void + { + try { + new \SimpleXmlElement(''); + } catch (\Exception $e) { + + } + } + + public function doBar(): void + { + try { + new \SimpleXmlElement('foo'); + } catch (\Exception $e) { + + } + } + + public function doBaz(string $string): void + { + try { + new \SimpleXmlElement($string); + } catch (\Exception $e) { + + } + } + +} + +/** @throws void */ +function acceptCallable(callable $cb): void +{ + +} + +/** + * @throws void + * @param-later-invoked-callable $cb + */ +function acceptCallableAndCallLater(callable $cb): void +{ + +} + +class CallCallable +{ + + /** + * @throws void + */ + public function doFoo(callable $cb): void + { + try { + $cb(); + } catch (\Exception $e) { + + } + } + + public function passCallableToFunction(): void + { + try { + // immediately called by default + acceptCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + public function passCallableToFunction2(): void + { + try { + // later called thanks to @param-later-invoked-callable + acceptCallableAndCallLater(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + /** @throws void */ + public function acceptCallable(callable $cb): void + { + + } + + public function passCallableToMethod(): void + { + try { + // later called by default + $this->acceptCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + + /** + * @throws void + * @param-immediately-invoked-callable $cb + */ + public function acceptAndCallCallable(callable $cb): void + { + + } + + public function passCallableToMethod2(): void + { + try { + // immediately called thanks to @param-immediately-invoked-callable + $this->acceptAndCallCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + +} + +class ExtendsCallCallable extends CallCallable +{ + + public function acceptAndCallCallable(callable $cb): void + { + + } + + public function passCallableToMethod2(): void + { + try { + // immediately called thanks to @param-immediately-invoked-callable + $this->acceptAndCallCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + +} + +class ExtendsCallCallable2 extends CallCallable +{ + + /** + * @param callable $cb + */ + public function acceptAndCallCallable(callable $cb): void + { + + } + + public function passCallableToMethod2(): void + { + try { + // immediately called thanks to @param-immediately-invoked-callable + $this->acceptAndCallCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + +} + +class ExtendsCallCallable3 extends CallCallable +{ + + /** + * @param callable $cb + * @param-later-invoked-callable $cb + */ + public function acceptAndCallCallable(callable $cb): void + { + + } + + public function passCallableToMethod2(): void + { + try { + // later called thanks to @param-later-invoked-callable + $this->acceptAndCallCallable(function () { + throw new \InvalidArgumentException(); + }); + } catch (\InvalidArgumentException $e) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/disable-detect-multi-catch.neon b/tests/PHPStan/Rules/Exceptions/disable-detect-multi-catch.neon new file mode 100644 index 0000000000..e763557205 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/disable-detect-multi-catch.neon @@ -0,0 +1,3 @@ +parameters: + featureToggles: + detectDeadTypeInMultiCatch: false diff --git a/tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php b/tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php index bb60484ed6..95ac9ed295 100644 --- a/tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php @@ -11,13 +11,16 @@ class ArrayFilterRuleTest extends RuleTestCase { + private bool $treatPhpDocTypesAsCertain = true; + protected function getRule(): Rule { - return new ArrayFilterRule($this->createReflectionProvider()); + return new ArrayFilterRule($this->createReflectionProvider(), $this->treatPhpDocTypesAsCertain); } public function testFile(): void { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $expectedErrors = [ [ 'Parameter #1 $array (array{1, 3}) to function array_filter does not contain falsy values, the array will always stay the same.', @@ -38,6 +41,7 @@ public function testFile(): void [ 'Parameter #1 $array (array) to function array_filter does not contain falsy values, the array will always stay the same.', 20, + $tipText, ], [ 'Parameter #1 $array (array{0}) to function array_filter contains falsy values only, the result will always be an empty array.', @@ -58,6 +62,7 @@ public function testFile(): void [ 'Parameter #1 $array (array) to function array_filter contains falsy values only, the result will always be an empty array.', 27, + $tipText, ], [ 'Parameter #1 $array (array{}) to function array_filter is empty, call has no effect.', @@ -68,4 +73,26 @@ public function testFile(): void $this->analyse([__DIR__ . '/data/array_filter_empty.php'], $expectedErrors); } + public function testBug2065WithPhpDocTypesAsCertain(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + + $expectedErrors = [ + [ + 'Parameter #1 $array (array) to function array_filter does not contain falsy values, the array will always stay the same.', + 12, + $tipText, + ], + ]; + + $this->analyse([__DIR__ . '/data/bug-array-filter.php'], $expectedErrors); + } + + public function testBug2065WithoutPhpDocTypesAsCertain(): void + { + $this->treatPhpDocTypesAsCertain = false; + + $this->analyse([__DIR__ . '/data/bug-array-filter.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php b/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php new file mode 100644 index 0000000000..c945b1efea --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php @@ -0,0 +1,90 @@ + + */ +class ArrayValuesRuleTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain = true; + + protected function getRule(): Rule + { + return new ArrayValuesRule($this->createReflectionProvider(), $this->treatPhpDocTypesAsCertain); + } + + public function testFile(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $expectedErrors = [ + [ + 'Parameter #1 $array (array{0, 1, 3}) of array_values is already a list, call has no effect.', + 8, + ], + [ + 'Parameter #1 $array (array{1, 3}) of array_values is already a list, call has no effect.', + 9, + ], + [ + 'Parameter #1 $array (array{\'test\'}) of array_values is already a list, call has no effect.', + 10, + ], + [ + 'Parameter #1 $array (array{\'\', \'test\'}) of array_values is already a list, call has no effect.', + 12, + ], + [ + 'Parameter #1 $array (list) of array_values is already a list, call has no effect.', + 14, + $tipText, + ], + [ + 'Parameter #1 $array (array{0}) of array_values is already a list, call has no effect.', + 17, + ], + [ + 'Parameter #1 $array (array{null, null}) of array_values is already a list, call has no effect.', + 19, + ], + [ + 'Parameter #1 $array (array{null, 0}) of array_values is already a list, call has no effect.', + 20, + ], + [ + 'Parameter #1 $array (array{}) to function array_values is empty, call has no effect.', + 21, + ], + [ + 'Parameter #1 $array (array{}) to function array_values is empty, call has no effect.', + 25, + $tipText, + ], + ]; + + if (PHP_VERSION_ID >= 80000) { + $expectedErrors[] = [ + 'Parameter #1 $array (list) of array_values is already a list, call has no effect.', + 28, + $tipText, + ]; + } else { + $expectedErrors[] = [ + 'Parameter #1 $array (true) to function array_values is empty, call has no effect.', + 27, + ]; + $expectedErrors[] = [ + 'Parameter #1 $array (true) to function array_values is empty, call has no effect.', + 28, + ]; + } + + $this->analyse([__DIR__ . '/data/array_values_list.php'], $expectedErrors); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php index 61c726ada6..7f3aab6ac2 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -26,7 +28,7 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), @@ -37,7 +39,11 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, ), ); } diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php index e5f4c2a5fd..bf46cd56a6 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php @@ -23,6 +23,8 @@ protected function getRule(): Rule true, false, false, + true, + false, ))); } @@ -37,6 +39,10 @@ public function testRule(): void 'Anonymous function should return int but returns string.', 14, ], + [ + 'Anonymous function should never return but return statement found.', + 44, + ], ]); } @@ -54,4 +60,18 @@ public function testBug8179(): void $this->analyse([__DIR__ . '/data/bug-8179.php'], []); } + public function testBugSpaceship(): void + { + $this->analyse([__DIR__ . '/data/bug-spaceship.php'], []); + } + + public function testBugFunctionMethodConstants(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->analyse([__DIR__ . '/data/bug-anonymous-function-method-constant.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index a14be6743e..bcd1c9ee87 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -22,7 +22,7 @@ class CallCallablesRuleTest extends RuleTestCase protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, false); + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, false, true, false); return new CallCallablesRule( new FunctionCallParametersCheck( $ruleLevelHelper, @@ -43,7 +43,7 @@ protected function getRule(): Rule public function testRule(): void { - $this->analyse([__DIR__ . '/data/callables.php'], [ + $errors = [ [ 'Trying to invoke string but it might not be a callable.', 17, @@ -145,7 +145,16 @@ public function testRule(): void 'Trying to invoke array{\'CallCallables\\\ConstantArrayUnionCallables\', \'doBaz\'|\'doFoo\'} but it might not be a callable.', 212, ], - ]); + ]; + + if (PHP_VERSION_ID >= 80000) { + $errors[] = [ + 'Trying to invoke array{\'CallCallables\\\ConstantArrayUnionCallables\'|\'CallCallables\\\ConstantArrayUnionCallablesTest\', \'doBar\'|\'doFoo\'} but it\'s not a callable.', + 220, + ]; + } + + $this->analyse([__DIR__ . '/data/callables.php'], $errors); } public function testNamedArguments(): void @@ -181,7 +190,7 @@ public function dataBug3566(): array true, [ [ - 'Parameter #1 $ of closure expects int, TMemberType given.', + 'Parameter #1 of closure expects int, TMemberType given.', 29, ], ], @@ -195,7 +204,7 @@ public function dataBug3566(): array /** * @dataProvider dataBug3566 - * @param mixed[] $errors + * @param list $errors */ public function testBug3566(bool $checkExplicitMixed, array $errors): void { @@ -257,4 +266,54 @@ public function testBug6701(): void ]); } + public function testStaticCallInFunctions(): void + { + $this->analyse([__DIR__ . '/data/static-call-in-functions.php'], []); + } + + public function testBug5867(): void + { + $this->analyse([__DIR__ . '/data/bug-5867.php'], []); + } + + public function testBug6485(): void + { + $this->analyse([__DIR__ . '/data/bug-6485.php'], [ + [ + 'Parameter #1 of closure expects never, TBlockType of Bug6485\Block given.', + 33, + ], + ]); + } + + public function testBug6633(): void + { + $this->analyse([__DIR__ . '/data/bug-6633.php'], []); + } + + public function testBug3818b(): void + { + $this->analyse([__DIR__ . '/data/bug-3818b.php'], []); + } + + public function testBug9594(): void + { + $this->analyse([__DIR__ . '/data/bug-9594.php'], []); + } + + public function testBug9614(): void + { + $this->analyse([__DIR__ . '/data/bug-9614.php'], []); + } + + public function testBug10814(): void + { + $this->analyse([__DIR__ . '/data/bug-10814.php'], [ + [ + 'Parameter #1 of closure expects DateTime, DateTimeImmutable given.', + 10, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 43a315f838..1a8770dd05 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -26,7 +26,7 @@ protected function getRule(): Rule $broker = $this->createReflectionProvider(); return new CallToFunctionParametersRule( $broker, - new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true), + new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true), ); } @@ -297,6 +297,10 @@ public function testPassingNonVariableToParameterPassedByReference(): void 'Parameter #1 $array of function reset expects array|object, null given.', 39, ], + [ + 'Parameter #1 $s of function PassedByReference\bar expects string, int given.', + 48, + ], ]); } @@ -550,16 +554,18 @@ public function testArrayReduceCallback(): void { $this->analyse([__DIR__ . '/data/array_reduce.php'], [ [ - 'Parameter #2 $callback of function array_reduce expects callable(string, int): string, Closure(string, string): string given.', + 'Parameter #2 $callback of function array_reduce expects callable(string, 1|2|3): string, Closure(string, string): string given.', 5, ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-empty-string given.', 13, + 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-empty-string given.', 22, + 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], ]); } @@ -568,16 +574,18 @@ public function testArrayReduceArrowFunctionCallback(): void { $this->analyse([__DIR__ . '/data/array_reduce_arrow.php'], [ [ - 'Parameter #2 $callback of function array_reduce expects callable(string, int): string, Closure(string, string): string given.', + 'Parameter #2 $callback of function array_reduce expects callable(string, 1|2|3): string, Closure(string, string): string given.', 5, ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-empty-string given.', 11, + 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', + 'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, 1|2|3): (non-empty-string|null), Closure(string, int): non-empty-string given.', 18, + 'Type string of parameter #1 $foo of passed callable needs to be same or wider than parameter type string|null of accepting callable.', ], ]); } @@ -596,6 +604,7 @@ public function testArrayWalkCallback(): void [ 'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\'): mixed, Closure(int, string, int): \'\' given.', 23, + 'Parameter #3 $extra of passed callable is required but accepting callable does not have that parameter. It will be called without it.', ], ]); } @@ -614,6 +623,7 @@ public function testArrayWalkArrowFunctionCallback(): void [ 'Parameter #2 $callback of function array_walk expects callable(1|2, \'bar\'|\'foo\'): mixed, Closure(int, string, int): \'\' given.', 19, + 'Parameter #3 $extra of passed callable is required but accepting callable does not have that parameter. It will be called without it.', ], ]); } @@ -622,11 +632,11 @@ public function testArrayUdiffCallback(): void { $this->analyse([__DIR__ . '/data/array_udiff.php'], [ [ - 'Parameter #3 $data_comp_func of function array_udiff expects callable(int, int): int<-1, 1>, Closure(string, string): string given.', + 'Parameter #3 $data_comp_func of function array_udiff expects callable(1|2|3|4|5|6, 1|2|3|4|5|6): int, Closure(string, string): string given.', 6, ], [ - 'Parameter #3 $data_comp_func of function array_udiff expects callable(int, int): int<-1, 1>, Closure(int, int): non-falsy-string given.', + 'Parameter #3 $data_comp_func of function array_udiff expects callable(1|2|3|4|5|6, 1|2|3|4|5|6): int, Closure(int, int): non-falsy-string given.', 14, ], [ @@ -638,7 +648,7 @@ public function testArrayUdiffCallback(): void 21, ], [ - 'Parameter #3 $data_comp_func of function array_udiff expects callable(string, string): int<-1, 1>, Closure(string, int): non-empty-string given.', + 'Parameter #3 $data_comp_func of function array_udiff expects callable(string, string): int, Closure(string, int): non-empty-string given.', 22, ], ]); @@ -692,7 +702,7 @@ public function testUasortCallback(): void { $this->analyse([__DIR__ . '/data/uasort.php'], [ [ - 'Parameter #2 $callback of function uasort expects callable(int, int): int, Closure(string, string): 1 given.', + 'Parameter #2 $callback of function uasort expects callable(1|2|3, 1|2|3): int, Closure(string, string): 1 given.', 7, ], ]); @@ -702,7 +712,7 @@ public function testUasortArrowFunctionCallback(): void { $this->analyse([__DIR__ . '/data/uasort_arrow.php'], [ [ - 'Parameter #2 $callback of function uasort expects callable(int, int): int, Closure(string, string): 1 given.', + 'Parameter #2 $callback of function uasort expects callable(1|2|3, 1|2|3): int, Closure(string, string): 1 given.', 7, ], ]); @@ -712,7 +722,7 @@ public function testUsortCallback(): void { $this->analyse([__DIR__ . '/data/usort.php'], [ [ - 'Parameter #2 $callback of function usort expects callable(int, int): int, Closure(string, string): 1 given.', + 'Parameter #2 $callback of function usort expects callable(1|2|3, 1|2|3): int, Closure(string, string): 1 given.', 14, ], ]); @@ -722,7 +732,7 @@ public function testUsortArrowFunctionCallback(): void { $this->analyse([__DIR__ . '/data/usort_arrow.php'], [ [ - 'Parameter #2 $callback of function usort expects callable(int, int): int, Closure(string, string): 1 given.', + 'Parameter #2 $callback of function usort expects callable(1|2|3, 1|2|3): int, Closure(string, string): 1 given.', 14, ], ]); @@ -732,7 +742,7 @@ public function testUksortCallback(): void { $this->analyse([__DIR__ . '/data/uksort.php'], [ [ - 'Parameter #2 $callback of function uksort expects callable(string, string): int, Closure(stdClass, stdClass): 1 given.', + 'Parameter #2 $callback of function uksort expects callable(\'one\'|\'three\'|\'two\', \'one\'|\'three\'|\'two\'): int, Closure(stdClass, stdClass): 1 given.', 14, ], [ @@ -746,7 +756,7 @@ public function testUksortArrowFunctionCallback(): void { $this->analyse([__DIR__ . '/data/uksort_arrow.php'], [ [ - 'Parameter #2 $callback of function uksort expects callable(string, string): int, Closure(stdClass, stdClass): 1 given.', + 'Parameter #2 $callback of function uksort expects callable(\'one\'|\'three\'|\'two\', \'one\'|\'three\'|\'two\'): int, Closure(stdClass, stdClass): 1 given.', 14, ], [ @@ -811,8 +821,9 @@ public function testProcOpen(): void $this->analyse([__DIR__ . '/data/proc_open.php'], [ [ - 'Parameter #1 $command of function proc_open expects array|string, array given.', + "Parameter #1 \$command of function proc_open expects list|string, array{something: 'bogus', in: 'here'} given.", 6, + "Type #1 from the union: array{something: 'bogus', in: 'here'} is not a list.", ], ]); } @@ -861,14 +872,15 @@ public function testArrayFilterCallback(bool $checkExplicitMixed): void $this->checkExplicitMixed = $checkExplicitMixed; $errors = [ [ - 'Parameter #2 $callback of function array_filter expects callable(int): mixed, Closure(string): true given.', + 'Parameter #2 $callback of function array_filter expects (callable(int): bool)|null, Closure(string): true given.', 17, ], ]; if ($checkExplicitMixed) { $errors[] = [ - 'Parameter #2 $callback of function array_filter expects callable(mixed): mixed, Closure(int): true given.', + 'Parameter #2 $callback of function array_filter expects (callable(mixed): bool)|null, Closure(int): true given.', 20, + 'Type #1 from the union: Type int of parameter #1 $i of passed callable needs to be same or wider than parameter type mixed of accepting callable.', ]; } $this->analyse([__DIR__ . '/data/array_filter_callback.php'], $errors); @@ -902,7 +914,7 @@ public function testBug2782(): void { $this->analyse([__DIR__ . '/data/bug-2782.php'], [ [ - 'Parameter #2 $callback of function usort expects callable(stdClass, stdClass): int, Closure(int, int): -1|1 given.', + 'Parameter #2 $callback of function usort expects callable(stdClass, stdClass): int, Closure(int, int): (-1|1) given.', 13, ], ]); @@ -1090,10 +1102,14 @@ public function testDiscussion7450WithCheckExplicitMixed(): void [ 'Parameter #1 $foo of function Discussion7450\foo expects array{policy: non-empty-string, entitlements: array}, array{policy: mixed, entitlements: mixed} given.', 18, + "• Offset 'policy' (non-empty-string) does not accept type mixed. +• Offset 'entitlements' (array) does not accept type mixed.", ], [ 'Parameter #1 $foo of function Discussion7450\foo expects array{policy: non-empty-string, entitlements: array}, array{policy: mixed, entitlements: mixed} given.', 28, + "• Offset 'policy' (non-empty-string) does not accept type mixed. +• Offset 'entitlements' (array) does not accept type mixed.", ], ]); } @@ -1118,6 +1134,12 @@ public function testBug5474(): void ]); } + public function testBug6261(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6261.php'], []); + } + public function testBug6781(): void { $this->analyse([__DIR__ . '/data/bug-6781.php'], []); @@ -1183,41 +1205,468 @@ public function testCurlSetOpt(): void $this->analyse([__DIR__ . '/data/curl_setopt.php'], [ [ 'Parameter #3 $value of function curl_setopt expects 0|2, bool given.', - 8, + 10, ], [ 'Parameter #3 $value of function curl_setopt expects non-empty-string, int given.', - 14, + 16, ], [ - 'Parameter #3 $value of function curl_setopt expects array, int given.', - 15, + 'Parameter #3 $value of function curl_setopt expects array, int given.', + 17, ], [ 'Parameter #3 $value of function curl_setopt expects bool, int given.', - 17, + 19, ], [ 'Parameter #3 $value of function curl_setopt expects bool, string given.', - 18, + 20, ], [ 'Parameter #3 $value of function curl_setopt expects int, string given.', - 20, + 22, ], [ 'Parameter #3 $value of function curl_setopt expects array, string given.', - 22, + 24, ], [ 'Parameter #3 $value of function curl_setopt expects resource, string given.', - 24, + 26, ], [ 'Parameter #3 $value of function curl_setopt expects array|string, int given.', + 28, + ], + [ + 'Parameter #3 $value of function curl_setopt expects array, array given.', + 67, + ], + ]); + } + + public function testBug8280(): void + { + $this->analyse([__DIR__ . '/data/bug-8280.php'], []); + } + + public function testBug8389(): void + { + $this->analyse([__DIR__ . '/data/bug-8389.php'], []); + } + + public function testBug8449(): void + { + $this->analyse([__DIR__ . '/data/bug-8449.php'], []); + } + + public function testBug5288(): void + { + $this->analyse([__DIR__ . '/data/bug-5288.php'], []); + } + + public function testBug5986(): void + { + $this->analyse([__DIR__ . '/data/bug-5986.php'], [ + [ + 'Parameter #1 $data of function Bug5986\test2 expects array{mov?: int, appliesTo?: string, expireDate?: string|null, effectiveFrom?: int, merchantId?: int, link?: string, channel?: string, voucherExternalId?: int}, array{mov?: int, appliesTo?: string, expireDate?: string|null, effectiveFrom?: string, merchantId?: int, link?: string, channel?: string, voucherExternalId?: int} given.', + 18, + "Offset 'effectiveFrom' (int) does not accept type string.", + ], + ]); + } + + public function testBug7239(): void + { + $tipText = 'array{} is empty.'; + $this->analyse([__DIR__ . '/../../Analyser/data/bug-7239.php'], [ + [ + 'Parameter #1 ...$arg1 of function max expects non-empty-array, array{} given.', + 14, + $tipText, + ], + [ + 'Parameter #1 ...$arg1 of function min expects non-empty-array, array{} given.', + 15, + $tipText, + ], + [ + 'Parameter #1 ...$arg1 of function max expects non-empty-array, array{} given.', + 21, + $tipText, + ], + [ + 'Parameter #1 ...$arg1 of function min expects non-empty-array, array{} given.', + 22, + $tipText, + ], + [ + 'Parameter #1 ...$arg1 of function max expects non-empty-array, array{} given.', + 32, + $tipText, + ], + [ + 'Parameter #1 ...$arg1 of function min expects non-empty-array, array{} given.', + 33, + $tipText, + ], + ]); + } + + public function testFilterInputType(): void + { + $errors = [ + [ + 'Parameter #1 $type of function filter_input expects 0|1|2|4|5, -1 given.', + 16, + ], + [ + 'Parameter #1 $type of function filter_input expects 0|1|2|4|5, int given.', + 17, + ], + [ + 'Parameter #1 $type of function filter_input_array expects 0|1|2|4|5, -1 given.', + 28, + ], + [ + 'Parameter #1 $type of function filter_input_array expects 0|1|2|4|5, int given.', + 29, + ], + ]; + + if (PHP_VERSION_ID < 80000) { + $errors = []; + } + + $this->analyse([__DIR__ . '/data/filter-input-type.php'], $errors); + } + + public function testBug9283(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/bug-9283.php'], []); + } + + public function testBug9380(): void + { + $errors = [ + [ + 'Parameter #2 $message_type of function error_log expects 0|1|3|4, 2 given.', + 7, + ], + ]; + + if (PHP_VERSION_ID < 80000) { + $errors = []; + } + + $this->analyse([__DIR__ . '/data/bug-9380.php'], $errors); + } + + public function testBenevolentSuperglobalKeys(): void + { + $this->analyse([__DIR__ . '/data/benevolent-superglobal-keys.php'], []); + } + + public function testFileParams(): void + { + $this->analyse([__DIR__ . '/data/file.php'], [ + [ + 'Parameter #2 $flags of function file expects 0|1|2|3|4|5|6|7|16|17|18|19|20|21|22|23, 8 given.', + 16, + ], + ]); + } + + public function testFlockParams(): void + { + $this->analyse([__DIR__ . '/data/flock.php'], [ + [ + 'Parameter #2 $operation of function flock expects int<0, 7>, 8 given.', + 45, + ], + ]); + } + + public function testJsonValidate(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3'); + } + + $this->analyse([__DIR__ . '/data/json_validate.php'], [ + [ + 'Parameter #2 $depth of function json_validate expects int<1, max>, 0 given.', + 6, + ], + [ + 'Parameter #3 $flags of function json_validate expects 0|1048576, 2 given.', + 7, + ], + ]); + } + + public function testBug4612(): void + { + $this->analyse([__DIR__ . '/data/bug-4612.php'], []); + } + + public function testBug2508(): void + { + $this->analyse([__DIR__ . '/data/bug-2508.php'], []); + } + + public function testBug6175(): void + { + $this->analyse([__DIR__ . '/data/bug-6175.php'], []); + } + + public function testBug9699(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/bug-9699.php'], [ + [ + 'Parameter #1 $f of function Bug9699\int_int_int_string expects Closure(int, int, int, string): int, Closure(int, int, int ...): int given.', + 19, + ], + ]); + } + + public function testBug9133(): void + { + $this->analyse([__DIR__ . '/data/bug-9133.php'], [ + [ + 'Parameter #1 $value of function Bug9133\assertNever expects never, int given.', + 29, + ], + ]); + } + + public function testBug9803(): void + { + $this->analyse([__DIR__ . '/data/bug-9803.php'], []); + } + + public function testBug9018(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/bug-9018.php'], [ + [ + 'Unknown parameter $str1 in call to function levenshtein.', + 13, + ], + [ + 'Unknown parameter $str2 in call to function levenshtein.', + 13, + ], + [ + 'Missing parameter $string1 (string) in call to function levenshtein.', + 13, + ], + [ + 'Missing parameter $string2 (string) in call to function levenshtein.', + 13, + ], + ]); + } + + public function testBug9399(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/bug-9399.php'], []); + } + + public function testBug9923(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/bug-9923.php'], []); + } + + public function testBug9823(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/bug-9823.php'], []); + } + + public function testNamedParametersForMultiVariantFunctions(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/call-to-function-named-params-multivariant.php'], []); + } + + public function testBug9793(): void + { + $errors = []; + + if (PHP_VERSION_ID < 80200) { + $errors = [ + [ + 'Parameter #1 $iterator of function iterator_to_array expects Traversable, array given.', + 13, + ], + [ + 'Parameter #1 $iterator of function iterator_to_array expects Traversable, array|Iterator given.', + 14, + ], + [ + 'Parameter #1 $iterator of function iterator_count expects Traversable, array given.', + 15, + ], + [ + 'Parameter #1 $iterator of function iterator_count expects Traversable, array|Iterator given.', + 16, + ], + ]; + } + + $errors[] = [ + 'Parameter #1 $iterator of function iterator_apply expects Traversable, array given.', + 17, + ]; + $errors[] = [ + 'Parameter #1 $iterator of function iterator_apply expects Traversable, array|Iterator given.', + 18, + ]; + + $this->analyse([__DIR__ . '/data/bug-9793.php'], $errors); + } + + public function testCallToArrayFilterWithNullCallback(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/array_filter_null_callback.php'], []); + } + + public function testBug10171(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/bug-10171.php'], [ + [ + 'Unknown parameter $samesite in call to function setcookie.', + 12, + ], + [ + 'Function setcookie invoked with 9 parameters, 1-7 required.', + 13, + ], + [ + 'Unknown parameter $samesite in call to function setrawcookie.', + 25, + ], + [ + 'Function setrawcookie invoked with 9 parameters, 1-7 required.', 26, ], ]); } + public function testBug6720(): void + { + $this->analyse([__DIR__ . '/data/bug-6720.php'], []); + } + + public function testBug8659(): void + { + $this->analyse([__DIR__ . '/data/bug-8659.php'], []); + } + + public function testBug9580(): void + { + $this->analyse([__DIR__ . '/data/bug-9580.php'], []); + } + + public function testBug7283(): void + { + $this->analyse([__DIR__ . '/data/bug-7283.php'], []); + } + + public function testBug9697(): void + { + $this->analyse([__DIR__ . '/data/bug-9697.php'], []); + } + + public function testDiscussion10454(): void + { + $this->analyse([__DIR__ . '/data/discussion-10454.php'], [ + [ + "Parameter #2 \$callback of function array_filter expects (callable('bar'|'baz'|'foo'|'quux'|'qux'): bool)|null, Closure(string): stdClass given.", + 13, + ], + [ + "Parameter #2 \$callback of function array_filter expects (callable('bar'|'baz'|'foo'|'quux'|'qux'): bool)|null, Closure(string): stdClass given.", + 23, + ], + ]); + } + + public function testBug10626(): void + { + $this->analyse([__DIR__ . '/data/bug-10626.php'], [ + [ + 'Parameter #1 $value of function Bug10626\intByValue expects int, string given.', + 16, + ], + [ + 'Parameter #1 $value of function Bug10626\intByReference expects int, string given.', + 17, + ], + ]); + } + + public function testArgon2PasswordHash(): void + { + $this->analyse([__DIR__ . '/data/argon2id-password-hash.php'], []); + } + + public function testParamClosureThis(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->analyse([__DIR__ . '/data/function-call-param-closure-this.php'], [ + [ + 'Parameter #1 $cb of function FunctionCallParamClosureThis\acceptClosure expects bindable closure, static closure given.', + 18, + ], + [ + 'Parameter #1 $cb of function FunctionCallParamClosureThis\acceptClosure expects bindable closure, static closure given.', + 23, + ], + ]); + } + + public function testBug10297(): void + { + $this->analyse([__DIR__ . '/data/bug-10297.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php index d53dc16668..85c1f400ee 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -23,6 +24,33 @@ public function testRule(): void 'Call to function sprintf() on a separate line has no effect.', 13, ], + [ + 'Call to function var_export() on a separate line has no effect.', + 24, + ], + [ + 'Call to function print_r() on a separate line has no effect.', + 26, + ], + ]); + + if (PHP_VERSION_ID < 80000) { + return; + } + + $this->analyse([__DIR__ . '/data/function-call-statement-no-side-effects-8.0.php'], [ + [ + 'Call to function var_export() on a separate line has no effect.', + 19, + ], + [ + 'Call to function print_r() on a separate line has no effect.', + 20, + ], + [ + 'Call to function highlight_string() on a separate line has no effect.', + 21, + ], ]); } @@ -43,9 +71,17 @@ public function testPhpDoc(): void 10, ], [ - 'Call to function FunctionCallStatementNoSideEffectsPhpDoc\pureAndThrowsVoid() on a separate line has no effect.', + 'Call to function FunctionCallStatementNoSideEffectsPhpDoc\pure4() on a separate line has no effect.', 11, ], + [ + 'Call to function FunctionCallStatementNoSideEffectsPhpDoc\pure5() on a separate line has no effect.', + 12, + ], + [ + 'Call to function FunctionCallStatementNoSideEffectsPhpDoc\pureAndThrowsVoid() on a separate line has no effect.', + 13, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php b/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php index 0f5876e7bc..15734fdf18 100644 --- a/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php @@ -35,7 +35,6 @@ public function testCallToNonexistentFunction(): void 'Function foobarNonExistentFunction not found.', 5, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', - ], ]); } @@ -47,7 +46,6 @@ public function testCallToNonexistentNestedFunction(): void 'Function barNonExistentFunction not found.', 5, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', - ], ]); } @@ -97,6 +95,61 @@ public function testMatchExprAnalysis(): void ]); } + public function testCallToRemovedFunctionsOnPhp8(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/removed-functions-from-php8.php'], [ + [ + 'Function convert_cyr_string not found.', + 3, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function ezmlm_hash not found.', + 4, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function fgetss not found.', + 5, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function get_magic_quotes_gpc not found.', + 6, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function hebrevc not found.', + 7, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function imap_header not found.', + 8, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function ldap_control_paged_result not found.', + 9, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function ldap_control_paged_result_response not found.', + 10, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function restore_include_path not found.', + 11, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + public function testCreateFunctionPhp8(): void { if (PHP_VERSION_ID < 80000) { @@ -186,4 +239,23 @@ public function testBug8058b(): void ]); } + public function testBug8205(): void + { + $this->analyse([__DIR__ . '/data/bug-8205.php'], []); + } + + public function testBug10003(): void + { + $this->analyse([__DIR__ . '/data/bug-10003.php'], [ + [ + 'Call to function MongoDB\Driver\Monitoring\addSubscriber() with incorrect case: MONGODB\Driver\Monitoring\addSubscriber', + 10, + ], + [ + 'Call to function MongoDB\Driver\Monitoring\addSubscriber() with incorrect case: mongodb\driver\monitoring\addsubscriber', + 14, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php new file mode 100644 index 0000000000..9eed739399 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php @@ -0,0 +1,86 @@ + + */ +class CallUserFuncRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new CallUserFuncRule($reflectionProvider, new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, true, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true)); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/call-user-func.php'], [ + [ + 'Callable passed to call_user_func() invoked with 0 parameters, 1 required.', + 15, + ], + [ + 'Parameter #1 $i of callable passed to call_user_func() expects int, string given.', + 17, + ], + [ + 'Parameter $i of callable passed to call_user_func() expects int, string given.', + 18, + ], + [ + 'Parameter $i of callable passed to call_user_func() expects int, string given.', + 19, + ], + [ + 'Unknown parameter $j in call to callable passed to call_user_func().', + 22, + ], + [ + 'Missing parameter $i (int) in call to callable passed to call_user_func().', + 22, + ], + [ + 'Callable passed to call_user_func() invoked with 0 parameters, 2-4 required.', + 30, + ], + [ + 'Callable passed to call_user_func() invoked with 1 parameter, 2-4 required.', + 31, + ], + [ + 'Callable passed to call_user_func() invoked with 0 parameters, at least 2 required.', + 40, + ], + [ + 'Callable passed to call_user_func() invoked with 1 parameter, at least 2 required.', + 41, + ], + [ + 'Result of callable passed to call_user_func() (void) is used.', + 43, + ], + ]); + } + + public function testBug7057(): void + { + $this->analyse([__DIR__ . '/data/bug-7057.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php index e18afd120d..f4a3d5a1bb 100644 --- a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -26,7 +28,7 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), @@ -37,7 +39,11 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, ), ); } diff --git a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php index cfd120be47..a62e35ea03 100644 --- a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -15,7 +16,7 @@ class ClosureReturnTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ClosureReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false))); + return new ClosureReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false))); } public function testClosureReturnTypeRule(): void @@ -93,4 +94,48 @@ public function testBug3891(): void $this->analyse([__DIR__ . '/data/bug-3891.php'], []); } + public function testBug6806(): void + { + $this->analyse([__DIR__ . '/data/bug-6806.php'], []); + } + + public function testBug4739(): void + { + $this->analyse([__DIR__ . '/data/bug-4739.php'], []); + } + + public function testBug4739b(): void + { + $this->analyse([__DIR__ . '/data/bug-4739b.php'], []); + } + + public function testBug5753(): void + { + $this->analyse([__DIR__ . '/data/bug-5753.php'], []); + } + + public function testBug6559(): void + { + $this->analyse([__DIR__ . '/data/bug-6559.php'], []); + } + + public function testBug6902(): void + { + $this->analyse([__DIR__ . '/data/bug-6902.php'], []); + } + + public function testBug7220(): void + { + $this->analyse([__DIR__ . '/data/bug-7220.php'], []); + } + + public function testBugFunctionMethodConstants(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->analyse([__DIR__ . '/data/bug-anonymous-function-method-constant.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/DuplicateFunctionDeclarationRuleTest.php b/tests/PHPStan/Rules/Functions/DuplicateFunctionDeclarationRuleTest.php new file mode 100644 index 0000000000..5a78c1d1aa --- /dev/null +++ b/tests/PHPStan/Rules/Functions/DuplicateFunctionDeclarationRuleTest.php @@ -0,0 +1,52 @@ + + */ +class DuplicateFunctionDeclarationRuleTest extends RuleTestCase +{ + + private const FILENAME = __DIR__ . '/data/duplicate-function.php'; + + protected function getRule(): Rule + { + $fileHelper = new FileHelper(__DIR__ . '/data'); + + return new DuplicateFunctionDeclarationRule( + new DefaultReflector(new OptimizedSingleFileSourceLocator( + self::getContainer()->getByType(FileNodesFetcher::class), + self::FILENAME, + )), + new SimpleRelativePathHelper($fileHelper->normalizePath($fileHelper->getWorkingDirectory(), '/')), + ); + } + + public function testRule(): void + { + $this->analyse([self::FILENAME], [ + [ + "Function DuplicateFunctionDeclaration\\foo declared multiple times:\n- duplicate-function.php:10\n- duplicate-function.php:15\n- duplicate-function.php:20", + 10, + ], + [ + "Function DuplicateFunctionDeclaration\\foo declared multiple times:\n- duplicate-function.php:10\n- duplicate-function.php:15\n- duplicate-function.php:20", + 15, + ], + [ + "Function DuplicateFunctionDeclaration\\foo declared multiple times:\n- duplicate-function.php:10\n- duplicate-function.php:15\n- duplicate-function.php:20", + 20, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php index 16c4a3d9a5..2b2cec639b 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionDefinitionCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; @@ -20,8 +22,21 @@ class ExistingClassesInArrowFunctionTypehintsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInArrowFunctionTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker, true), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersionId), true, false)); + $reflectionProvider = $this->createReflectionProvider(); + return new ExistingClassesInArrowFunctionTypehintsRule( + new FunctionDefinitionCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new UnresolvableTypeHelper(), + new PhpVersion($this->phpVersionId), + true, + false, + ), + new PhpVersion(PHP_VERSION_ID), + ); } public function testRule(): void @@ -63,7 +78,7 @@ public function dataNativeUnionTypes(): array /** * @dataProvider dataNativeUnionTypes - * @param mixed[] $errors + * @param list $errors */ public function testNativeUnionTypes(int $phpVersionId, array $errors): void { @@ -76,7 +91,20 @@ public function dataRequiredParameterAfterOptional(): array return [ [ 70400, - [], + [ + [ + "Anonymous function uses native union types but they're supported only on PHP 8.0 and later.", + 17, + ], + [ + "Anonymous function uses native union types but they're supported only on PHP 8.0 and later.", + 19, + ], + [ + "Anonymous function uses native union types but they're supported only on PHP 8.0 and later.", + 25, + ], + ], ], [ 80000, @@ -93,6 +121,116 @@ public function dataRequiredParameterAfterOptional(): array 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', 11, ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 25, + ], + ], + ], + [ + 80100, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 9, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 11, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 15, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 25, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 25, + ], + ], + ], + [ + 80300, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 9, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 11, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 15, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 19, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 23, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 25, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 25, + ], ], ], ]; @@ -100,7 +238,7 @@ public function dataRequiredParameterAfterOptional(): array /** * @dataProvider dataRequiredParameterAfterOptional - * @param mixed[] $errors + * @param list $errors */ public function testRequiredParameterAfterOptional(int $phpVersionId, array $errors): void { @@ -138,7 +276,7 @@ public function dataIntersectionTypes(): array /** * @dataProvider dataIntersectionTypes - * @param mixed[] $errors + * @param list $errors */ public function testIntersectionTypes(int $phpVersion, array $errors): void { @@ -147,4 +285,18 @@ public function testIntersectionTypes(int $phpVersion, array $errors): void $this->analyse([__DIR__ . '/data/arrow-function-intersection-types.php'], $errors); } + public function testNever(): void + { + $errors = []; + if (PHP_VERSION_ID < 80200) { + $errors = [ + [ + 'Never return type in arrow function is supported only on PHP 8.2 and later.', + 6, + ], + ]; + } + $this->analyse([__DIR__ . '/data/arrow-function-never.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php index fdffdfc2c2..439f5bdd71 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionDefinitionCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; @@ -20,8 +22,20 @@ class ExistingClassesInClosureTypehintsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInClosureTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker, true), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersionId), true, false)); + $reflectionProvider = $this->createReflectionProvider(); + return new ExistingClassesInClosureTypehintsRule( + new FunctionDefinitionCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new UnresolvableTypeHelper(), + new PhpVersion($this->phpVersionId), + true, + false, + ), + ); } public function testExistingClassInTypehint(): void @@ -108,7 +122,7 @@ public function dataNativeUnionTypes(): array /** * @dataProvider dataNativeUnionTypes - * @param mixed[] $errors + * @param list $errors */ public function testNativeUnionTypes(int $phpVersionId, array $errors): void { @@ -121,7 +135,20 @@ public function dataRequiredParameterAfterOptional(): array return [ [ 70400, - [], + [ + [ + "Anonymous function uses native union types but they're supported only on PHP 8.0 and later.", + 29, + ], + [ + "Anonymous function uses native union types but they're supported only on PHP 8.0 and later.", + 33, + ], + [ + "Anonymous function uses native union types but they're supported only on PHP 8.0 and later.", + 45, + ], + ], ], [ 80000, @@ -138,6 +165,116 @@ public function dataRequiredParameterAfterOptional(): array 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', 17, ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 29, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 37, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 45, + ], + ], + ], + [ + 80100, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 29, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 37, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 45, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 45, + ], + ], + ], + [ + 80300, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 13, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 29, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 33, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 37, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 41, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 45, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 45, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 45, + ], ], ], ]; @@ -145,7 +282,7 @@ public function dataRequiredParameterAfterOptional(): array /** * @dataProvider dataRequiredParameterAfterOptional - * @param mixed[] $errors + * @param list $errors */ public function testRequiredParameterAfterOptional(int $phpVersionId, array $errors): void { @@ -183,7 +320,7 @@ public function dataIntersectionTypes(): array /** * @dataProvider dataIntersectionTypes - * @param mixed[] $errors + * @param list $errors */ public function testIntersectionTypes(int $phpVersion, array $errors): void { diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php index 00c7bec0b4..227044f2d7 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionDefinitionCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; @@ -20,8 +22,20 @@ class ExistingClassesInTypehintsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker, true), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersionId), true, false)); + $reflectionProvider = $this->createReflectionProvider(); + return new ExistingClassesInTypehintsRule( + new FunctionDefinitionCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new UnresolvableTypeHelper(), + new PhpVersion($this->phpVersionId), + true, + false, + ), + ); } public function testExistingClassInTypehint(): void @@ -189,7 +203,7 @@ public function dataNativeUnionTypes(): array /** * @dataProvider dataNativeUnionTypes - * @param mixed[] $errors + * @param list $errors */ public function testNativeUnionTypes(int $phpVersionId, array $errors): void { @@ -202,7 +216,20 @@ public function dataRequiredParameterAfterOptional(): array return [ [ 70400, - [], + [ + [ + "Function RequiredAfterOptional\doAmet() uses native union types but they're supported only on PHP 8.0 and later.", + 34, + ], + [ + "Function RequiredAfterOptional\doConsectetur() uses native union types but they're supported only on PHP 8.0 and later.", + 38, + ], + [ + "Function RequiredAfterOptional\doSed() uses native union types but they're supported only on PHP 8.0 and later.", + 50, + ], + ], ], [ 80000, @@ -219,6 +246,116 @@ public function dataRequiredParameterAfterOptional(): array 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', 18, ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 26, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 34, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 42, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 50, + ], + ], + ], + [ + 80100, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 14, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 18, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 26, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 30, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 34, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 42, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 50, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 50, + ], + ], + ], + [ + 80300, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 5, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 14, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 18, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 26, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 30, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 34, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 38, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 42, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 46, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 50, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 50, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 50, + ], ], ], ]; @@ -226,7 +363,7 @@ public function dataRequiredParameterAfterOptional(): array /** * @dataProvider dataRequiredParameterAfterOptional - * @param mixed[] $errors + * @param list $errors */ public function testRequiredParameterAfterOptional(int $phpVersionId, array $errors): void { @@ -264,7 +401,7 @@ public function dataIntersectionTypes(): array /** * @dataProvider dataIntersectionTypes - * @param mixed[] $errors + * @param list $errors */ public function testIntersectionTypes(int $phpVersion, array $errors): void { @@ -285,7 +422,7 @@ public function dataTrueTypes(): array /** * @dataProvider dataTrueTypes - * @param mixed[] $errors + * @param list $errors */ public function testTrueTypehint(int $phpVersion, array $errors): void { @@ -304,4 +441,14 @@ public function testConditionalReturnType(): void ]); } + public function testTemplateInParamOut(): void + { + $this->analyse([__DIR__ . '/data/param-out.php'], [ + [ + 'Template type S of function ParamOutTemplate\uselessGeneric() is not referenced in a parameter.', + 9, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php index afe7c1f9a9..553ab6c462 100644 --- a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -26,7 +28,7 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), @@ -37,7 +39,11 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, ), ); } diff --git a/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php b/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php index e97e4a4b8c..2a92e5f5a2 100644 --- a/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php @@ -20,7 +20,7 @@ protected function getRule(): Rule return new FunctionCallableRule( $reflectionProvider, - new RuleLevelHelper($reflectionProvider, true, false, true, false, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), new PhpVersion(PHP_VERSION_ID), true, true, diff --git a/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php b/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php index f3e255b610..547f66ebc6 100644 --- a/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php @@ -15,7 +15,7 @@ class ImplodeFunctionRuleTest extends RuleTestCase protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new ImplodeFunctionRule($broker, new RuleLevelHelper($broker, true, false, true, false, false)); + return new ImplodeFunctionRule($broker, new RuleLevelHelper($broker, true, false, true, false, false, true, false)); } public function testFile(): void @@ -53,4 +53,9 @@ public function testBug6000(): void $this->analyse([__DIR__ . '/../Arrays/data/bug-6000.php'], []); } + public function testBug8467a(): void + { + $this->analyse([__DIR__ . '/../Arrays/data/bug-8467a.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRuleTest.php b/tests/PHPStan/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRuleTest.php new file mode 100644 index 0000000000..9ec40a74a9 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/IncompatibleArrowFunctionDefaultParameterTypeRuleTest.php @@ -0,0 +1,33 @@ + + */ +class IncompatibleArrowFunctionDefaultParameterTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new IncompatibleArrowFunctionDefaultParameterTypeRule(); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + $this->analyse([__DIR__ . '/data/incompatible-default-parameter-type-arrow-functions.php'], [ + [ + 'Default value of the parameter #1 $i (string) of anonymous function is incompatible with type int.', + 13, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/IncompatibleClosureFunctionDefaultParameterTypeRuleTest.php b/tests/PHPStan/Rules/Functions/IncompatibleClosureFunctionDefaultParameterTypeRuleTest.php new file mode 100644 index 0000000000..8d0506f9fc --- /dev/null +++ b/tests/PHPStan/Rules/Functions/IncompatibleClosureFunctionDefaultParameterTypeRuleTest.php @@ -0,0 +1,33 @@ + + */ +class IncompatibleClosureFunctionDefaultParameterTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new IncompatibleClosureDefaultParameterTypeRule(); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + $this->analyse([__DIR__ . '/data/incompatible-default-parameter-type-closure.php'], [ + [ + 'Default value of the parameter #1 $i (string) of anonymous function is incompatible with type int.', + 19, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/InvalidLexicalVariablesInClosureUseRuleTest.php b/tests/PHPStan/Rules/Functions/InvalidLexicalVariablesInClosureUseRuleTest.php new file mode 100644 index 0000000000..5efe470207 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/InvalidLexicalVariablesInClosureUseRuleTest.php @@ -0,0 +1,117 @@ + + */ +class InvalidLexicalVariablesInClosureUseRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new InvalidLexicalVariablesInClosureUseRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/invalid-lexical-variables-in-closure-use.php'], [ + [ + 'Cannot use $this as lexical variable.', + 25, + ], + [ + 'Cannot use superglobal variable $GLOBALS as lexical variable.', + 35, + ], + [ + 'Cannot use superglobal variable $_COOKIE as lexical variable.', + 35, + ], + [ + 'Cannot use superglobal variable $_ENV as lexical variable.', + 35, + ], + [ + 'Cannot use superglobal variable $_FILES as lexical variable.', + 35, + ], + [ + 'Cannot use superglobal variable $_GET as lexical variable.', + 35, + ], + [ + 'Cannot use superglobal variable $_POST as lexical variable.', + 35, + ], + [ + 'Cannot use superglobal variable $_REQUEST as lexical variable.', + 35, + ], + [ + 'Cannot use superglobal variable $_SERVER as lexical variable.', + 35, + ], + [ + 'Cannot use superglobal variable $_SESSION as lexical variable.', + 35, + ], + [ + 'Cannot use lexical variable $baz since a parameter with the same name already exists.', + 55, + ], + [ + 'Cannot use $this as lexical variable.', + 68, + ], + [ + 'Cannot use superglobal variable $GLOBALS as lexical variable.', + 81, + ], + [ + 'Cannot use superglobal variable $_COOKIE as lexical variable.', + 82, + ], + [ + 'Cannot use superglobal variable $_ENV as lexical variable.', + 83, + ], + [ + 'Cannot use superglobal variable $_FILES as lexical variable.', + 84, + ], + [ + 'Cannot use superglobal variable $_GET as lexical variable.', + 85, + ], + [ + 'Cannot use superglobal variable $_POST as lexical variable.', + 86, + ], + [ + 'Cannot use superglobal variable $_REQUEST as lexical variable.', + 87, + ], + [ + 'Cannot use superglobal variable $_SERVER as lexical variable.', + 88, + ], + [ + 'Cannot use superglobal variable $_SESSION as lexical variable.', + 89, + ], + [ + 'Cannot use lexical variable $baz since a parameter with the same name already exists.', + 111, + ], + [ + 'Cannot use lexical variable $bar since a parameter with the same name already exists.', + 112, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php index 492487bc0a..35bf719f76 100644 --- a/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php @@ -14,8 +14,7 @@ class MissingFunctionParameterTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new MissingFunctionParameterTypehintRule(new MissingTypehintCheck($broker, true, true, true, true, [])); + return new MissingFunctionParameterTypehintRule(new MissingTypehintCheck(true, true, true, true, []), true); } public function testRule(): void @@ -83,6 +82,21 @@ public function testRule(): void 'Function MissingFunctionParameterTypehint\missingCallableSignature() has parameter $cb with no signature specified for callable.', 161, ], + [ + 'Function MissingParamOutType\oneArray() has @param-out PHPDoc tag for parameter $a with no value type specified in iterable type array.', + 173, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + [ + 'Function MissingParamOutType\generics() has @param-out PHPDoc tag for parameter $a with generic class ReflectionClass but does not specify its types: T', + 181, + 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + ], + [ + 'Function MissingParamClosureThisType\generics() has @param-closure-this PHPDoc tag for parameter $cb with generic class ReflectionClass but does not specify its types: T', + 191, + 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php index f69828cbf2..0861bfb2ce 100644 --- a/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php @@ -14,8 +14,7 @@ class MissingFunctionReturnTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new MissingFunctionReturnTypehintRule(new MissingTypehintCheck($broker, true, true, true, true, [])); + return new MissingFunctionReturnTypehintRule(new MissingTypehintCheck(true, true, true, true, [])); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php index 2fdf001d84..60d620474c 100644 --- a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -26,7 +28,7 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), @@ -37,7 +39,11 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, ), ); } @@ -65,4 +71,9 @@ public function testSensitiveParameterAttribute(): void $this->analyse([__DIR__ . '/data/sensitive-parameter.php'], []); } + public function testBug10298(): void + { + $this->analyse([__DIR__ . '/data/bug-10298.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php index d9effb60e0..4e2880fda7 100644 --- a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php @@ -110,4 +110,14 @@ public function testBug4717(): void $this->analyse([__DIR__ . '/data/bug-4717.php'], $errors); } + public function testBug2342(): void + { + $this->analyse([__DIR__ . '/data/bug-2342.php'], [ + [ + 'Call to sprintf contains 1 placeholder, 0 values given.', + 5, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/RedefinedParametersRuleTest.php b/tests/PHPStan/Rules/Functions/RedefinedParametersRuleTest.php new file mode 100644 index 0000000000..3c68e09b7b --- /dev/null +++ b/tests/PHPStan/Rules/Functions/RedefinedParametersRuleTest.php @@ -0,0 +1,37 @@ + + */ +class RedefinedParametersRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RedefinedParametersRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/redefined-parameters.php'], [ + [ + 'Redefinition of parameter $foo.', + 11, + ], + [ + 'Redefinition of parameter $bar.', + 13, + ], + [ + 'Redefinition of parameter $baz.', + 15, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php index 292d219563..bb99cc426a 100644 --- a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -19,7 +20,7 @@ class ReturnTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), $this->checkNullables, false, true, $this->checkExplicitMixed, false))); + return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), $this->checkNullables, false, true, $this->checkExplicitMixed, false, true, false))); } public function testReturnTypeRule(): void @@ -154,10 +155,14 @@ public function testBug3801(): void [ 'Function Bug3801\do_foo() should return array{bool, null}|array{null, bool} but returns array{false, true}.', 17, + '• Type #1 from the union: Offset 1 (null) does not accept type true. +• Type #2 from the union: Offset 0 (null) does not accept type false.', ], [ 'Function Bug3801\do_foo() should return array{bool, null}|array{null, bool} but returns array{false, false}.', 21, + '• Type #1 from the union: Offset 1 (null) does not accept type false. +• Type #2 from the union: Offset 0 (null) does not accept type false.', ], ]); } @@ -181,4 +186,94 @@ public function testListWithNullablesUnchecked(): void $this->analyse([__DIR__ . '/data/return-list-nullables.php'], []); } + public function testBug6787(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-6787.php'], [ + [ + 'Function Bug6787\f() should return T of DateTimeInterface but returns DateTime.', + 11, + 'Type DateTime is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + ]); + } + + public function testBug6568(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-6568.php'], [ + [ + 'Function Bug6568\test() should return T of array but returns array.', + 12, + 'Type array is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + ]); + } + + public function testBug7766(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-7766.php'], [ + [ + "Function Bug7766\problem() should return array but returns array{array{id: 1, created: DateTimeImmutable, updated: DateTimeImmutable, valid_from: DateTimeImmutable, valid_till: DateTimeImmutable, string: 'string', other_string: 'string', another_string: 'string', ...}}.", + 20, + "Offset 'count' (int<0, max>) does not accept type '4'.", + ], + ]); + } + + public function testBug8846(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-8846.php'], []); + } + + public function testBug10077(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-10077.php'], [ + [ + 'Function Bug10077\mergeMediaQueries() should return list|null but returns list.', + 56, + ], + ]); + } + + public function testBug8683(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-8683.php'], []); + } + + public function testBug7984(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-7984.php'], []); + } + + public function testBug5594(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-5594.php'], []); + } + + public function testBug5592(): void + { + $this->checkExplicitMixed = true; + $this->checkNullables = true; + $this->analyse([__DIR__ . '/data/bug-5592.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/VariadicParametersDeclarationRuleTest.php b/tests/PHPStan/Rules/Functions/VariadicParametersDeclarationRuleTest.php new file mode 100644 index 0000000000..7955385f36 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/VariadicParametersDeclarationRuleTest.php @@ -0,0 +1,37 @@ + + */ +class VariadicParametersDeclarationRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new VariadicParametersDeclarationRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/variadic-parameters-declaration.php'], [ + [ + 'Only the last parameter can be variadic.', + 7, + ], + [ + 'Only the last parameter can be variadic.', + 11, + ], + [ + 'Only the last parameter can be variadic.', + 21, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/argon2id-password-hash.php b/tests/PHPStan/Rules/Functions/data/argon2id-password-hash.php new file mode 100644 index 0000000000..e50e81894c --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/argon2id-password-hash.php @@ -0,0 +1,7 @@ + $list */ +$list = [1, 2, 3]; +/** @var list $list */ +$array = ['a' => 1, 'b' => 2, 'c' => 3]; + +array_values([0,1,3]); +array_values([1,3]); +array_values(['test']); +array_values(['a' => 'test']); +array_values(['', 'test']); +array_values(['a' => '', 'b' => 'test']); +array_values($list); +array_values($array); + +array_values([0]); +array_values(['a' => null, 'b' => null]); +array_values([null, null]); +array_values([null, 0]); +array_values([]); + +/** @var array{} $empty */ +$empty = doFoo(); +array_values($empty); + +array_values(unused: true, array: $array); +array_values(unused: true, array: $list); diff --git a/tests/PHPStan/Rules/Functions/data/arrow-function-never.php b/tests/PHPStan/Rules/Functions/data/arrow-function-never.php new file mode 100644 index 0000000000..227ad163b2 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/arrow-function-never.php @@ -0,0 +1,7 @@ += 7.4 + +namespace ArrowFunctionNever; + +function (): void { + $g = fn (): never => throw new \Exception(); +}; diff --git a/tests/PHPStan/Rules/Functions/data/arrow-functions-return-type.php b/tests/PHPStan/Rules/Functions/data/arrow-functions-return-type.php index 4a18708fba..552bf901c6 100644 --- a/tests/PHPStan/Rules/Functions/data/arrow-functions-return-type.php +++ b/tests/PHPStan/Rules/Functions/data/arrow-functions-return-type.php @@ -33,3 +33,15 @@ public function doBar(): void } static fn (int $value): iterable => yield $value; + +class Baz +{ + + public function doFoo(): void + { + $f = fn () => throw new \Exception(); + $g = fn (): never => throw new \Exception(); + $g = fn (): never => 1; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/benevolent-superglobal-keys.php b/tests/PHPStan/Rules/Functions/data/benevolent-superglobal-keys.php new file mode 100644 index 0000000000..b5a3c485c8 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/benevolent-superglobal-keys.php @@ -0,0 +1,87 @@ + $v) { + trim($k); + } + + foreach ($_SERVER as $k => $v) { + trim($k); + } + + foreach ($_GET as $k => $v) { + trim($k); + } + + foreach ($_POST as $k => $v) { + trim($k); + } + + foreach ($_FILES as $k => $v) { + trim($k); + } + + foreach ($_COOKIE as $k => $v) { + trim($k); + } + + foreach ($_SESSION as $k => $v) { + trim($k); + } + + foreach ($_REQUEST as $k => $v) { + trim($k); + } + + foreach ($_ENV as $k => $v) { + trim($k); + } +} + +function benevolentKeysOfSuperglobalsInt(): void +{ + foreach ($GLOBALS as $k => $v) { + acceptInt($k); + } + + foreach ($_SERVER as $k => $v) { + acceptInt($k); + } + + foreach ($_GET as $k => $v) { + acceptInt($k); + } + + foreach ($_POST as $k => $v) { + acceptInt($k); + } + + foreach ($_FILES as $k => $v) { + acceptInt($k); + } + + foreach ($_COOKIE as $k => $v) { + acceptInt($k); + } + + foreach ($_SESSION as $k => $v) { + acceptInt($k); + } + + foreach ($_REQUEST as $k => $v) { + acceptInt($k); + } + + foreach ($_ENV as $k => $v) { + acceptInt($k); + } +} + +function acceptInt(int $i): void +{ +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-10003.php b/tests/PHPStan/Rules/Functions/data/bug-10003.php new file mode 100644 index 0000000000..af669a63ed --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10003.php @@ -0,0 +1,16 @@ += 8.1 + +namespace Bug10077; + +interface MediaQueryMergeResult +{ +} + + +enum MediaQuerySingletonMergeResult implements MediaQueryMergeResult +{ + case empty; + case unrepresentable; +} + +// In actual code, this is a final class implementing its methods +abstract class CssMediaQuery implements MediaQueryMergeResult +{ + abstract public function merge(CssMediaQuery $other): MediaQueryMergeResult; +} + + +/** + * Returns a list of queries that selects for contexts that match both + * $queries1 and $queries2. + * + * Returns the empty list if there are no contexts that match both $queries1 + * and $queries2, or `null` if there are contexts that can't be represented + * by media queries. + * + * @param CssMediaQuery[] $queries1 + * @param CssMediaQuery[] $queries2 + * + * @return list|null + */ +function mergeMediaQueries(array $queries1, array $queries2): ?array +{ + $queries = []; + + foreach ($queries1 as $query1) { + foreach ($queries2 as $query2) { + $result = $query1->merge($query2); + + if ($result === MediaQuerySingletonMergeResult::empty) { + continue; + } + + if ($result === MediaQuerySingletonMergeResult::unrepresentable) { + return null; + } + + $queries[] = $result; + } + } + + return $queries; +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-10171.php b/tests/PHPStan/Rules/Functions/data/bug-10171.php new file mode 100644 index 0000000000..45a5545184 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10171.php @@ -0,0 +1,36 @@ += 8.0 + +namespace Bug10171; + +setcookie("name", "value", 0, "/", secure: true, httponly: true); +setcookie('name', expires_or_options: ['samesite' => 'lax']); + +setrawcookie("name", "value", 0, "/", secure: true, httponly: true); +setrawcookie('name', expires_or_options: ['samesite' => 'lax']); + +// Wrong +setcookie('name', samesite: 'lax'); +setcookie( + 'aaa', + 'bbb', + 10, + '/', + 'example.com', + true, + false, + 'lax', + 1, +); + +setrawcookie('name', samesite: 'lax'); +setrawcookie( + 'aaa', + 'bbb', + 10, + '/', + 'example.com', + true, + false, + 'lax', + 1, +); diff --git a/tests/PHPStan/Rules/Functions/data/bug-10297.php b/tests/PHPStan/Rules/Functions/data/bug-10297.php new file mode 100644 index 0000000000..ed3a8cf3dd --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10297.php @@ -0,0 +1,86 @@ + $stream + * @param callable(T, K): iterable $fn + * + * @return Generator + */ +function scollect(iterable $stream, callable $fn): Generator +{ + foreach ($stream as $key => $value) { + yield from $fn($value, $key); + } +} + +/** + * @template K of array-key + * @template T + * @template L of array-key + * @template U + * + * @param array $array + * @param callable(T, K): iterable $fn + * + * @return array + */ +function collectWithKeys(array $array, callable $fn): array +{ + $map = []; + $counter = 0; + + try { + foreach (scollect($array, $fn) as $key => $value) { + $map[$key] = $value; + ++$counter; + } + } catch (TypeError) { + throw new UnexpectedValueException('The key yielded in the callable is not compatible with the type "array-key".'); + } + + if ($counter !== count($map)) { + throw new UnexpectedValueException( + 'Data loss occurred because of duplicated keys. Use `collect()` if you do not care about ' . + 'the yielded keys, or use `scollect()` if you need to support duplicated keys (as arrays cannot).', + ); + } + + return $map; +} + +class SomeUnitTest +{ + /** + * @return iterable + */ + public static function someProvider(): iterable + { + $unsupportedTypes = [ + // this one does not work: + 'Not a Number' => NAN, + // these work: + 'Infinity' => INF, + stdClass::class => new stdClass(), + self::class => self::class, + 'hello there' => 'hello there', + 'array' => [[42]], + ]; + + yield from collectWithKeys($unsupportedTypes, static function (mixed $value, string $type): iterable { + $error = sprintf('Some %s error message', $type); + + yield sprintf('"%s" something something', $type) => [$value, [$error, $error, $error]]; + }); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-10298.php b/tests/PHPStan/Rules/Functions/data/bug-10298.php new file mode 100644 index 0000000000..dfbfa7979e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10298.php @@ -0,0 +1,18 @@ +real_connect( + null, + null, + null, + null, + null, + null, + \MYSQLI_CLIENT_SSL + ); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-3818b.php b/tests/PHPStan/Rules/Functions/data/bug-3818b.php new file mode 100644 index 0000000000..0a9a3d6c83 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3818b.php @@ -0,0 +1,29 @@ +handleA(...) : $this->handleB(...); + + $method($obj); + } + + private function handleA(A $a): void + { + } + + private function handleB(B $b): void + { + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-4612.php b/tests/PHPStan/Rules/Functions/data/bug-4612.php new file mode 100644 index 0000000000..3f8c3f6bd5 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-4612.php @@ -0,0 +1,16 @@ + $array */ +$array = []; + +foreach ($array as $k => $v) { + if (check($k) && isset($prev)) { + $array[$prev] = $v; + } + + $prev = $k; +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-4739.php b/tests/PHPStan/Rules/Functions/data/bug-4739.php new file mode 100644 index 0000000000..e8d0fdfbaa --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-4739.php @@ -0,0 +1,19 @@ + $value) { + if ($predicate($value)) { + yield $key => $value; + } + } +} + + +class Record { + /** + * @var boolean + */ + public $isInactive; + /** + * @var string + */ + public $name; +} + +function doFoo() { + $emails = []; + $records = []; + filter( + function (Record $domain) use (&$emails): bool { + if (!isset($emails[$domain->name])) { + $emails[$domain->name] = TRUE; + return TRUE; + } + return !$domain->isInactive; + }, + $records + ); + $test = (bool) mt_rand(0, 1); + filter( + function (bool $arg) use (&$emails): bool { + if (empty($emails)) { + return TRUE; + } + return $arg; + }, + $records + ); + filter( + function (bool $arg) use ($emails): bool { + if (empty($emails)) { + return TRUE; + } + return $arg; + }, + $records + ); + $test = (bool) mt_rand(0, 1); + filter( + function (bool $arg) use (&$test): bool { + if ($test) { + return TRUE; + } + return $arg; + }, + $records + ); + filter( + function (bool $arg) use ($test): bool { + if ($test) { + return TRUE; + } + return $arg; + }, + $records + ); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5288.php b/tests/PHPStan/Rules/Functions/data/bug-5288.php new file mode 100644 index 0000000000..a116082737 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5288.php @@ -0,0 +1,54 @@ +get_iterator(); + $data = array_map( + function ($value): void {}, + iterator_to_array($iterator) + ); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5592.php b/tests/PHPStan/Rules/Functions/data/bug-5592.php new file mode 100644 index 0000000000..56ae4204e5 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5592.php @@ -0,0 +1,53 @@ + $map + * @return numeric-string + */ +function mapGet(\Ds\Map $map, \Ds\Hashable $key): string +{ + return $map->get($key, '0'); +} + +/** + * @template TDefault + * @param TDefault $default + * @return numeric-string|TDefault + */ +function getFooOrDefault($default) { + if ((bool) random_int(0, 1)) { + /** @var numeric-string */ + $foo = '5'; + return $foo; + } else { + return $default; + } +} + +function doStuff(): int +{ + /** + * @var \Ds\Map + */ + $map = new \Ds\Map(); + + return $map->get('foo', 1); +} + +/** + * @return numeric-string + */ +function doStuff1(): string { + /** @var numeric-string */ + $foo = '12'; + return getFooOrDefault($foo); +} + +/** + * @return numeric-string + */ +function doStuff2(): string { + return getFooOrDefault('12'); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5594.php b/tests/PHPStan/Rules/Functions/data/bug-5594.php new file mode 100644 index 0000000000..19dd58ed83 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5594.php @@ -0,0 +1,14 @@ + + */ +function createIterator(array $items): ArrayIterator +{ + return new ArrayIterator($items); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5753.php b/tests/PHPStan/Rules/Functions/data/bug-5753.php new file mode 100644 index 0000000000..ea9675a3ef --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5753.php @@ -0,0 +1,11 @@ + */ + public function getCreateTableSQL(): array + { + $sqls = array_merge( + $this->a(), + parent::b() // @phpstan-ignore-line + ); + + return $sqls; + } +} + +class A { + public function a(): mixed { + throw new \Exception(); + } + + /** @return null */ + public function b() { + throw new \Exception(); + } +} + +class B extends A +{ + use T; +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-6261.php b/tests/PHPStan/Rules/Functions/data/bug-6261.php new file mode 100644 index 0000000000..055f809075 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-6261.php @@ -0,0 +1,15 @@ +> + */ + private array $serializers = []; + + /** + * @phpstan-template TBlockType of Block + * @phpstan-param TBlockType $block + */ + public function serialize(Block $block) : CompoundTag{ + $class = get_class($block); + $serializer = $this->serializers[$class][$block->getTypeId()] ?? null; + + if($serializer === null){ + //TODO: use a proper exception type for this + throw new \InvalidArgumentException("No serializer registered for this block (this is probably a plugin bug)"); + } + + return $serializer($block); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-6559.php b/tests/PHPStan/Rules/Functions/data/bug-6559.php new file mode 100644 index 0000000000..0fcef8ca3d --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-6559.php @@ -0,0 +1,13 @@ + true]; + + $find = function(string $key) use (&$array) { + return $array[$key] ?? null; + }; + + $find('a') ?? false; +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-6568.php b/tests/PHPStan/Rules/Functions/data/bug-6568.php new file mode 100644 index 0000000000..399e3111cc --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-6568.php @@ -0,0 +1,13 @@ +name . $this->version; + } +} + +class ServiceRedis +{ + public function __construct( + private string $name, + private string $version, + private bool $persistent, + ) {} + + public function touchAll() : string{ + return $this->persistent ? $this->name : $this->version; + } +} + +function test(?string $type = NULL) : void { + $types = [ + 'solr' => [ + 'label' => 'SOLR Search', + 'data_class' => CreateServiceSolrData::class, + 'to_entity' => function (CreateServiceSolrData $data) { + assert($data->name !== NULL && $data->version !== NULL, "Incorrect form validation"); + return new ServiceSolr($data->name, $data->version); + }, + ], + 'redis' => [ + 'label' => 'Redis', + 'data_class' => CreateServiceRedisData::class, + 'to_entity' => function (CreateServiceRedisData $data) { + assert($data->name !== NULL && $data->version !== NULL && $data->persistent !== NULL, "Incorrect form validation"); + return new ServiceRedis($data->name, $data->version, $data->persistent); + }, + ], + ]; + + if ($type === NULL || !isset($types[$type])) { + throw new \RuntimeException("404 or choice form here"); + } + + $data = new $types[$type]['data_class'](); + + $service = $types[$type]['to_entity']($data); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-6720.php b/tests/PHPStan/Rules/Functions/data/bug-6720.php new file mode 100644 index 0000000000..0fc2e546c6 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-6720.php @@ -0,0 +1,11 @@ + [ 'type' => 'a' ], 'second' => [ 'type' => 'b' ] ]; + + $types = array_fill_keys($types, true); + $defs = array_filter($defs, function($def) use(&$types) { return isset($types[$def['type']]); }); +}; diff --git a/tests/PHPStan/Rules/Functions/data/bug-6902.php b/tests/PHPStan/Rules/Functions/data/bug-6902.php new file mode 100644 index 0000000000..2d079f2286 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-6902.php @@ -0,0 +1,23 @@ + 1, 'b' => 2]; + /** @var array **/ + $array2 = ['a' => 1]; + + $check = function(string $key) use (&$array1, &$array2): bool { + if (!isset($array1[$key], $array2[$key])) { + return false; + } + // ... more conditions here ... + return true; + }; + + if ($check('a')) { + // ... + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-7057.php b/tests/PHPStan/Rules/Functions/data/bug-7057.php new file mode 100644 index 0000000000..749baa4b79 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-7057.php @@ -0,0 +1,11 @@ + $value) { + if ($predicate($value)) { + yield $key => $value; + } + } +} + +function getFiltered(): \Iterator { + $already_seen = []; + return filter(function (string $value) use (&$already_seen): bool { + $result = !isset($already_seen[$value]); + $already_seen[$value] = TRUE; + return $result; + }, ['a', 'b', 'a']); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-7283.php b/tests/PHPStan/Rules/Functions/data/bug-7283.php new file mode 100644 index 0000000000..ecf155d62a --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-7283.php @@ -0,0 +1,22 @@ + + */ +function onlyTrue(mixed $value): array +{ + return array_fill(0, 5, $value); +} + +/** + * @param array $values + */ +function needTrue(array $values): void {} + +function (): void { + needTrue(onlyTrue(true)); +}; diff --git a/tests/PHPStan/Rules/Functions/data/bug-7766.php b/tests/PHPStan/Rules/Functions/data/bug-7766.php new file mode 100644 index 0000000000..143862df62 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-7766.php @@ -0,0 +1,32 @@ +, + * other_count: int<0, max> + * }> + */ +function problem(): array { + return [[ + 'id' => 1, + 'created' => new \DateTimeImmutable(), + 'updated' => new \DateTimeImmutable(), + 'valid_from' => new \DateTimeImmutable(), + 'valid_till' => new \DateTimeImmutable(), + 'string' => 'string', + 'other_string' => 'string', + 'another_string' => 'string', + 'count' => '4', + 'other_count' => 3, + ]]; +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-7984.php b/tests/PHPStan/Rules/Functions/data/bug-7984.php new file mode 100644 index 0000000000..2ba1d267c7 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-7984.php @@ -0,0 +1,20 @@ +takes(function () { + test123(); + }); + } + + $tc->takes(function () { + if(function_exists('test123')) { + test123(); + } + }); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-8280.php b/tests/PHPStan/Rules/Functions/data/bug-8280.php new file mode 100644 index 0000000000..5808b4df72 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-8280.php @@ -0,0 +1,18 @@ + $var + */ +function foo($var): void {} + +/** @var string|list|null $var */ +if (null !== $var) { + assertType('list', (array) $var); + foo((array) $var); // should work the same as line below + assertType('list', !is_array($var) ? [$var] : $var); + foo(!is_array($var) ? [$var] : $var); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-8389.php b/tests/PHPStan/Rules/Functions/data/bug-8389.php new file mode 100644 index 0000000000..43823068e5 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-8389.php @@ -0,0 +1,50 @@ + $a */ +$a = [1]; +/** @var list $b */ +$b = [2]; + +array_push($a, ...$b); + +/** + * @param list $parameter + */ +function test(array $parameter): void +{ +} + +test($a); diff --git a/tests/PHPStan/Rules/Functions/data/bug-8659.php b/tests/PHPStan/Rules/Functions/data/bug-8659.php new file mode 100644 index 0000000000..619f3e40d3 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-8659.php @@ -0,0 +1,30 @@ + + */ +function myCallableFunction() { + return ['test1' => 4, 'test2' => 45, 'test3' => 3, 'total' => 52]; +} + +/** + * @return array<'test1'|'test2'|'test3'|'total', int> + */ +function IwillCallTheCallable() { + return theCaller(fn () => myCallableFunction()); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-8846.php b/tests/PHPStan/Rules/Functions/data/bug-8846.php new file mode 100644 index 0000000000..a4fc961d4c --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-8846.php @@ -0,0 +1,24 @@ += 8.0 + +namespace Bug9018; + +// This works +echo levenshtein('test1', 'test2'); + +// This works but fails analysis +echo levenshtein(string1: 'test1', string2: 'test2'); + +// This passes analysis but throws an error +// Warning: Uncaught Error: Unknown named parameter $str1 in php shell code:1 +echo levenshtein(str1: 'test1', str2: 'test2'); diff --git a/tests/PHPStan/Rules/Functions/data/bug-9133.php b/tests/PHPStan/Rules/Functions/data/bug-9133.php new file mode 100644 index 0000000000..a17b650920 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9133.php @@ -0,0 +1,31 @@ += 8.0 + +namespace Bug9283; + +/** + * @param \Stringable $obj + */ +function test(object $obj): string { + return strval($obj); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-9380.php b/tests/PHPStan/Rules/Functions/data/bug-9380.php new file mode 100644 index 0000000000..bcd48e6938 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9380.php @@ -0,0 +1,9 @@ += 8.0 + +namespace Bug9399; + +setlocale(category: LC_ALL, locales: 'nl_NL'); diff --git a/tests/PHPStan/Rules/Functions/data/bug-9580.php b/tests/PHPStan/Rules/Functions/data/bug-9580.php new file mode 100644 index 0000000000..1b6feb16fc --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9580.php @@ -0,0 +1,22 @@ + [1, 2, 3], + 'greet' => fn (int $value) => 'I am '.$value, + ], + [ + 'elements' => ['hello', 'world'], + 'greet' => fn (string $value) => 'I am '.$value, + ], + ]; + + foreach ($data as $entry) { + foreach ($entry['elements'] as $element) { + $entry['greet']($element); + } + } + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-9614.php b/tests/PHPStan/Rules/Functions/data/bug-9614.php new file mode 100644 index 0000000000..1209501f2e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9614.php @@ -0,0 +1,27 @@ + function() { + return 'test'; + }, + 'foo' => function($a) { + return 'foo'; + }, + 'bar' => function($a, $b) { + return 'bar'; + } + ]; + + if (!isset($funcs[$key])) { + return ''; + } + + return $funcs[$key]($a, $b); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-9697.php b/tests/PHPStan/Rules/Functions/data/bug-9697.php new file mode 100644 index 0000000000..742a5937cf --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9697.php @@ -0,0 +1,19 @@ += 7.4 + +namespace Bug9697; + +function doFoo(): void +{ + $oldItems = [1,2,3]; + $newItems = [1,2]; + + $comparator = fn (int $a, int $b):int => $a - $b; + + usort($oldItems, $comparator); + + array_udiff( + $oldItems, + $newItems, + $comparator, + ); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-9699.php b/tests/PHPStan/Rules/Functions/data/bug-9699.php new file mode 100644 index 0000000000..09307d4c47 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9699.php @@ -0,0 +1,19 @@ += 8.1 + +namespace Bug9699; + +function withVariadicParam(int $a, int $b, int ...$rest): int +{ + return array_sum([$a, $b, ...$rest]); +} + +/** + * @param \Closure(int, int, int, string): int $f + */ +function int_int_int_string(\Closure $f): void +{ + $f(0, 0, 0, ''); +} + +// false negative: expected issue here +int_int_int_string(withVariadicParam(...)); diff --git a/tests/PHPStan/Rules/Functions/data/bug-9793.php b/tests/PHPStan/Rules/Functions/data/bug-9793.php new file mode 100644 index 0000000000..dbcfc62b8b --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9793.php @@ -0,0 +1,19 @@ + $arr + * @param \Iterator<\stdClass>|array<\stdClass> $itOrArr + */ +function foo(array $arr, $itOrArr): void +{ + \iterator_to_array($arr); + \iterator_to_array($itOrArr); + echo \iterator_count($arr); + echo \iterator_count($itOrArr); + \iterator_apply($arr, fn ($x) => $x); + \iterator_apply($itOrArr, fn ($x) => $x); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-9803.php b/tests/PHPStan/Rules/Functions/data/bug-9803.php new file mode 100644 index 0000000000..6e02f6ea99 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9803.php @@ -0,0 +1,28 @@ +', $keys); + } + + assertType('array', $keys); + $theKeys = array_keys($keys); + assertType('list', $theKeys); +} + + diff --git a/tests/PHPStan/Rules/Functions/data/bug-9823.php b/tests/PHPStan/Rules/Functions/data/bug-9823.php new file mode 100644 index 0000000000..3c8b3cfb77 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9823.php @@ -0,0 +1,5 @@ += 8.0 + +namespace Bug9923; + +echo join(separator: ' ', array: ['a', 'b', 'c']); +echo implode(separator: ' ', array: ['a', 'b', 'c']); diff --git a/tests/PHPStan/Rules/Functions/data/bug-anonymous-function-method-constant.php b/tests/PHPStan/Rules/Functions/data/bug-anonymous-function-method-constant.php new file mode 100644 index 0000000000..5347b1116f --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-anonymous-function-method-constant.php @@ -0,0 +1,14 @@ += 7.4 + +namespace BugAnonymousFunctionMethodConstant; + +$a = fn() => __FUNCTION__; +$b = fn() => __METHOD__; + +$c = function() { return __FUNCTION__; }; +$d = function() { return __METHOD__; }; + +\PHPStan\Testing\assertType("'{closure}'", $a()); +\PHPStan\Testing\assertType("'{closure}'", $b()); +\PHPStan\Testing\assertType("'{closure}'", $c()); +\PHPStan\Testing\assertType("'{closure}'", $d()); diff --git a/tests/PHPStan/Rules/Functions/data/bug-array-filter.php b/tests/PHPStan/Rules/Functions/data/bug-array-filter.php new file mode 100644 index 0000000000..cb95969e30 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-array-filter.php @@ -0,0 +1,14 @@ + $a <=> $b); diff --git a/tests/PHPStan/Rules/Functions/data/call-to-function-named-params-multivariant.php b/tests/PHPStan/Rules/Functions/data/call-to-function-named-params-multivariant.php new file mode 100644 index 0000000000..71cd2b6653 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/call-to-function-named-params-multivariant.php @@ -0,0 +1,67 @@ += 8.0 + +namespace CallToFunctionNamedParamsMultiVariant; + +// docs say that it's not compatible with named params, but it actually works +setcookie(name: 'aaa', value: 'bbb', expires_or_options: ['httponly' => true]); +setrawcookie(name: 'aaa1', value: 'bbb', expires_or_options: ['httponly' => true]); +var_dump(abs(num: 5)); +var_dump(array_rand(array: [5])); +var_dump(array_rand(array: [5], num: 1)); +var_dump(getenv(name: 'aaa', local_only: true)); +$cal = new \IntlGregorianCalendar(); +var_dump(intlcal_set(calendar: $cal, month: 5, year: 6)); +var_dump(join(separator: 'a', array: [])); +var_dump(join(separator: ['aaa', 'bbb'])); +var_dump(implode(separator: 'a', array: [])); +var_dump(implode(separator: ['aaa', 'bbb'])); +var_dump(levenshtein(string1: 'aaa', string2: 'bbb', insertion_cost: 1, deletion_cost: 1, replacement_cost: 1)); +var_dump(levenshtein(string1: 'aaa', string2: 'bbb')); +// Is it possible to call it with multiple named args? +var_dump(max(value: [5, 6])); +session_set_cookie_params(lifetime_or_options: []); +session_set_cookie_params(lifetime_or_options: 1, path: '/'); +session_set_save_handler(open: new class implements \SessionHandlerInterface { + public function close(): bool + { + return true; + } + + public function destroy(string $id): bool + { + return true; + } + + public function gc(int $max_lifetime): int|false + { + return 0; + } + + public function open(string $path, string $name): bool + { + return true; + } + + public function read(string $id): string|false + { + return true; + } + + public function write(string $id, string $data): bool + { + return true; + } + +}, close: true); +setlocale(category: 0, locales: 'aaa'); +setlocale(category: 0, locales: []); +sscanf(string: 'aaa', format: 'aaa'); +$context = fopen('php://input', 'r'); +assert($context !== false); +stream_context_set_option(context: $context, wrapper_or_options: []); +stream_context_set_option(context: $context, wrapper_or_options: 'aaa', option_name: "aaa", value: 'aaa'); +var_dump(strtok(string: 'bbb aaa ccc', token: 'a')); +// docs say it's not compatible with named params, but it actually works +var_dump(strtok(string: 'a')); +var_dump(strtr(string: 'aaa', from: 'a', to: 'b')); +var_dump(strtr(string: 'aaa', from: ['a' => 'b'])); diff --git a/tests/PHPStan/Rules/Functions/data/call-user-func.php b/tests/PHPStan/Rules/Functions/data/call-user-func.php new file mode 100644 index 0000000000..a0606ba59f --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/call-user-func.php @@ -0,0 +1,46 @@ += 8.0 + +namespace CallUserFuncRule; + +use function call_user_func; + +class Foo +{ + + public function doFoo(): void + { + $f = function (int $i): void { + + }; + call_user_func($f); + call_user_func($f, 1); + call_user_func($f, 'foo'); + call_user_func($f, i: 'foo'); + call_user_func(i: 'foo', callback: $f); + call_user_func($f, i: 1); + call_user_func(i: 1, callback: $f); + call_user_func($f, j: 1); + } + + public function doBar(): void + { + $f = function (int $i, $j, $g = 2, $h = 3): void { + }; + + call_user_func($f); + call_user_func($f, 1); + call_user_func($f, 2, 'foo'); + } + + public function doVariadic(): void + { + $f = function ($i, $j, ...$params): void { + }; + + call_user_func($f); + call_user_func($f, 1); + call_user_func($f, 2, 'foo'); + $result = call_user_func($f, 2, 'foo'); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/curl_setopt.php b/tests/PHPStan/Rules/Functions/data/curl_setopt.php index f5d33fe43d..76d3136a2d 100644 --- a/tests/PHPStan/Rules/Functions/data/curl_setopt.php +++ b/tests/PHPStan/Rules/Functions/data/curl_setopt.php @@ -1,5 +1,7 @@ 'application/json', + ]; + curl_setopt($curl, CURLOPT_HTTPHEADER, $header_dictionary); + + $header_list = [ + 'Accept: application/json', + ]; + curl_setopt($curl, CURLOPT_HTTPHEADER, $header_list); + } } diff --git a/tests/PHPStan/Rules/Functions/data/discussion-10454.php b/tests/PHPStan/Rules/Functions/data/discussion-10454.php new file mode 100644 index 0000000000..67adaf0dd0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/discussion-10454.php @@ -0,0 +1,29 @@ += 7.4 + +namespace FunctionCallParamClosureThis; + +/** + * @param-closure-this \stdClass $cb + */ +function acceptClosure(callable $cb): void +{ + +} + +function (): void { + acceptClosure(function () { + + }); + + acceptClosure(static function () { + + }); + + acceptClosure(fn () => 1); + acceptClosure(static fn () => 1); +}; diff --git a/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-8.0.php b/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-8.0.php new file mode 100644 index 0000000000..497de17310 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-8.0.php @@ -0,0 +1,36 @@ + [ + 'method' => 'POST', + 'header' => 'Content-Type: application/json', + 'content' => json_encode($data, JSON_THROW_ON_ERROR), + ], + ])); + file_get_contents($url, false, null); + var_export([]); + var_export([], true); + print_r([]); + print_r([], true); } public function doBar(string $s) diff --git a/tests/PHPStan/Rules/Functions/data/incompatible-default-parameter-type-arrow-functions.php b/tests/PHPStan/Rules/Functions/data/incompatible-default-parameter-type-arrow-functions.php new file mode 100644 index 0000000000..1623621c08 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/incompatible-default-parameter-type-arrow-functions.php @@ -0,0 +1,16 @@ += 7.4 + +namespace IncompatibleArrowFunctionDefaultParameterType; + +class Foo +{ + + public function doFoo(): void + { + $f = fn (int $i = null) => '1'; + $g = fn (?int $i = null) => '1'; + $h = fn (int $i = 5) => '1'; + $i = fn (int $i = 'foo') => '1'; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/incompatible-default-parameter-type-closure.php b/tests/PHPStan/Rules/Functions/data/incompatible-default-parameter-type-closure.php new file mode 100644 index 0000000000..6037f3f707 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/incompatible-default-parameter-type-closure.php @@ -0,0 +1,24 @@ += 7.4 + +namespace IncompatibleClosureDefaultParameterType; + +class Foo +{ + + public function doFoo(): void + { + $f = function (int $i = null) { + return '1'; + }; + $g = function (?int $i = null) { + return '1'; + }; + $h = function (int $i = 5) { + return '1'; + }; + $i = function (int $i = 'foo') { + return '1'; + }; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/invalid-lexical-variables-in-closure-use.php b/tests/PHPStan/Rules/Functions/data/invalid-lexical-variables-in-closure-use.php new file mode 100644 index 0000000000..310241c852 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/invalid-lexical-variables-in-closure-use.php @@ -0,0 +1,118 @@ +foo())(); + }; + } + + /** + * @return \Closure(): array + */ + public function superglobals(): \Closure + { + return function () use ($GLOBALS, $_COOKIE, $_ENV, $_FILES, $_GET, $_POST, $_REQUEST, $_SERVER, $_SESSION): array { + return array_merge( + $GLOBALS, + $_COOKIE, + $_ENV, + $_FILES, + $_GET, + $_POST, + $_REQUEST, + $_SERVER, + $_SESSION, + ); + }; + } + + /** + * @return \Closure(int, string, bool): bool + */ + public function sameAsParameter(): \Closure + { + return function (int $foo, string $bar, bool $baz) use ($baz): bool { + return $baz; + }; + } + + /** + * @return \Closure(): string + */ + public function multilineThis(): \Closure + { + $message = 'hello'; + + return function () use ( + $this, + $message + ): string { + return ($this->foo())() . $message; + }; + } + + /** + * @return \Closure(): array + */ + public function multilineSuperglobals(): \Closure + { + return function () use ( + $GLOBALS, + $_COOKIE, + $_ENV, + $_FILES, + $_GET, + $_POST, + $_REQUEST, + $_SERVER, + $_SESSION + ): array { + return array_merge( + $GLOBALS, + $_COOKIE, + $_ENV, + $_FILES, + $_GET, + $_POST, + $_REQUEST, + $_SERVER, + $_SESSION, + ); + }; + } + + /** + * @return \Closure(int, string, bool): bool + */ + public function multilineSameAsParameter(): \Closure + { + return function (int $foo, string $bar, bool $baz) use ( + $baz, + $bar, + ): bool { + return (bool) ($baz . $bar); + }; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/json_validate.php b/tests/PHPStan/Rules/Functions/data/json_validate.php new file mode 100644 index 0000000000..08c1268bb7 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/json_validate.php @@ -0,0 +1,13 @@ + $a + * @param-out array $a + */ + function oneArray(&$a): void { + + } + + /** + * @param mixed $a + * @param-out \ReflectionClass $a + */ + function generics(&$a): void { + + } +} + +namespace MissingParamClosureThisType { + /** + * @param-closure-this \ReflectionClass $cb + * @param callable(): void $cb + */ + function generics(callable $cb): void + { + + } +} diff --git a/tests/PHPStan/Rules/Functions/data/param-out.php b/tests/PHPStan/Rules/Functions/data/param-out.php new file mode 100644 index 0000000000..637750be98 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-out.php @@ -0,0 +1,11 @@ + (int) $bar; + + return function (string $baz, int $baz) use ($callback): int { + return $callback($baz, []); + }; + } + + /** + * @return \Closure(string, bool): int + */ + public function bar(string $pipe, int $count): \Closure + { + $cb = fn (int $a, string $b): int => $a + (int) $b; + + return function (string $c, bool $d) use ($cb, $pipe, $count): int { + return $cb((int) $d, $c) + $cb($count, $pipe); + }; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/removed-functions-from-php8.php b/tests/PHPStan/Rules/Functions/data/removed-functions-from-php8.php new file mode 100644 index 0000000000..459dd98d28 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/removed-functions-from-php8.php @@ -0,0 +1,11 @@ += 7.4 += 8.0 namespace RequiredAfterOptional; @@ -9,3 +9,17 @@ fn (int $foo = 1, $bar): int => 1; // not OK fn (bool $foo = true, $bar): int => 1; // not OK + +fn (?int $foo = 1, $bar): int => 1; // not OK + +fn (?int $foo = null, $bar): int => 1; // not OK + +fn (int|null $foo = 1, $bar): int => 1; // not OK + +fn (int|null $foo = null, $bar): int => 1; // not OK + +fn (mixed $foo = 1, $bar): int => 1; // not OK + +fn (mixed $foo = null, $bar): int => 1; // not OK + +fn (int|null $foo = null, $bar, ?int $baz = null, $qux, int $quux = 1, $quuz): int => 1; // not OK diff --git a/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional-closures.php b/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional-closures.php index fdd7db4709..da96ec6909 100644 --- a/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional-closures.php +++ b/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional-closures.php @@ -1,4 +1,4 @@ -= 8.0 namespace RequiredAfterOptional; @@ -17,3 +17,31 @@ function (int $foo = 1, $bar): void // not OK function(bool $foo = true, $bar): void // not OK { }; + +function (?int $foo = 1, $bar): void // not OK +{ +}; + +function (?int $foo = null, $bar): void // not OK +{ +}; + +function (int|null $foo = 1, $bar): void // not OK +{ +}; + +function (int|null $foo = null, $bar): void // not OK +{ +}; + +function (mixed $foo = 1, $bar): void // not OK +{ +}; + +function (mixed $foo = null, $bar): void // not OK +{ +}; + +function (int|null $foo = null, $bar, ?int $baz = null, $qux, int $quux = 1, $quuz): void // not OK +{ +}; diff --git a/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional.php b/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional.php index 373a441903..8937d262a7 100644 --- a/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional.php +++ b/tests/PHPStan/Rules/Functions/data/required-parameter-after-optional.php @@ -1,4 +1,4 @@ -= 8.0 namespace RequiredAfterOptional; @@ -22,3 +22,31 @@ function doLorem(bool $foo = true, $bar): void // not OK function doIpsum(bool $foo = true, ...$bar): void // OK { } + +function doDolor(?int $foo = 1, $bar): void // not OK +{ +} + +function doSit(?int $foo = null, $bar): void // not OK +{ +} + +function doAmet(int|null $foo = 1, $bar): void // not OK +{ +} + +function doConsectetur(int|null $foo = null, $bar): void // not OK +{ +} + +function doAdipiscing(mixed $foo = 1, $bar): void // not OK +{ +} + +function doElit(mixed $foo = null, $bar): void // not OK +{ +} + +function doSed(int|null $foo = null, $bar, ?int $baz = null, $qux, int $quux = 1, $quuz): void // not OK +{ +} diff --git a/tests/PHPStan/Rules/Functions/data/static-call-in-functions.php b/tests/PHPStan/Rules/Functions/data/static-call-in-functions.php new file mode 100644 index 0000000000..149dc64e71 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/static-call-in-functions.php @@ -0,0 +1,34 @@ + static function ($one, $two) { + return $one ?? $two; + }, + 'b' => static function ($one, $two, $three) { + return $one ?? $two ?? $three; + }, + ]; + + foreach (['c', 'd', 'e'] as $name) { + self::$resolvers[$name] = static function ($one, $two) { + return self::$resolvers['a']($one, $two); + }; + + self::$resolvers[$name] = static fn ($one, $two) => self::$resolvers['a']($one, $two); + } + } + + return self::$resolvers; + } +} diff --git a/tests/PHPStan/Rules/Functions/data/variadic-parameters-declaration.php b/tests/PHPStan/Rules/Functions/data/variadic-parameters-declaration.php new file mode 100644 index 0000000000..cc5c195b16 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/variadic-parameters-declaration.php @@ -0,0 +1,23 @@ +format('j. n. Y'); + } + + public function variadicParamAtEnd(int $number, int ...$numbers): void + { + } +} + +function variadicFunction(int ...$a, string $b): void +{ +} diff --git a/tests/PHPStan/Rules/Generators/YieldFromTypeRuleTest.php b/tests/PHPStan/Rules/Generators/YieldFromTypeRuleTest.php index 3f85c5972b..deada99fb1 100644 --- a/tests/PHPStan/Rules/Generators/YieldFromTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generators/YieldFromTypeRuleTest.php @@ -14,7 +14,7 @@ class YieldFromTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new YieldFromTypeRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false), true); + return new YieldFromTypeRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false), true); } public function testRule(): void @@ -39,6 +39,7 @@ public function testRule(): void [ 'Generator expects value type array{DateTime, DateTime, stdClass, DateTimeImmutable}, array{0: DateTime, 1: DateTime, 2: stdClass, 4: DateTimeImmutable} given.', 74, + 'Array does not have offset 3.', ], [ 'Result of yield from (void) is used.', diff --git a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php index 929832b4b3..deec20a074 100644 --- a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php @@ -14,7 +14,7 @@ class YieldTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new YieldTypeRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false)); + return new YieldTypeRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false)); } public function testRule(): void @@ -47,6 +47,7 @@ public function testRule(): void [ 'Generator expects value type array{0: DateTime, 1: DateTime, 2: stdClass, 4: DateTimeImmutable}, array{DateTime, DateTime, stdClass, DateTimeImmutable} given.', 25, + 'Array does not have offset 4.', ], [ 'Result of yield (void) is used.', @@ -65,6 +66,7 @@ public function testBug7484(): void [ 'Generator expects key type K of int|string, (K of int)|string given.', 21, + 'Type string is not always the same as K. It breaks the contract for some argument types, typically subtypes.', ], ]); } diff --git a/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php index fda1c25d6f..ae78243b8c 100644 --- a/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php @@ -17,7 +17,7 @@ protected function getRule(): Rule new GenericAncestorsCheck( $this->createReflectionProvider(), new GenericObjectTypeCheck(), - new VarianceCheck(), + new VarianceCheck(true, true), true, [], ), @@ -95,11 +95,41 @@ public function testRuleExtends(): void 'Template type T is declared as covariant, but occurs in invariant position in extended type ClassAncestorsExtends\FooGeneric8 of class ClassAncestorsExtends\FooGeneric9.', 192, ], + [ + 'Template type T is declared as contravariant, but occurs in covariant position in extended type ClassAncestorsExtends\FooGeneric8 of class ClassAncestorsExtends\FooGeneric10.', + 201, + ], + [ + 'Template type T is declared as contravariant, but occurs in invariant position in extended type ClassAncestorsExtends\FooGeneric8 of class ClassAncestorsExtends\FooGeneric10.', + 201, + ], [ 'Class ClassAncestorsExtends\FilterIteratorChild extends generic class FilterIterator but does not specify its types: TKey, TValue, TIterator', - 197, + 215, + 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + ], + [ + 'Class ClassAncestorsExtends\FooObjectStorage @extends tag contains incompatible type ClassAncestorsExtends\FooObjectStorage.', + 226, + ], + [ + 'Class ClassAncestorsExtends\FooObjectStorage extends generic class SplObjectStorage but does not specify its types: TObject, TData', + 226, + 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + ], + [ + 'Class ClassAncestorsExtends\FooCollection @extends tag contains incompatible type ClassAncestorsExtends\FooCollection&iterable.', + 239, + ], + [ + 'Class ClassAncestorsExtends\FooCollection extends generic class ClassAncestorsExtends\AbstractFooCollection but does not specify its types: T', + 239, 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], + [ + 'Call-site variance annotation of covariant Throwable in generic type ClassAncestorsExtends\FooGeneric in PHPDoc tag @extends is not allowed.', + 246, + ], ]); } @@ -186,6 +216,28 @@ public function testRuleImplements(): void 'Template type T is declared as covariant, but occurs in invariant position in implemented type ClassAncestorsImplements\FooGeneric9 of class ClassAncestorsImplements\FooGeneric10.', 216, ], + [ + 'Class ClassAncestorsImplements\FooIterator @implements tag contains incompatible type ClassAncestorsImplements\FooIterator&iterable.', + 222, + ], + [ + 'Class ClassAncestorsImplements\FooIterator implements generic interface Iterator but does not specify its types: TKey, TValue', + 222, + 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + ], + [ + 'Class ClassAncestorsImplements\FooCollection @implements tag contains incompatible type ClassAncestorsImplements\FooCollection&iterable.', + 235, + ], + [ + 'Class ClassAncestorsImplements\FooCollection implements generic interface ClassAncestorsImplements\AbstractFooCollection but does not specify its types: T', + 235, + 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + ], + [ + 'Call-site variance annotation of covariant Throwable in generic type ClassAncestorsImplements\FooGeneric in PHPDoc tag @implements is not allowed.', + 242, + ], ]); } @@ -224,4 +276,9 @@ public function testScalarClassName(): void $this->analyse([__DIR__ . '/data/scalar-class-name.php'], []); } + public function testBug8473(): void + { + $this->analyse([__DIR__ . '/data/bug-8473.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php index f9d99309ad..788be4cbd5 100644 --- a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php @@ -3,8 +3,11 @@ namespace PHPStan\Rules\Generics; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -14,13 +17,16 @@ class ClassTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new ClassTemplateTypeRule( new TemplateTypeCheck( - $broker, - new ClassCaseSensitivityCheck($broker, true), + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), new GenericObjectTypeCheck(), $typeAliasResolver, true, @@ -71,6 +77,15 @@ public function testRule(): void 'PHPDoc tag @template for anonymous class cannot have existing type alias TypeAlias as its name.', 78, ], + [ + 'Call-site variance of covariant int in generic type ClassTemplateType\Consecteur in PHPDoc tag @template U is redundant, template type T of class ClassTemplateType\Consecteur has the same variance.', + 113, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type ClassTemplateType\Consecteur in PHPDoc tag @template W is in conflict with covariant template type T of class ClassTemplateType\Consecteur.', + 113, + ], ]); } @@ -110,4 +125,17 @@ public function testInInterface(): void $this->analyse([__DIR__ . '/data/interface-template.php'], []); } + public function testBug10049(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $this->analyse([__DIR__ . '/data/bug-10049.php'], [ + [ + 'PHPDoc tag @template for class Bug10049\SimpleEntity cannot have existing class Bug10049\SimpleEntity as its name.', + 8, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php index 22524bd2ce..6b19578ec2 100644 --- a/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php @@ -18,7 +18,7 @@ protected function getRule(): Rule new GenericAncestorsCheck( $this->createReflectionProvider(), new GenericObjectTypeCheck(), - new VarianceCheck(), + new VarianceCheck(true, true), true, [], ), @@ -54,6 +54,10 @@ public function testRule(): void 'Enum EnumGenericAncestors\Foo7 has @extends tag, but cannot extend anything.', 64, ], + [ + 'Call-site variance annotation of covariant EnumGenericAncestors\NonGeneric in generic type EnumGenericAncestors\Generic in PHPDoc tag @implements is not allowed.', + 93, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php index 22cb75b8b4..35df206865 100644 --- a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Generics; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; @@ -15,12 +17,21 @@ class FunctionTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new FunctionTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker, true), new GenericObjectTypeCheck(), $typeAliasResolver, true), + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } @@ -47,6 +58,15 @@ public function testRule(): void 'PHPDoc tag @template T for function FunctionTemplateType\nullNotSupported() with bound type null is not supported.', 68, ], + [ + 'Call-site variance of covariant int in generic type FunctionTemplateType\GenericCovariant in PHPDoc tag @template U is redundant, template type T of class FunctionTemplateType\GenericCovariant has the same variance.', + 94, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type FunctionTemplateType\GenericCovariant in PHPDoc tag @template W is in conflict with covariant template type T of class FunctionTemplateType\GenericCovariant.', + 94, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php index 63855f7c05..f0e148bf56 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php @@ -17,7 +17,7 @@ protected function getRule(): Rule new GenericAncestorsCheck( $this->createReflectionProvider(), new GenericObjectTypeCheck(), - new VarianceCheck(), + new VarianceCheck(true, true), true, [], ), @@ -108,6 +108,10 @@ public function testRuleImplements(): void 'Interface InterfaceAncestorsImplements\FooGenericGeneric8 has @implements tag, but can not implement any interface, must extend from it.', 182, ], + [ + 'Interface InterfaceAncestorsImplements\FooTypeProjection has @implements tag, but can not implement any interface, must extend from it.', + 190, + ], ]); } @@ -194,6 +198,10 @@ public function testRuleExtends(): void 'Template type T is declared as covariant, but occurs in invariant position in extended type InterfaceAncestorsExtends\FooGeneric9 of interface InterfaceAncestorsExtends\FooGeneric10.', 215, ], + [ + 'Call-site variance annotation of covariant LogicException in generic type InterfaceAncestorsExtends\FooGeneric in PHPDoc tag @extends is not allowed.', + 223, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php index 243a7bd69f..0623a7d1c2 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Generics; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -14,11 +16,20 @@ class InterfaceTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new InterfaceTemplateTypeRule( - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker, true), new GenericObjectTypeCheck(), $typeAliasResolver, true), + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } @@ -45,6 +56,15 @@ public function testRule(): void 'PHPDoc tag @template for interface InterfaceTemplateType\Ipsum cannot have existing type alias ImportedAlias as its name.', 45, ], + [ + 'Call-site variance of covariant int in generic type InterfaceTemplateType\Covariant in PHPDoc tag @template U is redundant, template type T of interface InterfaceTemplateType\Covariant has the same variance.', + 74, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type InterfaceTemplateType\Covariant in PHPDoc tag @template W is in conflict with covariant template type T of interface InterfaceTemplateType\Covariant.', + 74, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php b/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php index b0b64eb766..8f743f4dc7 100644 --- a/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php @@ -22,25 +22,215 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/method-signature-variance.php'], [ [ - 'Template type T is declared as covariant, but occurs in contravariant position in parameter a of method MethodSignatureVariance\C::a().', - 25, + 'Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type U in in method MethodSignatureVariance\C::b().', + 16, ], [ - 'Template type T is declared as covariant, but occurs in invariant position in parameter b of method MethodSignatureVariance\C::a().', - 25, + 'Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type U in in method MethodSignatureVariance\C::c().', + 22, ], + ]); + + $this->analyse([__DIR__ . '/data/method-signature-variance-invariant.php'], []); + + $this->analyse([__DIR__ . '/data/method-signature-variance-covariant.php'], [ [ - 'Template type T is declared as covariant, but occurs in contravariant position in parameter c of method MethodSignatureVariance\C::a().', - 25, + 'Template type X is declared as covariant, but occurs in contravariant position in parameter a of method MethodSignatureVariance\Covariant\C::a().', + 35, ], [ - 'Template type W is declared as covariant, but occurs in contravariant position in parameter d of method MethodSignatureVariance\C::a().', - 25, + 'Template type X is declared as covariant, but occurs in contravariant position in parameter c of method MethodSignatureVariance\Covariant\C::a().', + 35, ], [ - 'Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type U in in method MethodSignatureVariance\C::b().', + 'Template type X is declared as covariant, but occurs in invariant position in parameter e of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in parameter f of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in parameter h of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in parameter i of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in parameter j of method MethodSignatureVariance\Covariant\C::a().', 35, ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in parameter k of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in parameter l of method MethodSignatureVariance\Covariant\C::a().', + 35, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in return type of method MethodSignatureVariance\Covariant\C::c().', + 41, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in return type of method MethodSignatureVariance\Covariant\C::e().', + 47, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in return type of method MethodSignatureVariance\Covariant\C::f().', + 50, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in return type of method MethodSignatureVariance\Covariant\C::h().', + 56, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in return type of method MethodSignatureVariance\Covariant\C::j().', + 62, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in return type of method MethodSignatureVariance\Covariant\C::k().', + 65, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in return type of method MethodSignatureVariance\Covariant\C::l().', + 68, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in return type of method MethodSignatureVariance\Covariant\C::m().', + 71, + ], + ]); + + $this->analyse([__DIR__ . '/data/method-signature-variance-contravariant.php'], [ + [ + 'Template type X is declared as contravariant, but occurs in covariant position in parameter b of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in parameter d of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in parameter e of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in parameter g of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in parameter i of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in parameter j of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in parameter k of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in parameter l of method MethodSignatureVariance\Contravariant\C::a().', + 35, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\Contravariant\C::b().', + 38, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\Contravariant\C::d().', + 44, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in return type of method MethodSignatureVariance\Contravariant\C::f().', + 50, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\Contravariant\C::g().', + 53, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\Contravariant\C::i().', + 59, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in return type of method MethodSignatureVariance\Contravariant\C::j().', + 62, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in return type of method MethodSignatureVariance\Contravariant\C::k().', + 65, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in return type of method MethodSignatureVariance\Contravariant\C::l().', + 68, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in return type of method MethodSignatureVariance\Contravariant\C::m().', + 71, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in param-out type of parameter a of method MethodSignatureVariance\Contravariant\C::paramOut().', + 79, + ], + ]); + + $this->analyse([__DIR__ . '/data/method-signature-variance-constructor.php'], []); + + $this->analyse([__DIR__ . '/data/method-signature-variance-static.php'], [ + [ + 'Template type X is declared as covariant, but occurs in contravariant position in parameter a of method MethodSignatureVariance\StaticMethod\B::a().', + 43, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in parameter c of method MethodSignatureVariance\StaticMethod\B::a().', + 43, + ], + [ + 'Template type X is declared as covariant, but occurs in contravariant position in return type of method MethodSignatureVariance\StaticMethod\B::c().', + 49, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in parameter b of method MethodSignatureVariance\StaticMethod\C::a().', + 62, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\StaticMethod\C::b().', + 65, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\StaticMethod\C::d().', + 71, + ], + ]); + } + + public function testBug8880(): void + { + $this->analyse([__DIR__ . '/data/bug-8880.php'], [ + [ + 'Template type T is declared as covariant, but occurs in contravariant position in parameter items of method Bug8880\IProcessor::processItems().', + 17, + ], + ]); + } + + public function testBug9161(): void + { + $this->analyse([__DIR__ . '/data/bug-9161.php'], []); + } + + public function testPr2465(): void + { + $this->analyse([__DIR__ . '/data/pr-2465.php'], [ + [ + 'Template type T is declared as covariant, but occurs in invariant position in parameter thing of method Pr2465\UnitOfTest::foo().', + 16, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/MethodTagTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/MethodTagTemplateTypeRuleTest.php new file mode 100644 index 0000000000..e754794566 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/MethodTagTemplateTypeRuleTest.php @@ -0,0 +1,60 @@ + + */ +class MethodTagTemplateTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); + + return new MethodTagTemplateTypeRule( + self::getContainer()->getByType(FileTypeMapper::class), + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/method-tag-template.php'], [ + [ + 'PHPDoc tag @method template U for method MethodTagTemplate\HelloWorld::sayHello() has invalid bound type MethodTagTemplate\Nonexisting.', + 13, + ], + [ + 'PHPDoc tag @method template for method MethodTagTemplate\HelloWorld::sayHello() cannot have existing class stdClass as its name.', + 13, + ], + [ + 'PHPDoc tag @method template T for method MethodTagTemplate\HelloWorld::sayHello() shadows @template T for class MethodTagTemplate\HelloWorld.', + 13, + ], + [ + 'PHPDoc tag @method template for method MethodTagTemplate\HelloWorld::typeAlias() cannot have existing type alias TypeAlias as its name.', + 13, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php index c46eb033db..a845993344 100644 --- a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Generics; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; @@ -15,12 +17,21 @@ class MethodTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new MethodTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker, true), new GenericObjectTypeCheck(), $typeAliasResolver, true), + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } @@ -53,6 +64,15 @@ public function testRule(): void 'PHPDoc tag @template for method MethodTemplateType\Ipsum::doFoo() cannot have existing type alias ImportedAlias as its name.', 85, ], + [ + 'Call-site variance of covariant int in generic type MethodTemplateType\Dolor in PHPDoc tag @template U is redundant, template type T of class MethodTemplateType\Dolor has the same variance.', + 109, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type MethodTemplateType\Dolor in PHPDoc tag @template W is in conflict with covariant template type T of class MethodTemplateType\Dolor.', + 109, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/PropertyVarianceRuleTest.php b/tests/PHPStan/Rules/Generics/PropertyVarianceRuleTest.php new file mode 100644 index 0000000000..97870f3572 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/PropertyVarianceRuleTest.php @@ -0,0 +1,142 @@ + + */ +class PropertyVarianceRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PropertyVarianceRule( + self::getContainer()->getByType(VarianceCheck::class), + true, + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/property-variance.php'], [ + [ + 'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\B::$a.', + 51, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\B::$b.', + 54, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\B::$c.', + 57, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\B::$d.', + 60, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\C::$a.', + 80, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\C::$b.', + 83, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\C::$c.', + 86, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\C::$d.', + 89, + ], + ]); + } + + public function testPromoted(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/property-variance-promoted.php'], [ + [ + 'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$a.', + 58, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$b.', + 59, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$c.', + 60, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\Promoted\B::$d.', + 61, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$a.', + 84, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$b.', + 85, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$c.', + 86, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\Promoted\C::$d.', + 87, + ], + ]); + } + + public function testReadOnly(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/property-variance-readonly.php'], [ + [ + 'Template type X is declared as covariant, but occurs in contravariant position in property PropertyVariance\ReadOnly\B::$b.', + 45, + ], + [ + 'Template type X is declared as covariant, but occurs in invariant position in property PropertyVariance\ReadOnly\B::$d.', + 51, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in property PropertyVariance\ReadOnly\C::$a.', + 62, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in property PropertyVariance\ReadOnly\C::$c.', + 68, + ], + [ + 'Template type X is declared as contravariant, but occurs in invariant position in property PropertyVariance\ReadOnly\C::$d.', + 71, + ], + [ + 'Template type X is declared as contravariant, but occurs in covariant position in property PropertyVariance\ReadOnly\D::$a.', + 86, + ], + ]); + } + + public function testBug9153(): void + { + $this->analyse([__DIR__ . '/data/bug-9153.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php index 135d9e78be..99ad839123 100644 --- a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Generics; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; @@ -15,12 +17,21 @@ class TraitTemplateTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); return new TraitTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker, true), new GenericObjectTypeCheck(), $typeAliasResolver, true), + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), ); } @@ -49,6 +60,15 @@ public function testRule(): void 'PHPDoc tag @template for trait TraitTemplateType\Ipsum cannot have existing type alias ImportedAlias as its name.', 45, ], + [ + 'Call-site variance of covariant int in generic type TraitTemplateType\Dolor in PHPDoc tag @template U is redundant, template type T of class TraitTemplateType\Dolor has the same variance.', + 64, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type TraitTemplateType\Dolor in PHPDoc tag @template W is in conflict with covariant template type T of class TraitTemplateType\Dolor.', + 64, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php b/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php index aead3360e2..3ec30d55c6 100644 --- a/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php @@ -19,7 +19,7 @@ protected function getRule(): Rule new GenericAncestorsCheck( $this->createReflectionProvider(), new GenericObjectTypeCheck(), - new VarianceCheck(), + new VarianceCheck(true, true), true, [], ), @@ -55,6 +55,10 @@ public function testRule(): void 54, 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], + [ + 'Call-site variance annotation of covariant Throwable in generic type UsedTraits\GenericTrait in PHPDoc tag @use is not allowed.', + 69, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/data/bug-10049.php b/tests/PHPStan/Rules/Generics/data/bug-10049.php new file mode 100644 index 0000000000..e32a2d7399 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-10049.php @@ -0,0 +1,41 @@ += 8.1 + +namespace Bug10049; + +/** + * @template SELF of SimpleEntity + */ +abstract class SimpleEntity +{ + /** + * @param SimpleTable $table + */ + public function __construct(protected readonly SimpleTable $table) + { + } +} + +/** + * @template-covariant E of SimpleEntity + */ +class SimpleTable +{ + /** + * @template ENTITY of SimpleEntity + * + * @param class-string $className + * + * @return SimpleTable + */ + public static function table(string $className, string $name): SimpleTable + { + return new SimpleTable($className, $name); + } + + /** + * @param class-string $className + */ + private function __construct(readonly string $className, readonly string $table) + { + } +} diff --git a/tests/PHPStan/Rules/Generics/data/bug-3769.php b/tests/PHPStan/Rules/Generics/data/bug-3769.php index 1101ad3547..aeb273d479 100644 --- a/tests/PHPStan/Rules/Generics/data/bug-3769.php +++ b/tests/PHPStan/Rules/Generics/data/bug-3769.php @@ -29,6 +29,7 @@ function foo( $a = assertType('array', stringValues($foo)); $a = assertType('array', stringValues($bar)); $a = assertType('array', stringValues($baz)); + echo 'test'; }; /** @@ -37,6 +38,7 @@ function foo( */ function fooUnion($foo): void { $a = assertType('T of Exception|stdClass (function Bug3769\fooUnion(), argument)', $foo); + echo 'test'; } /** @@ -70,8 +72,8 @@ function stringBound(string $a) } function (): void { - $a = assertType('int', mixedBound(1)); - $a = assertType('string', mixedBound('str')); + $a = assertType('1', mixedBound(1)); + $a = assertType('\'str\'', mixedBound('str')); $a = assertType('1', intBound(1)); $a = assertType('\'str\'', stringBound('str')); }; diff --git a/tests/PHPStan/Rules/Generics/data/bug-8473.php b/tests/PHPStan/Rules/Generics/data/bug-8473.php new file mode 100644 index 0000000000..a4c46fe246 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-8473.php @@ -0,0 +1,23 @@ + */ +class AccountCollection extends Paginator +{ +} + +class AccountEntity +{} diff --git a/tests/PHPStan/Rules/Generics/data/bug-8880.php b/tests/PHPStan/Rules/Generics/data/bug-8880.php new file mode 100644 index 0000000000..4f3b1d39f5 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-8880.php @@ -0,0 +1,34 @@ + $items + * @return void + */ + function processItems($items); +} + +/** @implements IProcessor */ +final class StringPrinter implements IProcessor { + function processItems($items) { + foreach ($items as $s) + putStrLn($s); + } +} + +/** + * @param IProcessor $p + * @return void + */ +function callWithInt($p) { + $p->processItems([1]); +} diff --git a/tests/PHPStan/Rules/Generics/data/bug-9153.php b/tests/PHPStan/Rules/Generics/data/bug-9153.php new file mode 100644 index 0000000000..d78363dd69 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-9153.php @@ -0,0 +1,22 @@ + + * + * @immutable + */ +final class LanguageProperty +{ + /** @var Value */ + public $value; + + /** + * @param Value $value + */ + public function __construct($value) + { + $this->value = $value; + } +} diff --git a/tests/PHPStan/Rules/Generics/data/bug-9161.php b/tests/PHPStan/Rules/Generics/data/bug-9161.php new file mode 100644 index 0000000000..05c2fb07ba --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-9161.php @@ -0,0 +1,39 @@ += 8.0 + +namespace Bug9161; + +/** + * @template-covariant TKey of int|string + * @template-covariant TValue + */ +final class Map +{ + /** + * @param array $items + */ + public function __construct( + private array $items = [], + ) { + } + + /** + * @return array + */ + public function toArray(): array + { + return $this->items; + } + + /** + * @return list + */ + public function toPairs(): array + { + $pairs = []; + foreach ($this->items as $key => $value) { + $pairs[] = [$key, $value]; + } + + return $pairs; + } +} diff --git a/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php b/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php index caa906001a..bb942838ed 100644 --- a/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php +++ b/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php @@ -194,6 +194,24 @@ class FooGeneric9 extends FooGeneric8 } +/** + * @template-contravariant T + * @extends FooGeneric8 + */ +class FooGeneric10 extends FooGeneric8 +{ + +} + +/** + * @template T + * @extends FooGeneric8 + */ +class FooGeneric11 extends FooGeneric8 +{ + +} + class FilterIteratorChild extends \FilterIterator { @@ -203,3 +221,29 @@ public function accept() } } + +/** @extends FooObjectStorage */ +class FooObjectStorage extends \SplObjectStorage +{ +} + +/** + * @template T + * @implements \Iterator + */ +abstract class AbstractFooCollection implements \Iterator +{ +} + +/** @extends FooCollection */ +class FooCollection extends AbstractFooCollection +{ +} + +/** + * @extends FooGeneric + */ +class FooTypeProjection extends FooGeneric +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/class-ancestors-implements.php b/tests/PHPStan/Rules/Generics/data/class-ancestors-implements.php index 716c58c2b7..7a016c36ce 100644 --- a/tests/PHPStan/Rules/Generics/data/class-ancestors-implements.php +++ b/tests/PHPStan/Rules/Generics/data/class-ancestors-implements.php @@ -217,3 +217,28 @@ class FooGeneric10 implements FooGeneric9 { } + +/** @implements FooIterator */ +class FooIterator implements \Iterator +{ +} + +/** + * @template T + * @implements \Iterator + */ +interface AbstractFooCollection extends \Iterator +{ +} + +/** @implements FooCollection */ +class FooCollection implements AbstractFooCollection +{ +} + +/** + * @implements FooGeneric + */ +class FooTypeProjection implements FooGeneric +{ +} diff --git a/tests/PHPStan/Rules/Generics/data/class-template.php b/tests/PHPStan/Rules/Generics/data/class-template.php index 458095fbcd..a76c2eeab0 100644 --- a/tests/PHPStan/Rules/Generics/data/class-template.php +++ b/tests/PHPStan/Rules/Generics/data/class-template.php @@ -95,3 +95,22 @@ class Amet { } + +/** + * @template-covariant T + */ +class Consecteur +{ + +} + +/** + * @template T of Consecteur + * @template U of Consecteur + * @template V of Consecteur<*> + * @template W of Consecteur + */ +class Adipiscing +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/enum-ancestors.php b/tests/PHPStan/Rules/Generics/data/enum-ancestors.php index 01066a0e88..e5d7848498 100644 --- a/tests/PHPStan/Rules/Generics/data/enum-ancestors.php +++ b/tests/PHPStan/Rules/Generics/data/enum-ancestors.php @@ -86,3 +86,11 @@ public function getIterator() } } + +/** + * @implements Generic + */ +enum TypeProjection implements Generic +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/function-template.php b/tests/PHPStan/Rules/Generics/data/function-template.php index 7124e4c138..8b9de2cdaf 100644 --- a/tests/PHPStan/Rules/Generics/data/function-template.php +++ b/tests/PHPStan/Rules/Generics/data/function-template.php @@ -75,3 +75,23 @@ function nullableUnionSupported() { } + +/** @template T of object{foo: int} */ +function objectShapes() +{ + +} + +/** @template-covariant T */ +class GenericCovariant {} + +/** + * @template T of GenericCovariant + * @template U of GenericCovariant + * @template V of GenericCovariant<*> + * @template W of GenericCovariant + */ +function typeProjections() +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/interface-ancestors-extends.php b/tests/PHPStan/Rules/Generics/data/interface-ancestors-extends.php index 510c9451b8..64e5e92129 100644 --- a/tests/PHPStan/Rules/Generics/data/interface-ancestors-extends.php +++ b/tests/PHPStan/Rules/Generics/data/interface-ancestors-extends.php @@ -216,3 +216,11 @@ interface FooGeneric10 extends FooGeneric9 { } + +/** + * @extends FooGeneric + */ +interface FooTypeProjection extends FooGeneric +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/interface-ancestors-implements.php b/tests/PHPStan/Rules/Generics/data/interface-ancestors-implements.php index 262e07e56a..4d15ae57c0 100644 --- a/tests/PHPStan/Rules/Generics/data/interface-ancestors-implements.php +++ b/tests/PHPStan/Rules/Generics/data/interface-ancestors-implements.php @@ -183,3 +183,11 @@ interface FooGenericGeneric8 { } + +/** + * @implements FooGeneric + */ +interface FooTypeProjection +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/interface-template.php b/tests/PHPStan/Rules/Generics/data/interface-template.php index ed7f3ef667..8ae819c99b 100644 --- a/tests/PHPStan/Rules/Generics/data/interface-template.php +++ b/tests/PHPStan/Rules/Generics/data/interface-template.php @@ -58,3 +58,20 @@ interface UnionBound { } + +/** @template-covariant T */ +interface Covariant +{ + +} + +/** + * @template T of Covariant + * @template U of Covariant + * @template V of Covariant<*> + * @template W of Covariant + */ +interface TypeProjections +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/method-signature-variance-constructor.php b/tests/PHPStan/Rules/Generics/data/method-signature-variance-constructor.php new file mode 100644 index 0000000000..57d4dfc3b9 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance-constructor.php @@ -0,0 +1,72 @@ + $b + * @param In> $c + * @param In> $d + * @param In> $e + * @param Out $f + * @param Out> $g + * @param Out> $h + * @param Out> $i + * @param Invariant $j + * @param Invariant> $k + * @param Invariant> $l + */ + function __construct($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l) {} +} + +/** @template-covariant X */ +class B { + /** + * @param X $a + * @param In $b + * @param In> $c + * @param In> $d + * @param In> $e + * @param Out $f + * @param Out> $g + * @param Out> $h + * @param Out> $i + * @param Invariant $j + * @param Invariant> $k + * @param Invariant> $l + */ + function __construct($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l) {} +} + +/** @template-contravariant X */ +class C { + /** + * @param X $a + * @param In $b + * @param In> $c + * @param In> $d + * @param In> $e + * @param Out $f + * @param Out> $g + * @param Out> $h + * @param Out> $i + * @param Invariant $j + * @param Invariant> $k + * @param Invariant> $l + */ + function __construct($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l) {} +} diff --git a/tests/PHPStan/Rules/Generics/data/method-signature-variance-contravariant.php b/tests/PHPStan/Rules/Generics/data/method-signature-variance-contravariant.php new file mode 100644 index 0000000000..311958192a --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance-contravariant.php @@ -0,0 +1,83 @@ + $b + * @param In> $c + * @param In> $d + * @param In> $e + * @param Out $f + * @param Out> $g + * @param Out> $h + * @param Out> $i + * @param Invariant $j + * @param Invariant> $k + * @param Invariant> $l + */ + function a($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l) {} + + /** @return X */ + function b() {} + + /** @return In */ + function c() {} + + /** @return In> */ + function d() {} + + /** @return In> */ + function e() {} + + /** @return In> */ + function f() {} + + /** @return Out */ + function g() {} + + /** @return Out> */ + function h() {} + + /** @return Out> */ + function i() {} + + /** @return Out> */ + function j() {} + + /** @return Invariant */ + function k() {} + + /** @return Invariant> */ + function l() {} + + /** @return Invariant> */ + function m() {} + + /** @return X */ + private function n() {} + + /** + * @param-out X $a + */ + public function paramOut(&$a) + { + + } +} diff --git a/tests/PHPStan/Rules/Generics/data/method-signature-variance-covariant.php b/tests/PHPStan/Rules/Generics/data/method-signature-variance-covariant.php new file mode 100644 index 0000000000..4837dbba5d --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance-covariant.php @@ -0,0 +1,75 @@ + $b + * @param In> $c + * @param In> $d + * @param In> $e + * @param Out $f + * @param Out> $g + * @param Out> $h + * @param Out> $i + * @param Invariant $j + * @param Invariant> $k + * @param Invariant> $l + */ + function a($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l) {} + + /** @return X */ + function b() {} + + /** @return In */ + function c() {} + + /** @return In> */ + function d() {} + + /** @return In> */ + function e() {} + + /** @return In> */ + function f() {} + + /** @return Out */ + function g() {} + + /** @return Out> */ + function h() {} + + /** @return Out> */ + function i() {} + + /** @return Out> */ + function j() {} + + /** @return Invariant */ + function k() {} + + /** @return Invariant> */ + function l() {} + + /** @return Invariant> */ + function m() {} + + /** @param X $n */ + private function n($n) {} +} diff --git a/tests/PHPStan/Rules/Generics/data/method-signature-variance-invariant.php b/tests/PHPStan/Rules/Generics/data/method-signature-variance-invariant.php new file mode 100644 index 0000000000..54dd3e4eb7 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance-invariant.php @@ -0,0 +1,70 @@ + $b + * @param In> $c + * @param In> $d + * @param In $e + * @param Out $f + * @param Out> $g + * @param Out> $h + * @param Out $i + * @param Invariant $j + * @param Invariant> $k + * @param Invariant> $l + * @return X + */ + function a($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l) {} + + /** @return In */ + function b() {} + + /** @return In>*/ + function c() {} + + /** @return In>*/ + function d() {} + + /** @return In */ + function e() {} + + /** @return Out */ + function f() {} + + /** @return Out>*/ + function g() {} + + /** @return Out>*/ + function h() {} + + /** @return Out */ + function i() {} + + /** @return Invariant */ + function j() {} + + /** @return Invariant>*/ + function k() {} + + /** @return Invariant>*/ + function l() {} +} diff --git a/tests/PHPStan/Rules/Generics/data/method-signature-variance-static.php b/tests/PHPStan/Rules/Generics/data/method-signature-variance-static.php new file mode 100644 index 0000000000..692f6672f2 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance-static.php @@ -0,0 +1,72 @@ + $b + * @param Out $c + */ + static function a($a, $b, $c) {} + + /** @return X */ + static function b() {} + + /** @return In */ + static function c() {} + + /** @return Out */ + static function d() {} +} + +/** @template-covariant X */ +class B { + /** + * @param X $a + * @param In $b + * @param Out $c + */ + static function a($a, $b, $c) {} + + /** @return X */ + static function b() {} + + /** @return In */ + static function c() {} + + /** @return Out */ + static function d() {} +} + +/** @template-contravariant X */ +class C { + /** + * @param X $a + * @param In $b + * @param Out $c + */ + static function a($a, $b, $c) {} + + /** @return X */ + static function b() {} + + /** @return In */ + static function c() {} + + /** @return Out */ + static function d() {} +} diff --git a/tests/PHPStan/Rules/Generics/data/method-signature-variance.php b/tests/PHPStan/Rules/Generics/data/method-signature-variance.php index c783c69191..e7ffcbfaa2 100644 --- a/tests/PHPStan/Rules/Generics/data/method-signature-variance.php +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance.php @@ -2,37 +2,22 @@ namespace MethodSignatureVariance; -/** @template-covariant T */ -interface Out { -} - -/** @template T */ -interface Invariant { -} - -/** - * @template-covariant T - * @template-covariant W of \DateTimeInterface - */ class C { /** - * @param Out $a - * @param Invariant $b - * @param T $c - * @param W $d - * @return T + * @template U + * @return void */ - function a($a, $b, $c, $d) { - return $c; - } + function a() {} + /** * @template-covariant U - * @param Out $a - * @param Invariant $b - * @param U $c - * @return U + * @return void + */ + function b() {} + + /** + * @template-contravariant U + * @return void */ - function b($a, $b, $c) { - return $c; - } + function c() {} } diff --git a/tests/PHPStan/Rules/Generics/data/method-tag-template.php b/tests/PHPStan/Rules/Generics/data/method-tag-template.php new file mode 100644 index 0000000000..77a6f202c2 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/method-tag-template.php @@ -0,0 +1,15 @@ +(T $a, U $b, stdClass $c) + * @method void typeAlias(TypeAlias $a) + */ +class HelloWorld +{ +} diff --git a/tests/PHPStan/Rules/Generics/data/method-template.php b/tests/PHPStan/Rules/Generics/data/method-template.php index fc6c4c87e2..0fc67c1b24 100644 --- a/tests/PHPStan/Rules/Generics/data/method-template.php +++ b/tests/PHPStan/Rules/Generics/data/method-template.php @@ -88,3 +88,27 @@ public function doFoo() } } + +/** + * @template-covariant T + */ +class Dolor +{ + +} + +class Sit +{ + + /** + * @template T of Dolor + * @template U of Dolor + * @template V of Dolor<*> + * @template W of Dolor + */ + public function doSit() + { + + } + +} diff --git a/tests/PHPStan/Rules/Generics/data/pr-2465.php b/tests/PHPStan/Rules/Generics/data/pr-2465.php new file mode 100644 index 0000000000..fe07d78e9d --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/pr-2465.php @@ -0,0 +1,17 @@ +> $thing + */ + public function foo(InvariantThing $thing): void {} +} diff --git a/tests/PHPStan/Rules/Generics/data/property-variance-promoted.php b/tests/PHPStan/Rules/Generics/data/property-variance-promoted.php new file mode 100644 index 0000000000..fa56e56822 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/property-variance-promoted.php @@ -0,0 +1,93 @@ += 8.0 + +namespace PropertyVariance\Promoted; + +/** @template-contravariant T */ +interface In { +} + +/** @template-covariant T */ +interface Out { +} + +/** @template T */ +interface Invariant { +} + +/** + * @template X + */ +class A { + /** + * @param X $a + * @param In $b + * @param Out $c + * @param Invariant $d + * @param X $e + * @param In $f + * @param Out $g + * @param Invariant $h + */ + public function __construct( + public $a, + public $b, + public $c, + public $d, + private $e, + private $f, + private $g, + private $h, + ) {} +} + +/** + * @template-covariant X + */ +class B { + /** + * @param X $a + * @param In $b + * @param Out $c + * @param Invariant $d + * @param X $e + * @param In $f + * @param Out $g + * @param Invariant $h + */ + public function __construct( + public $a, + public $b, + public $c, + public $d, + private $e, + private $f, + private $g, + private $h, + ) {} +} + +/** + * @template-contravariant X + */ +class C { + /** + * @param X $a + * @param In $b + * @param Out $c + * @param Invariant $d + * @param X $e + * @param In $f + * @param Out $g + * @param Invariant $h + */ + public function __construct( + public $a, + public $b, + public $c, + public $d, + private $e, + private $f, + private $g, + private $h, + ) {} +} diff --git a/tests/PHPStan/Rules/Generics/data/property-variance-readonly.php b/tests/PHPStan/Rules/Generics/data/property-variance-readonly.php new file mode 100644 index 0000000000..faefc2bd8b --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/property-variance-readonly.php @@ -0,0 +1,89 @@ += 8.1 + +namespace PropertyVariance\ReadOnly; + +/** @template-contravariant T */ +interface In { +} + +/** @template-covariant T */ +interface Out { +} + +/** @template T */ +interface Invariant { +} + +/** + * @template X + */ +class A { + /** @var X */ + public readonly mixed $a; + + /** @var In */ + public readonly mixed $b; + + /** @var Out */ + public readonly mixed $c; + + /** @var Invariant */ + public readonly mixed $d; + + /** @var Invariant */ + private readonly mixed $e; +} + +/** + * @template-covariant X + */ +class B { + /** @var X */ + public readonly mixed $a; + + /** @var In */ + public readonly mixed $b; + + /** @var Out */ + public readonly mixed $c; + + /** @var Invariant */ + public readonly mixed $d; + + /** @var Invariant */ + private readonly mixed $e; +} + +/** + * @template-contravariant X + */ +class C { + /** @var X */ + public readonly mixed $a; + + /** @var In */ + public readonly mixed $b; + + /** @var Out */ + public readonly mixed $c; + + /** @var Invariant */ + public readonly mixed $d; + + /** @var Invariant */ + private readonly mixed $e; +} + +/** + * @template-contravariant X + */ +class D { + /** + * @param X $a + * @param X $b + */ + public function __construct( + public readonly mixed $a, + private readonly mixed $b, + ) {} +} diff --git a/tests/PHPStan/Rules/Generics/data/property-variance.php b/tests/PHPStan/Rules/Generics/data/property-variance.php new file mode 100644 index 0000000000..a7c9406b8e --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/property-variance.php @@ -0,0 +1,102 @@ + */ + public $b; + + /** @var Out */ + public $c; + + /** @var Invariant */ + public $d; + + /** @var X */ + private $e; + + /** @var In */ + private $f; + + /** @var Out */ + private $g; + + /** @var Invariant */ + private $h; +} + +/** + * @template-covariant X + */ +class B { + /** @var X */ + public $a; + + /** @var In */ + public $b; + + /** @var Out */ + public $c; + + /** @var Invariant */ + public $d; + + /** @var X */ + private $e; + + /** @var In */ + private $f; + + /** @var Out */ + private $g; + + /** @var Invariant */ + private $h; +} + +/** + * @template-contravariant X + */ +class C { + /** @var X */ + public $a; + + /** @var In */ + public $b; + + /** @var Out */ + public $c; + + /** @var Invariant */ + public $d; + + /** @var X */ + private $e; + + /** @var In */ + private $f; + + /** @var Out */ + private $g; + + /** @var Invariant */ + private $h; +} diff --git a/tests/PHPStan/Rules/Generics/data/trait-template.php b/tests/PHPStan/Rules/Generics/data/trait-template.php index 8870b4dd98..7c9e179295 100644 --- a/tests/PHPStan/Rules/Generics/data/trait-template.php +++ b/tests/PHPStan/Rules/Generics/data/trait-template.php @@ -46,3 +46,22 @@ trait Ipsum { } + +/** + * @template-covariant T + */ +class Dolor +{ + +} + +/** + * @template T of Dolor + * @template U of Dolor + * @template V of Dolor<*> + * @template W of Dolor + */ +trait Sit +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/used-traits.php b/tests/PHPStan/Rules/Generics/data/used-traits.php index 855d38aa02..f01c5e9dfb 100644 --- a/tests/PHPStan/Rules/Generics/data/used-traits.php +++ b/tests/PHPStan/Rules/Generics/data/used-traits.php @@ -61,3 +61,11 @@ class Ipsum use NestedTrait; } + +class Dolor +{ + + /** @use GenericTrait */ + use GenericTrait; + +} diff --git a/tests/PHPStan/Rules/Ignore/IgnoreParseErrorRuleTest.php b/tests/PHPStan/Rules/Ignore/IgnoreParseErrorRuleTest.php new file mode 100644 index 0000000000..ea1b9d67b9 --- /dev/null +++ b/tests/PHPStan/Rules/Ignore/IgnoreParseErrorRuleTest.php @@ -0,0 +1,55 @@ + + */ +class IgnoreParseErrorRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new IgnoreParseErrorRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/ignore-parse-error.php'], [ + [ + 'Parse error in @phpstan-ignore: Unexpected comma (,)', + 10, + ], + [ + 'Parse error in @phpstan-ignore: Closing parenthesis ")" before opening parenthesis "("', + 13, + ], + [ + 'Parse error in @phpstan-ignore: Unclosed opening parenthesis "(" without closing parenthesis ")"', + 18, + ], + [ + 'Parse error in @phpstan-ignore: First token is not an identifier', + 23, + ], + [ + 'Parse error in @phpstan-ignore: Missing identifier', + 27, + ], + ]); + } + + public function testRuleWithUnusedTrait(): void + { + $this->analyse([__DIR__ . '/data/ignore-parse-error-trait.php'], [ + [ + 'Parse error in @phpstan-ignore: Unexpected comma (,)', + 10, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Ignore/data/ignore-parse-error-trait.php b/tests/PHPStan/Rules/Ignore/data/ignore-parse-error-trait.php new file mode 100644 index 0000000000..176089f480 --- /dev/null +++ b/tests/PHPStan/Rules/Ignore/data/ignore-parse-error-trait.php @@ -0,0 +1,13 @@ + + */ +class DeclareStrictTypesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DeclareStrictTypesRule(new ExprPrinter(new Printer())); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/declare-position.php'], [ + [ + 'Declare strict_types must be the very first statement.', + 5, + ], + ]); + } + + public function testRule2(): void + { + $this->analyse([__DIR__ . '/data/declare-position2.php'], [ + [ + 'Declare strict_types must be the very first statement.', + 1, + ], + ]); + } + + public function testNested(): void + { + $this->analyse([__DIR__ . '/data/declare-position-nested.php'], [ + [ + 'Declare strict_types must be the very first statement.', + 7, + ], + [ + 'Declare strict_types must be the very first statement.', + 12, + ], + ]); + } + + public function testValidPosition(): void + { + $this->analyse([__DIR__ . '/data/declare-position-valid.php'], []); + } + + public function testTicks(): void + { + $this->analyse([__DIR__ . '/data/declare-ticks.php'], []); + } + + public function testMulti(): void + { + $this->analyse([__DIR__ . '/data/declare-multi.php'], []); + } + + public function testShebang(): void + { + $this->analyse([__DIR__ . '/data/declare-shebang.php'], []); + $this->analyse([__DIR__ . '/data/declare-shebang2.php'], []); + $this->analyse([__DIR__ . '/data/declare-shebang3.php'], []); + } + + public function testHtmlBeforeDecalre(): void + { + $this->analyse([__DIR__ . '/data/declare-inline-html.php'], [ + [ + 'Declare strict_types must be the very first statement.', + 2, + ], + ]); + } + + public function testNonsense(): void + { + $this->analyse([__DIR__ . '/data/declare-strict-nonsense.php'], [ + [ + "Declare strict_types must have 0 or 1 as its value, 'foo' given.", + 1, + ], + ]); + } + + public function testNonsenseBool(): void + { + $this->analyse([__DIR__ . '/data/declare-strict-nonsense-bool.php'], [ + [ + 'Declare strict_types must have 0 or 1 as its value, \true given.', + 1, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Keywords/data/declare-inline-html.php b/tests/PHPStan/Rules/Keywords/data/declare-inline-html.php new file mode 100644 index 0000000000..9c270f6a18 --- /dev/null +++ b/tests/PHPStan/Rules/Keywords/data/declare-inline-html.php @@ -0,0 +1,2 @@ +some html + @@ -26,7 +27,7 @@ public function testRule(): void 15, ], [ - 'Non-abstract class AbstractMethod\Baz contains abstract method doBar().', + 'Interface AbstractMethod\Baz contains abstract method doBar().', 22, ], ]); @@ -60,4 +61,32 @@ public function testBug4214(): void $this->analyse([__DIR__ . '/data/bug-4214.php'], []); } + public function testNonAbstractMethodWithNoBody(): void + { + $this->analyse([__DIR__ . '/data/bug-4244.php'], [ + [ + 'Non-abstract method HelloWorld::sayHello() must contain a body.', + 5, + ], + ]); + } + + public function testEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/method-in-enum-without-body.php'], [ + [ + 'Non-abstract method MethodInEnumWithoutBody\Foo::doFoo() must contain a body.', + 8, + ], + [ + 'Enum MethodInEnumWithoutBody\Foo contains abstract method doBar().', + 10, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/AbstractPrivateMethodRuleTest.php b/tests/PHPStan/Rules/Methods/AbstractPrivateMethodRuleTest.php new file mode 100644 index 0000000000..8154e0b77d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/AbstractPrivateMethodRuleTest.php @@ -0,0 +1,31 @@ + */ +class AbstractPrivateMethodRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new AbstractPrivateMethodRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/abstract-private-method.php'], [ + [ + 'Private method PrivateAbstractMethod\HelloWorld::sayPrivate() cannot be abstract.', + 12, + ], + [ + 'Private method PrivateAbstractMethod\fooInterface::sayPrivate() cannot be abstract.', + 24, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleNoBleedingEdgeTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleNoBleedingEdgeTest.php index 204e848600..71678d99ff 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleNoBleedingEdgeTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleNoBleedingEdgeTest.php @@ -23,7 +23,7 @@ class CallMethodsRuleNoBleedingEdgeTest extends RuleTestCase protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, $this->checkExplicitMixed, false); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, $this->checkExplicitMixed, false, true, false); return new CallMethodsRule( new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion(PHP_VERSION_ID), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, false), diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index c40b61e2a4..9196f847c4 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -33,13 +33,42 @@ class CallMethodsRuleTest extends RuleTestCase protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, $this->checkNullables, $this->checkThisOnly, $this->checkUnionTypes, $this->checkExplicitMixed, $this->checkImplicitMixed); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, $this->checkNullables, $this->checkThisOnly, $this->checkUnionTypes, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false); return new CallMethodsRule( new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion($this->phpVersion), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true), ); } + public function testIsCallablePhp7(): void + { + if (PHP_VERSION_ID >= 80000) { + $this->markTestSkipped('Test requires PHP 7.0'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([ __DIR__ . '/data/call-methods-is-callable.php'], []); + } + + public function testIsCallablePhp8(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([ __DIR__ . '/data/call-methods-is-callable.php'], [ + [ + 'Parameter #1 $str of method TestMethodsIsCallable\CheckIsCallable::test() expects callable(): mixed, \'Test…\' given.', + 10, + ], + ]); + } + public function testCallMethods(): void { $this->checkThisOnly = false; @@ -471,12 +500,17 @@ public function testCallMethods(): void 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', ], [ - 'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): array{\'foo\'}|null given.', + 'Parameter #1 $other of method Test\CollectionWithStaticParam::add() expects static(Test\AppleCollection), Test\AppleCollection given.', + 1512, + ], + [ + 'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): (array{\'foo\'}|null) given.', 1533, ], [ 'Parameter #1 $members of method Test\\ParameterTypeCheckVerbosity::doBar() expects array, array given.', 1589, + "Array does not have offset 'id'.", ], [ 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects numeric-string, 123 given.', @@ -790,12 +824,17 @@ public function testCallMethodsOnThisOnly(): void 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', ], [ - 'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): array{\'foo\'}|null given.', + 'Parameter #1 $other of method Test\CollectionWithStaticParam::add() expects static(Test\AppleCollection), Test\AppleCollection given.', + 1512, + ], + [ + 'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): (array{\'foo\'}|null) given.', 1533, ], [ 'Parameter #1 $members of method Test\\ParameterTypeCheckVerbosity::doBar() expects array, array given.', 1589, + "Array does not have offset 'id'.", ], [ 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects numeric-string, 123 given.', @@ -929,6 +968,10 @@ public function testClosureBind(): void 'Call to an undefined method CallClosureBind\Foo::nonexistentMethod().', 44, ], + [ + 'Parameter #2 $newScope of method Closure::bindTo() expects \'static\'|class-string|object|null, \'CallClosureBind\\\Bar3\' given.', + 74, + ], ]); } @@ -1516,6 +1559,10 @@ public function dataExplicitMixed(): array 'Cannot call method foo() on mixed.', 17, ], + [ + 'Cannot call method foo() on T of mixed.', + 26, + ], [ 'Parameter #1 $i of method CheckExplicitMixedMethodCall\Bar::doBar() expects int, mixed given.', 43, @@ -1527,6 +1574,7 @@ public function dataExplicitMixed(): array [ 'Parameter #1 $cb of method CheckExplicitMixedMethodCall\CallableMixed::doFoo() expects callable(mixed): void, Closure(int): void given.', 133, + 'Type int of parameter #1 $i of passed callable needs to be same or wider than parameter type mixed of accepting callable.', ], [ 'Parameter #1 $cb of method CheckExplicitMixedMethodCall\CallableMixed::doBar2() expects callable(): int, Closure(): mixed given.', @@ -1543,7 +1591,7 @@ public function dataExplicitMixed(): array /** * @dataProvider dataExplicitMixed - * @param mixed[] $errors + * @param list $errors */ public function testExplicitMixed(bool $checkExplicitMixed, array $errors): void { @@ -1568,10 +1616,6 @@ public function dataImplicitMixed(): array 'Parameter #1 $i of method CheckImplicitMixedMethodCall\Bar::doBar() expects int, mixed given.', 42, ], - [ - 'Parameter #1 $i of method CheckImplicitMixedMethodCall\Bar::doBar() expects int, T given.', - 65, - ], [ 'Parameter #1 $cb of method CheckImplicitMixedMethodCall\CallableMixed::doBar2() expects callable(): int, Closure(): mixed given.', 139, @@ -1587,7 +1631,7 @@ public function dataImplicitMixed(): array /** * @dataProvider dataImplicitMixed - * @param mixed[] $errors + * @param list $errors */ public function testImplicitMixed(bool $checkImplicitMixed, array $errors): void { @@ -1739,6 +1783,14 @@ public function testNullSafe(): void 'Parameter #1 $passedByRef of method NullsafeMethodCall\Foo::doBaz() is passed by reference, so it expects variables only.', 27, ], + [ + 'Cannot call method foo() on null.', + 33, + ], + [ + 'Cannot call method foo() on null.', + 34, + ], ]); } @@ -2117,6 +2169,7 @@ public function testGenericObjectLowerBound(): void [ 'Parameter #1 $c of method GenericObjectLowerBound\Foo::doFoo() expects GenericObjectLowerBound\Collection, GenericObjectLowerBound\Collection given.', 48, + 'Template type T on class GenericObjectLowerBound\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', ], ]); } @@ -2148,17 +2201,30 @@ public function testBug5372(): void $this->checkNullables = true; $this->checkUnionTypes = true; $this->analyse([__DIR__ . '/data/bug-5372.php'], [ + [ + 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', + 64, + 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', + ], [ 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', 68, + 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', ], [ 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', 72, + 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', + ], + [ + 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', + 81, + 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', ], [ 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', 85, + 'Template type T on class Bug5372\Collection is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', ], ]); } @@ -2194,7 +2260,7 @@ public function testLiteralString(): void 60, ], [ - 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, array given.', + 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, array given.', 65, ], [ @@ -2512,6 +2578,38 @@ public function testGenericsInferCollectionLevel8(): void ]); } + public function testGenericVariance(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/generic-variance.php'], [ + [ + 'Parameter #1 $param of method GenericVarianceCall\Foo::invariant() expects GenericVarianceCall\Invariant, GenericVarianceCall\Invariant given.', + 45, + ], + [ + 'Parameter #1 $param of method GenericVarianceCall\Foo::invariant() expects GenericVarianceCall\Invariant, GenericVarianceCall\Invariant given.', + 53, + 'Template type T on class GenericVarianceCall\Invariant is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', + ], + [ + 'Parameter #1 $param of method GenericVarianceCall\Foo::covariant() expects GenericVarianceCall\Covariant, GenericVarianceCall\Covariant given.', + 60, + ], + [ + 'Parameter #1 $param of method GenericVarianceCall\Foo::contravariant() expects GenericVarianceCall\Contravariant, GenericVarianceCall\Contravariant given.', + 83, + ], + [ + 'Parameter #1 $param of method GenericVarianceCall\Foo::invariantArray() expects array{GenericVarianceCall\Invariant}, array{GenericVarianceCall\Invariant} given.', + 97, + 'Offset 0 (GenericVarianceCall\Invariant) does not accept type GenericVarianceCall\Invariant: Template type T on class GenericVarianceCall\Invariant is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', + ], + ]); + } + public function testBug6904(): void { if (PHP_VERSION_ID < 80100) { @@ -2558,6 +2656,10 @@ public function testUnresolvableParameter(): void 'Parameter #2 $v of method UnresolvableParameter\HelloWorld::foo() contains unresolvable type.', 19, ], + [ + 'Parameter #2 $v of method UnresolvableParameter\HelloWorld::foo() expects 1, 0 given.', + 21, + ], ]); } @@ -2657,4 +2759,545 @@ public function testBug8058b(): void ]); } + public function testArrayCastListTypes(): void + { + $this->checkThisOnly = false; + $this->checkNullables = false; + $this->checkUnionTypes = false; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/array-cast-list-types.php'], []); + } + + public function testBug5623(): void + { + $this->checkThisOnly = false; + $this->checkNullables = false; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-5623.php'], []); + } + + public function testImagickPixel(): void + { + $this->checkThisOnly = false; + $this->checkNullables = false; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/imagick-pixel.php'], []); + } + + public function testNewInstanceArgsIssue8679(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/reflection-class-issue-8679.php'], []); + } + + public function testNonEmptyArray(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + + $this->analyse([__DIR__ . '/data/non-empty-array.php'], [ + [ + 'Parameter #1 $nonEmpty of method AcceptNonEmptyArray\Foo::requireNonEmpty() expects non-empty-array, array given.', + 15, + 'array might be empty.', + ], + [ + 'Parameter #1 $nonEmpty of method AcceptNonEmptyArray\Foo::requireNonEmpty() expects non-empty-array, array{} given.', + 17, + 'array{} is empty.', + ], + ]); + } + + public function testBug8752(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/../../Analyser/data/bug-8752.php'], [ + [ + 'Cannot call method abc() on class-string.', + 18, + ], + ]); + } + + public function dataCallablesWithoutCheckNullables(): iterable + { + yield [false, false, []]; + yield [true, false, []]; + + $errors = [ + [ + 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBar() expects callable(float|null): (float|null), Closure(float): float given.', + 25, + 'Type float of parameter #1 $f of passed callable needs to be same or wider than parameter type float|null of accepting callable.', + ], + [ + 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBaz() expects Closure(float|null): (float|null), Closure(float): float given.', + 28, + 'Type float of parameter #1 $f of passed callable needs to be same or wider than parameter type float|null of accepting callable.', + ], + [ + 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBar2() expects callable(float|null): float, Closure(float|null): (float|null) given.', + 32, + ], + [ + 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBaz2() expects Closure(float|null): float, Closure(float|null): (float|null) given.', + 35, + ], + [ + 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBar2() expects callable(float|null): float, Closure(float): float given.', + 45, + 'Type float of parameter #1 $f of passed callable needs to be same or wider than parameter type float|null of accepting callable.', + ], + [ + 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBaz2() expects Closure(float|null): float, Closure(float): float given.', + 48, + 'Type float of parameter #1 $f of passed callable needs to be same or wider than parameter type float|null of accepting callable.', + ], + ]; + yield [false, true, $errors]; + yield [true, true, $errors]; + } + + /** + * @dataProvider dataCallablesWithoutCheckNullables + * @param list $expectedErrors + */ + public function testCallablesWithoutCheckNullables(bool $checkNullables, bool $checkUnionTypes, array $expectedErrors): void + { + $this->checkThisOnly = false; + $this->checkNullables = $checkNullables; + $this->checkUnionTypes = $checkUnionTypes; + $this->analyse([__DIR__ . '/data/callables-without-check-nullables.php'], $expectedErrors); + } + + public function testBug8713(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8713.php'], []); + } + + public function testCannotCallOnGenericClassString(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/../Comparison/data/impossible-method-exists-on-generic-class-string.php'], [ + [ + 'Cannot call method nonExistent() on class-string.', + 14, + ], + [ + 'Cannot call method staticAbc() on class-string.', + 20, + ], + [ + 'Cannot call method nonStaticAbc() on class-string.', + 25, + ], + [ + 'Cannot call method nonExistent() on class-string.', + 35, + ], + [ + 'Cannot call method staticAbc() on class-string.', + 41, + ], + [ + 'Cannot call method nonStaticAbc() on class-string.', + 46, + ], + ]); + } + + public function testBug8888(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8888.php'], []); + } + + public function testBug9542(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-9542.php'], []); + } + + public function testTrickyCallables(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/tricky-callables.php'], [ + [ + 'Parameter #1 $cb of method TrickyCallables\Foo::doBar() expects callable(string|null): void, callable(string): void given.', + 13, + 'Type string of parameter #1 of passed callable needs to be same or wider than parameter type string|null of accepting callable.', + ], + [ + 'Parameter #1 $cb of method TrickyCallables\Bar::doBar() expects callable(string=): void, callable(string): void given.', + 34, + 'Parameter #1 of passed callable is required but the parameter of accepting callable is optional. It might be called without it.', + ], + [ + 'Parameter #1 $cb of method TrickyCallables\Baz::doBar() expects callable(): void, callable(string): void given.', + 55, + 'Parameter #1 of passed callable is required but accepting callable does not have that parameter. It will be called without it.', + ], + [ + 'Parameter #1 $filter of method TrickyCallables\TwoErrorsAtOnce::run() expects callable(int|string=): bool, Closure(int): true given.', + 83, + '• Parameter #1 $key of passed callable is required but the parameter of accepting callable is optional. It might be called without it. +• Type int of parameter #1 $key of passed callable needs to be same or wider than parameter type int|string of accepting callable.', + ], + ]); + } + + public function testObjectShapes(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/object-shapes.php'], [ + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, stdClass given.', + 13, + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, Exception given.', + 14, + 'Exception might not have property $foo.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, object{foo: string, bar: int} given.', + 36, + 'Property ($foo) type int does not accept type string.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, object{foo?: int, bar: string} given.', + 37, + 'object{foo?: int, bar: string} might not have property $foo.', + ], + [ + 'Parameter #1 $std of method ObjectShapesAcceptance\Foo::requireStdClass() expects stdClass, object{foo: string, bar: int} given.', + 40, + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, object{foo: string, bar: int}&stdClass given.', + 43, + 'Property ($foo) type int does not accept type string.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, object given.', + 54, + '• object might not have property $foo. +• object might not have property $bar.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Foo::doBar() expects object{foo: int, bar: string}, stdClass given.', + 55, + ], + [ + 'Parameter #1 $bar of method ObjectShapesAcceptance\Bar::requireBar() expects ObjectShapesAcceptance\Bar, object{a: int} given.', + 71, + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Bar::doBar() expects object{a: string}, ObjectShapesAcceptance\Bar given.', + 77, + 'Property ($a) type string does not accept type int.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Baz::doBar() expects object{a: int}, $this(ObjectShapesAcceptance\Baz) given.', + 105, + 'Property ObjectShapesAcceptance\Baz::$a is not public.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Baz::doBaz() expects object{b: int}, $this(ObjectShapesAcceptance\Baz) given.', + 106, + 'Property ObjectShapesAcceptance\Baz::$b is static.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Baz::doLorem() expects object{c: int}, $this(ObjectShapesAcceptance\Baz) given.', + 107, + 'Property ObjectShapesAcceptance\Baz::$c is not readable.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\Baz::doIpsum() expects object{d: array{foo: string}}, $this(ObjectShapesAcceptance\Baz) given.', + 108, + 'Property ($d) type array{foo: string} does not accept type array{foo: int}: Offset \'foo\' (string) does not accept type int.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\OptionalProperty::doBar() expects object{foo?: int}, object{foo?: string} given.', + 156, + 'Property ($foo) type int does not accept type string.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\OptionalProperty::doBaz() expects object{foo: int}, object{foo?: string} given.', + 157, + 'Property ($foo) type int does not accept type string.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\TestAcceptance::doFoo() expects object{foo: int}, Traversable given.', + 209, + 'Traversable might not have property $foo.', + ], + [ + 'Parameter #1 $o of method ObjectShapesAcceptance\TestAcceptance::doFoo() expects object{foo: int}, ObjectShapesAcceptance\FinalClass given.', + 210, + PHP_VERSION_ID < 80200 ? 'ObjectShapesAcceptance\FinalClass might not have property $foo.' : 'ObjectShapesAcceptance\FinalClass does not have property $foo.', + ], + ]); + } + + public function testBug9951(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-9951.php'], [ + [ + 'Parameter #1 $field of method Bug9951\Cl::addCondition() expects array|Bug9951\AbstractScope|Bug9951\Expressionable|string, mixed given.', + 26, + ], + [ + 'Parameter #1 $field of method Bug9951\Cl::addCondition() expects array|Bug9951\AbstractScope|Bug9951\Expressionable|string, object|string|null given.', + 31, + ], + ]); + } + + public function testTypedClassConstants(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/return-type-class-constant.php'], []); + } + + public function testNamedParametersForMultiVariantFunctions(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/call-methods-named-params-multivariant.php'], [ + [ + 'Unknown parameter $options in call to method XSLTProcessor::setParameter().', + 10, + ], + [ + 'Missing parameter $name (array) in call to method XSLTProcessor::setParameter().', + 10, + ], + [ + 'Unknown parameter $colno in call to method PDO::query().', + 15, + ], + [ + 'Unknown parameter $className in call to method PDO::query().', + 17, + ], + [ + 'Unknown parameter $constructorArgs in call to method PDO::query().', + 17, + ], + [ + 'Unknown parameter $className in call to method PDOStatement::setFetchMode().', + 22, + ], + ]); + } + + public function testBug5518(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-5518.php'], []); + } + + public function testRequireExtends(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/../Properties/data/require-extends.php'], [ + [ + 'Call to an undefined method RequireExtends\MyInterface::doesNotExist().', + 43, + ], + ]); + } + + public function testRequireImplements(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/../Properties/data/require-implements.php'], [ + [ + 'Call to an undefined method RequireImplements\MyBaseClass::doesNotExist().', + 44, + ], + ]); + } + + public function testBug6371(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-6371.php'], [ + [ + 'Parameter #1 $t of method Bug6371\HelloWorld::compare() expects int, true given.', + 24, + ], + [ + 'Parameter #2 $k of method Bug6371\HelloWorld::compare() expects string, false given.', + 24, + ], + ]); + } + + public function testBugTemplateMixedUnionIntersect(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-template-mixed-union-intersect.php'], [ + [ + 'Call to an undefined method BugTemplateMixedUnionIntersect\FooInterface&T of mixed::bar().', + 17, + ], + [ + 'Call to an undefined method BugTemplateMixedUnionIntersect\FooInterface::bar().', + 20, + ], + [ + 'Cannot call method foo() on BugTemplateMixedUnionIntersect\FooInterface|T of mixed.', + 23, + ], + [ + 'Cannot call method foo() on mixed.', + 25, + ], + ]); + } + + public function testBug9009(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-9009.php'], []); + } + + public function testBuSplObjectStorageRemove(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-SplObjectStorage-remove.php'], [ + // removeNoIntersect should be reported, but unfortunately it cannot be expressed by the type system. + ]); + } + + public function testClosureBindToParamClosureThis(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/closure-bind-to-param-closure-this.php'], [ + [ + 'Parameter #1 $newThis of method Closure::bindTo() expects stdClass, ClosureBindToParamClosureThis\Foo given.', + 23, + ], + ]); + } + + public function testPureCallable(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/pure-callable-accepts.php'], [ + [ + 'Parameter #1 $cb of method PureCallableMethodAccepts\Foo::acceptsPureCallable() expects pure-callable(): mixed, callable(): mixed given.', + 33, + ], + [ + 'Parameter #1 $i of method PureCallableMethodAccepts\Foo::acceptsInt() expects int, callable given.', + 35, + ], + [ + 'Parameter #1 $i of method PureCallableMethodAccepts\Foo::acceptsInt() expects int, callable given.', + 36, + ], + [ + 'Parameter #1 $cb of method PureCallableMethodAccepts\Foo::acceptsPureCallable() expects pure-callable(): mixed, Closure(): 1 given.', + 41, + ], + [ + 'Parameter #1 $cb of method PureCallableMethodAccepts\Foo::acceptsPureClosure() expects pure-Closure, Closure(): 1 given.', + 61, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index 041ab7e92d..f60d015e6c 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -11,6 +13,9 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use function array_merge; +use function usort; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -22,13 +27,35 @@ class CallStaticMethodsRuleTest extends RuleTestCase private bool $checkExplicitMixed = false; + private bool $checkImplicitMixed = false; + protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, true, $this->checkExplicitMixed, false); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false); return new CallStaticMethodsRule( - new StaticMethodCallCheck($reflectionProvider, $ruleLevelHelper, new ClassCaseSensitivityCheck($reflectionProvider, true), true, true), - new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true, true), + new StaticMethodCallCheck( + $reflectionProvider, + $ruleLevelHelper, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, + true, + ), + new FunctionCallParametersCheck( + $ruleLevelHelper, + new NullsafeCheck(), + new PhpVersion(80000), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), + true, + true, + true, + true, + true, + ), ); } @@ -223,7 +250,7 @@ public function testCallStaticMethods(): void 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ - 'Call to an undefined static method static(CallStaticMethods\CallWithStatic)::nonexistent().', + 'Call to an undefined static method CallStaticMethods\CallWithStatic::nonexistent().', 344, ], ]); @@ -423,6 +450,10 @@ public function testBug4550(): void public function testBug1971(): void { + if (PHP_VERSION_ID >= 80000) { + $this->markTestSkipped('Test requires PHP 7.x'); + } + $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/bug-1971.php'], [ [ @@ -432,6 +463,29 @@ public function testBug1971(): void ]); } + public function testBug1971Php8(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-1971.php'], [ + [ + 'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{\'Bug1971\\\HelloWorld\', \'sayHello\'} given.', + 14, + ], + [ + 'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{class-string, \'sayHello\'} given.', + 15, + ], + [ + 'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{class-string, \'sayHello2\'} given.', + 16, + ], + ]); + } + public function testBug5259(): void { $this->checkThisOnly = false; @@ -515,6 +569,7 @@ public function testTemplateTypeInOneBranchOfConditional(): void [ 'Parameter #1 $params of static method TemplateTypeInOneBranchOfConditional\DriverManager::getConnection() expects array{wrapperClass?: class-string}, array{wrapperClass: \'stdClass\'} given.', 27, + "Offset 'wrapperClass' (class-string) does not accept type string.", ], [ 'Unable to resolve the template type T in call to method static method TemplateTypeInOneBranchOfConditional\DriverManager::getConnection()', @@ -531,4 +586,259 @@ public function testBug7489(): void $this->analyse([__DIR__ . '/data/bug-7489.php'], []); } + public function testHasMethodStaticCall(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/static-has-method.php'], [ + [ + 'Call to an undefined static method StaticHasMethodCall\rex_var::doesNotExist().', + 38, + ], + [ + 'Call to an undefined static method StaticHasMethodCall\rex_var::doesNotExist().', + 48, + ], + ]); + } + + public function testBug1267(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-1267.php'], []); + } + + public function testBug6147(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-6147.php'], []); + } + + public function testBug5781(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-5781.php'], [ + [ + 'Parameter #1 $param of static method Bug5781\Foo::bar() expects array{a: bool, b: bool, c: bool, d: bool, e: bool, f: bool, g: bool, h: bool, ...}, array{} given.', + 17, + "Array does not have offset 'a'.", + ], + ]); + } + + public function testBug8296(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-8296.php'], [ + [ + 'Parameter #1 $objects of static method Bug8296\VerifyLoginTask::continueDump() expects array, array given.', + 12, + ], + [ + 'Parameter #1 $string of static method Bug8296\VerifyLoginTask::stringByRef() expects string, int given.', + 15, + ], + ]); + } + + public function testRequireExtends(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = false; + + $this->analyse([__DIR__ . '/../Properties/data/require-extends.php'], [ + [ + 'Call to an undefined static method RequireExtends\MyInterface::doesNotExistStatic().', + 44, + ], + ]); + } + + public function testRequireImplements(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = false; + + $this->analyse([__DIR__ . '/../Properties/data/require-implements.php'], [ + [ + 'Call to an undefined static method RequireImplements\MyBaseClass::doesNotExistStatic().', + 45, + ], + ]); + } + + public function dataMixed(): array + { + $explicitOnlyErrors = [ + [ + 'Cannot call static method foo() on mixed.', + 17, + ], + [ + 'Cannot call static method foo() on T of mixed.', + 26, + ], + [ + 'Parameter #1 $i of static method CallStaticMethodMixed\Bar::doBar() expects int, mixed given.', + 43, + ], + [ + 'Only iterables can be unpacked, mixed given in argument #1.', + 52, + ], + [ + 'Parameter #1 $i of static method CallStaticMethodMixed\Bar::doBar() expects int, T given.', + 81, + ], + [ + 'Only iterables can be unpacked, T of mixed given in argument #1.', + 84, + ], + [ + 'Parameter #1 $cb of static method CallStaticMethodMixed\CallableMixed::callAcceptsExplicitMixed() expects callable(mixed): void, Closure(int): void given.', + 134, + 'Type int of parameter #1 $i of passed callable needs to be same or wider than parameter type mixed of accepting callable.', + ], + [ + 'Parameter #1 $cb of static method CallStaticMethodMixed\CallableMixed::callReturnsInt() expects callable(): int, Closure(): mixed given.', + 161, + ], + ]; + $implicitOnlyErrors = [ + [ + 'Cannot call static method foo() on mixed.', + 16, + ], + [ + 'Parameter #1 $i of static method CallStaticMethodMixed\Bar::doBar() expects int, mixed given.', + 42, + ], + [ + 'Only iterables can be unpacked, mixed given in argument #1.', + 51, + ], + [ + 'Parameter #1 $cb of static method CallStaticMethodMixed\CallableMixed::callReturnsInt() expects callable(): int, Closure(): mixed given.', + 168, + ], + ]; + $combinedErrors = array_merge($explicitOnlyErrors, $implicitOnlyErrors); + usort($combinedErrors, static fn (array $a, array $b): int => $a[1] <=> $b[1]); + + return [ + [ + true, + false, + $explicitOnlyErrors, + ], + [ + false, + true, + $implicitOnlyErrors, + ], + [ + true, + true, + $combinedErrors, + ], + [ + false, + false, + [], + ], + ]; + } + + /** + * @dataProvider dataMixed + * @param list $errors + */ + public function testMixed(bool $checkExplicitMixed, bool $checkImplicitMixed, array $errors): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = $checkExplicitMixed; + $this->checkImplicitMixed = $checkImplicitMixed; + $this->analyse([__DIR__ . '/data/call-static-method-mixed.php'], $errors); + } + + public function testBugWrongMethodNameWithTemplateMixed(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); + } + + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-wrong-method-name-with-template-mixed.php'], [ + [ + 'Call to an undefined static method T of mixed&UnitEnum::from().', + 14, + ], + [ + 'Call to an undefined static method T of mixed&UnitEnum::from().', + 25, + ], + [ + 'Call to an undefined static method T of object&UnitEnum::from().', + 36, + ], + [ + 'Call to an undefined static method UnitEnum::from().', + 43, + ], + ]); + } + + public function testConditionalParam(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/data/conditional-param.php'], [ + [ + 'Parameter #1 $demoArg of static method ConditionalParam\HelloWorld::replaceCallback() expects string, true given.', + 16, + ], + [ + 'Parameter #1 $demoArg of static method ConditionalParam\HelloWorld::replaceCallback() expects bool, string given.', + 20, + ], + [ + // wrong + 'Parameter #1 $demoArg of static method ConditionalParam\HelloWorld::replaceCallback() expects string, true given.', + 22, + ], + ]); + } + + public function testClosureBindParamClosureThis(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/closure-bind-param-closure-this.php'], [ + [ + 'Parameter #2 $newThis of static method Closure::bind() expects stdClass, ClosureBindParamClosureThis\Foo given.', + 25, + ], + ]); + } + + public function testClosureBind(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/closure-bind.php'], [ + [ + 'Parameter #3 $newScope of static method Closure::bind() expects \'static\'|class-string|object|null, \'CallClosureBind\\\Bar3\' given.', + 68, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRuleTest.php index 917cc77f8b..29e99526e2 100644 --- a/tests/PHPStan/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRuleTest.php @@ -13,7 +13,7 @@ class CallToConstructorStatementWithoutSideEffectsRuleTest extends RuleTestCase protected function getRule(): Rule { - return new CallToConstructorStatementWithoutSideEffectsRule($this->createReflectionProvider()); + return new CallToConstructorStatementWithoutSideEffectsRule($this->createReflectionProvider(), true); } public function testRule(): void @@ -23,6 +23,14 @@ public function testRule(): void 'Call to Exception::__construct() on a separate line has no effect.', 6, ], + [ + 'Call to new PDOStatement() on a separate line has no effect.', + 11, + ], + [ + 'Call to new stdClass() on a separate line has no effect.', + 12, + ], [ 'Call to ConstructorStatementNoSideEffects\ConstructorWithPure::__construct() on a separate line has no effect.', 57, @@ -31,6 +39,10 @@ public function testRule(): void 'Call to ConstructorStatementNoSideEffects\ConstructorWithPureAndThrowsVoid::__construct() on a separate line has no effect.', 58, ], + [ + 'Call to new ConstructorStatementNoSideEffects\NoConstructor() on a separate line has no effect.', + 68, + ], ]); } diff --git a/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php index 46f6e0170b..f1d3d5902d 100644 --- a/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php @@ -14,7 +14,7 @@ class CallToMethodStatementWithoutSideEffectsRuleTest extends RuleTestCase protected function getRule(): Rule { - return new CallToMethodStatementWithoutSideEffectsRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false)); + return new CallToMethodStatementWithoutSideEffectsRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false)); } public function testRule(): void @@ -63,15 +63,23 @@ public function testPhpDoc(): void $this->analyse([__DIR__ . '/data/method-call-statement-no-side-effects-phpdoc.php'], [ [ 'Call to method MethodCallStatementNoSideEffects\Bzz::pure1() on a separate line has no effect.', - 39, + 55, ], [ 'Call to method MethodCallStatementNoSideEffects\Bzz::pure2() on a separate line has no effect.', - 40, + 56, ], [ 'Call to method MethodCallStatementNoSideEffects\Bzz::pure3() on a separate line has no effect.', - 41, + 57, + ], + [ + 'Call to method MethodCallStatementNoSideEffects\Bzz::pure4() on a separate line has no effect.', + 58, + ], + [ + 'Call to method MethodCallStatementNoSideEffects\Bzz::pure5() on a separate line has no effect.', + 59, ], ]); } diff --git a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php index 179e6fe1ea..03cbdaa49d 100644 --- a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php @@ -16,7 +16,7 @@ protected function getRule(): Rule { $broker = $this->createReflectionProvider(); return new CallToStaticMethodStatementWithoutSideEffectsRule( - new RuleLevelHelper($broker, true, false, true, false, false), + new RuleLevelHelper($broker, true, false, true, false, false, true, false), $broker, ); } @@ -44,19 +44,27 @@ public function testPhpDoc(): void $this->analyse([__DIR__ . '/data/static-method-call-statement-no-side-effects-phpdoc.php'], [ [ 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure1() on a separate line has no effect.', - 39, + 55, ], [ 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure2() on a separate line has no effect.', - 40, + 56, ], [ 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure3() on a separate line has no effect.', - 41, + 57, + ], + [ + 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure4() on a separate line has no effect.', + 58, + ], + [ + 'Call to static method StaticMethodCallStatementNoSideEffects\BzzStatic::pure5() on a separate line has no effect.', + 59, ], [ 'Call to static method StaticMethodCallStatementNoSideEffects\PureThrows::pureAndThrowsVoid() on a separate line has no effect.', - 67, + 85, ], ]); } diff --git a/tests/PHPStan/Rules/Methods/ConsistentConstructorRuleTest.php b/tests/PHPStan/Rules/Methods/ConsistentConstructorRuleTest.php index 78da65d816..4ea2484ef5 100644 --- a/tests/PHPStan/Rules/Methods/ConsistentConstructorRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ConsistentConstructorRuleTest.php @@ -32,7 +32,7 @@ public function testRule(): void 58, ], [ - 'Method ConsistentConstructor\FakeConnection::__construct() overrides method ConsistentConstructor\TestConnection::__construct() but misses parameter #1 $i.', + 'Method ConsistentConstructor\FakeConnection::__construct() overrides method ConsistentConstructor\Connection::__construct() but misses parameter #1 $i.', 78, ], ]); diff --git a/tests/PHPStan/Rules/Methods/ConstructorReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ConstructorReturnTypeRuleTest.php new file mode 100644 index 0000000000..335972e370 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/ConstructorReturnTypeRuleTest.php @@ -0,0 +1,37 @@ + + */ +class ConstructorReturnTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ConstructorReturnTypeRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/constructor-return-type.php'], [ + [ + 'Constructor of class ConstructorReturnType\Bar has a return type.', + 17, + ], + [ + 'Constructor of class ConstructorReturnType\UsesFooTrait has a return type.', + 26, + ], + [ + 'Original constructor of trait ConstructorReturnType\BarTrait has a return type.', + 35, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php index 4667aba5ec..a0cf90c942 100644 --- a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionDefinitionCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; @@ -20,8 +22,20 @@ class ExistingClassesInTypehintsRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingClassesInTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker, true), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersionId), true, false)); + $reflectionProvider = $this->createReflectionProvider(); + return new ExistingClassesInTypehintsRule( + new FunctionDefinitionCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new UnresolvableTypeHelper(), + new PhpVersion($this->phpVersionId), + true, + false, + ), + ); } public function testExistingClassInTypehint(): void @@ -183,7 +197,7 @@ public function dataNativeUnionTypes(): array /** * @dataProvider dataNativeUnionTypes - * @param mixed[] $errors + * @param list $errors */ public function testNativeUnionTypes(int $phpVersionId, array $errors): void { @@ -196,7 +210,20 @@ public function dataRequiredParameterAfterOptional(): array return [ [ 70400, - [], + [ + [ + "Method RequiredAfterOptional\Foo::doAmet() uses native union types but they're supported only on PHP 8.0 and later.", + 33, + ], + [ + "Method RequiredAfterOptional\Foo::doConsectetur() uses native union types but they're supported only on PHP 8.0 and later.", + 37, + ], + [ + "Method RequiredAfterOptional\Foo::doSed() uses native union types but they're supported only on PHP 8.0 and later.", + 49, + ], + ], ], [ 80000, @@ -213,6 +240,116 @@ public function dataRequiredParameterAfterOptional(): array 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', 21, ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 33, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 41, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 49, + ], + ], + ], + [ + 80100, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 8, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 29, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 33, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 41, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 49, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 49, + ], + ], + ], + [ + 80300, + [ + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 8, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 17, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 21, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 25, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $bar follows optional parameter $foo.', + 29, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 33, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 37, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $bar follows optional parameter $foo.', + 41, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 45, + ], + [ + 'Deprecated in PHP 8.3: Required parameter $bar follows optional parameter $foo.', + 49, + ], + [ + 'Deprecated in PHP 8.1: Required parameter $qux follows optional parameter $baz.', + 49, + ], + [ + 'Deprecated in PHP 8.0: Required parameter $quuz follows optional parameter $quux.', + 49, + ], ], ], ]; @@ -220,7 +357,7 @@ public function dataRequiredParameterAfterOptional(): array /** * @dataProvider dataRequiredParameterAfterOptional - * @param mixed[] $errors + * @param list $errors */ public function testRequiredParameterAfterOptional(int $phpVersionId, array $errors): void { @@ -246,19 +383,19 @@ public function dataIntersectionTypes(): array 80100, [ [ - 'Parameter $a of method MethodIntersectionTypes\Foo::doBar() has unresolvable native type.', + 'Parameter $a of method MethodIntersectionTypes\FooClass::doBar() has unresolvable native type.', 33, ], [ - 'Method MethodIntersectionTypes\Foo::doBar() has unresolvable native return type.', + 'Method MethodIntersectionTypes\FooClass::doBar() has unresolvable native return type.', 33, ], [ - 'Parameter $a of method MethodIntersectionTypes\Foo::doBaz() has unresolvable native type.', + 'Parameter $a of method MethodIntersectionTypes\FooClass::doBaz() has unresolvable native type.', 38, ], [ - 'Method MethodIntersectionTypes\Foo::doBaz() has unresolvable native return type.', + 'Method MethodIntersectionTypes\FooClass::doBaz() has unresolvable native return type.', 38, ], ], @@ -268,7 +405,7 @@ public function dataIntersectionTypes(): array /** * @dataProvider dataIntersectionTypes - * @param mixed[] $errors + * @param list $errors */ public function testIntersectionTypes(int $phpVersion, array $errors): void { @@ -300,7 +437,7 @@ public function dataTrueTypes(): array /** * @dataProvider dataTrueTypes - * @param mixed[] $errors + * @param list $errors */ public function testTrueTypehint(int $phpVersion, array $errors): void { @@ -319,4 +456,19 @@ public function testConditionalReturnType(): void ]); } + public function testBug7519(): void + { + $this->analyse([__DIR__ . '/data/bug-7519.php'], []); + } + + public function testTemplateInParamOut(): void + { + $this->analyse([__DIR__ . '/data/param-out.php'], [ + [ + 'Template type T of method ParamOutTemplate\FooBar::uselessLocalTemplate() is not referenced in a parameter.', + 22, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleTest.php b/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleTest.php index 1d21a35212..64a4b89c0f 100644 --- a/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleTest.php @@ -40,7 +40,7 @@ public function dataRule(): array /** * @dataProvider dataRule - * @param mixed[] $errors + * @param list $errors */ public function testRule(int $phpVersion, array $errors): void { diff --git a/tests/PHPStan/Rules/Methods/IllegalConstructorStaticCallRuleTest.php b/tests/PHPStan/Rules/Methods/IllegalConstructorStaticCallRuleTest.php index 2654c0d658..065a5785bf 100644 --- a/tests/PHPStan/Rules/Methods/IllegalConstructorStaticCallRuleTest.php +++ b/tests/PHPStan/Rules/Methods/IllegalConstructorStaticCallRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -46,4 +47,13 @@ public function testMethods(): void ]); } + public function testBug9577(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9577.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php b/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php index 62c35c0573..e55eb6eede 100644 --- a/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php @@ -66,7 +66,16 @@ public function testDefaultValueForPromotedProperty(): void 'Default value of the parameter #2 $foo (string) of method DefaultValueForPromotedProperty\Foo::__construct() is incompatible with type int.', 10, ], + [ + 'Default value of the parameter #4 $intProp (null) of method DefaultValueForPromotedProperty\Foo::__construct() is incompatible with type int.', + 12, + ], ]); } + public function testBug10956(): void + { + $this->analyse([__DIR__ . '/data/bug-10956.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php index c1b24b3511..9ce75af6e6 100644 --- a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -28,7 +30,7 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), new NullsafeCheck(), new PhpVersion($this->phpVersion), new UnresolvableTypeHelper(), @@ -39,7 +41,11 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, ), ); } diff --git a/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php index 487dc631dd..b5bc1f8efb 100644 --- a/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php @@ -19,7 +19,7 @@ class MethodCallableRuleTest extends RuleTestCase protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, false, false); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false); return new MethodCallableRule( new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), diff --git a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php index 3bd4beadcd..93c6a67b9d 100644 --- a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Methods; use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\Php\PhpClassReflectionExtension; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -21,11 +22,17 @@ protected function getRule(): Rule { $phpVersion = new PhpVersion(PHP_VERSION_ID); + $phpClassReflectionExtension = self::getContainer()->getByType(PhpClassReflectionExtension::class); + return new OverridingMethodRule( $phpVersion, - new MethodSignatureRule($this->reportMaybes, $this->reportStatic), + new MethodSignatureRule($phpClassReflectionExtension, $this->reportMaybes, $this->reportStatic, true), + true, + new MethodParameterComparisonHelper($phpVersion, true), + $phpClassReflectionExtension, true, - new MethodParameterComparisonHelper($phpVersion), + true, + false, ); } @@ -316,7 +323,7 @@ public function testBug4707(): void $this->reportStatic = true; $this->analyse([__DIR__ . '/data/bug-4707.php'], [ [ - 'Return type (array) of method Bug4707\Block2::getChildren() should be compatible with return type (array>) of method Bug4707\ParentNodeInterface::getChildren()', + 'Return type (list) of method Bug4707\Block2::getChildren() should be compatible with return type (list>) of method Bug4707\ParentNodeInterface::getChildren()', 38, ], ]); @@ -328,7 +335,7 @@ public function testBug4707Covariant(): void $this->reportStatic = true; $this->analyse([__DIR__ . '/data/bug-4707-covariant.php'], [ [ - 'Return type (array) of method Bug4707Covariant\Block2::getChildren() should be covariant with return type (array>) of method Bug4707Covariant\ParentNodeInterface::getChildren()', + 'Return type (list) of method Bug4707Covariant\Block2::getChildren() should be covariant with return type (list>) of method Bug4707Covariant\ParentNodeInterface::getChildren()', 38, ], ]); @@ -384,7 +391,7 @@ public function testBug7652(): void $this->reportStatic = true; $this->analyse([__DIR__ . '/data/bug-7652.php'], [ [ - 'Return type mixed of method Bug7652\Options::offsetGet() is not covariant with tentative return type mixed of method ArrayAccess::offsetGet().', + 'Return type mixed of method Bug7652\Options::offsetGet() is not covariant with tentative return type mixed of method ArrayAccess,value-of>::offsetGet().', 23, 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.', ], @@ -402,4 +409,145 @@ public function testBug7103(): void $this->analyse([__DIR__ . '/data/bug-7103.php'], []); } + public function testListReturnTypeCovariance(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/list-return-type-covariance.php'], [ + [ + 'Return type (array) of method ListReturnTypeCovariance\ListChild::returnsList() should be covariant with return type (list) of method ListReturnTypeCovariance\ListParent::returnsList()', + 17, + ], + ]); + } + + public function testRuleError(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/rule-error-signature.php'], [ + [ + 'Return type (array) of method RuleErrorSignature\Baz::processNode() should be covariant with return type (list) of method PHPStan\Rules\Rule::processNode()', + 64, + 'Rules can no longer return plain strings. See: https://phpstan.org/blog/using-rule-error-builder', + ], + [ + 'Return type (array) of method RuleErrorSignature\Lorem::processNode() should be compatible with return type (list) of method PHPStan\Rules\Rule::processNode()', + 85, + 'Rules can no longer return plain strings. See: https://phpstan.org/blog/using-rule-error-builder', + ], + [ + 'Return type (array) of method RuleErrorSignature\Ipsum::processNode() should be covariant with return type (list) of method PHPStan\Rules\Rule::processNode()', + 106, + 'Errors are missing identifiers. See: https://phpstan.org/blog/using-rule-error-builder', + ], + [ + 'Return type (array) of method RuleErrorSignature\Dolor::processNode() should be covariant with return type (list) of method PHPStan\Rules\Rule::processNode()', + 127, + 'Return type must be a list. See: https://phpstan.org/blog/using-rule-error-builder', + ], + ]); + } + + public function testBug9905(): void + { + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-9905.php'], []); + } + + public function testTraits(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->reportMaybes = true; + $this->reportStatic = true; + + $this->analyse([__DIR__ . '/data/overriding-trait-methods-phpdoc.php'], [ + [ + 'Parameter #1 $i (non-empty-string) of method OverridingTraitMethodsPhpDoc\Bar::doBar() should be contravariant with parameter $i (string) of method OverridingTraitMethodsPhpDoc\Foo::doBar()', + 33, + ], + ]); + } + + public function testBug10166(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->reportMaybes = true; + $this->reportStatic = true; + + $this->analyse([__DIR__ . '/data/bug-10166.php'], [ + [ + 'Return type Bug10166\ReturnTypeClass2|null of method Bug10166\ReturnTypeClass2::createSelf() is not covariant with return type Bug10166\ReturnTypeClass2 of method Bug10166\ReturnTypeTrait::createSelf().', + 23, + ], + ]); + } + + public function testBug10184(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->reportMaybes = true; + $this->reportStatic = true; + + $this->analyse([__DIR__ . '/data/bug-10184.php'], []); + } + + public function testBug10208(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->reportMaybes = true; + $this->reportStatic = true; + + $this->analyse([__DIR__ . '/data/bug-10208.php'], []); + } + + public function testBug6462(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->reportMaybes = true; + $this->reportStatic = true; + + $this->analyse([__DIR__ . '/data/bug-6462.php'], []); + } + + public function testBug4396(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->reportMaybes = true; + $this->reportStatic = true; + + $this->analyse([__DIR__ . '/data/bug-4396.php'], []); + } + + public function testBug3580(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->reportMaybes = true; + $this->reportStatic = true; + + $this->analyse([__DIR__ . '/data/bug-3580.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/MethodVisibilityInInterfaceRuleTest.php b/tests/PHPStan/Rules/Methods/MethodVisibilityInInterfaceRuleTest.php new file mode 100644 index 0000000000..290e1c3364 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/MethodVisibilityInInterfaceRuleTest.php @@ -0,0 +1,31 @@ + */ +class MethodVisibilityInInterfaceRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MethodVisibilityInInterfaceRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/visibility-in-interace.php'], [ + [ + 'Method VisibilityInInterface\FooInterface::sayPrivate() cannot use non-public visibility in interface.', + 7, + ], + [ + 'Method VisibilityInInterface\FooInterface::sayProtected() cannot use non-public visibility in interface.', + 8, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/MissingMagicSerializationMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMagicSerializationMethodsRuleTest.php new file mode 100644 index 0000000000..989d8e0a52 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/MissingMagicSerializationMethodsRuleTest.php @@ -0,0 +1,41 @@ + + */ +class MissingMagicSerializationMethodsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MissingMagicSerializationMethodsRule(new PhpVersion(PHP_VERSION_ID)); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/missing-serialization.php'], [ + [ + 'Non-abstract class MissingMagicSerializationMethods\myObj implements the Serializable interface, but does not implement __serialize().', + 14, + 'See https://wiki.php.net/rfc/phase_out_serializable', + ], + [ + 'Non-abstract class MissingMagicSerializationMethods\myObj implements the Serializable interface, but does not implement __unserialize().', + 14, + 'See https://wiki.php.net/rfc/phase_out_serializable', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php index 432efad50e..2d4cf56a10 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php @@ -14,8 +14,7 @@ class MissingMethodParameterTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new MissingMethodParameterTypehintRule(new MissingTypehintCheck($broker, true, true, true, true, [])); + return new MissingMethodParameterTypehintRule(new MissingTypehintCheck(true, true, true, true, []), true); } public function testRule(): void @@ -70,6 +69,25 @@ public function testRule(): void 'Method MissingMethodParameterTypehint\CallableSignature::doFoo() has parameter $cb with no signature specified for callable.', 180, ], + [ + 'Method MissingMethodParameterTypehint\MissingParamOutType::oneArray() has @param-out PHPDoc tag for parameter $a with no value type specified in iterable type array.', + 207, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + [ + 'Method MissingMethodParameterTypehint\MissingParamOutType::generics() has @param-out PHPDoc tag for parameter $a with generic class ReflectionClass but does not specify its types: T', + 215, + 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + ], + [ + 'Method MissingMethodParameterTypehint\MissingParamClosureThisType::generics() has @param-closure-this PHPDoc tag for parameter $cb with generic class ReflectionClass but does not specify its types: T', + 226, + 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + ], + [ + 'Method MissingMethodParameterTypehint\MissingPureClosureSignatureType::doFoo() has parameter $cb with no signature specified for Closure.', + 238, + ], ]; $this->analyse([__DIR__ . '/data/missing-method-parameter-typehint.php'], $errors); diff --git a/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php index 39d38ac9d9..a4e64d8b6f 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php @@ -14,8 +14,7 @@ class MissingMethodReturnTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new MissingMethodReturnTypehintRule(new MissingTypehintCheck($broker, true, true, true, true, [])); + return new MissingMethodReturnTypehintRule(new MissingTypehintCheck(true, true, true, true, [])); } public function testRule(): void @@ -84,4 +83,19 @@ public function testBug5436(): void $this->analyse([__DIR__ . '/data/bug-5436.php'], []); } + public function testBug4758(): void + { + $this->analyse([__DIR__ . '/data/bug-4758.php'], []); + } + + public function testBug9571(): void + { + $this->analyse([__DIR__ . '/data/bug-9571.php'], []); + } + + public function testBug9571PhpDocs(): void + { + $this->analyse([__DIR__ . '/data/bug-9571-phpdocs.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php b/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php index 2734cc1d2a..73cd3c3f68 100644 --- a/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -26,4 +27,32 @@ public function testRule(): void ]); } + public function testNullsafeVsScalar(): void + { + $this->analyse([__DIR__ . '/../../Analyser/data/nullsafe-vs-scalar.php'], []); + } + + public function testBug8664(): void + { + $this->analyse([__DIR__ . '/../../Analyser/data/bug-8664.php'], []); + } + + public function testBug9293(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/../../Analyser/data/bug-9293.php'], []); + } + + public function testBug6922b(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-6922b.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php index b72e83269d..826586689a 100644 --- a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Methods; use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\Php\PhpClassReflectionExtension; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use function array_filter; @@ -17,15 +18,23 @@ class OverridingMethodRuleTest extends RuleTestCase private int $phpVersionId; + private bool $checkMissingOverrideMethodAttribute = false; + protected function getRule(): Rule { $phpVersion = new PhpVersion($this->phpVersionId); + $phpClassReflectionExtension = self::getContainer()->getByType(PhpClassReflectionExtension::class); + return new OverridingMethodRule( $phpVersion, - new MethodSignatureRule(true, true), + new MethodSignatureRule($phpClassReflectionExtension, true, true, true), false, - new MethodParameterComparisonHelper($phpVersion), + new MethodParameterComparisonHelper($phpVersion, true), + $phpClassReflectionExtension, + true, + true, + $this->checkMissingOverrideMethodAttribute, ); } @@ -84,7 +93,7 @@ public function testOverridingFinalMethod(int $phpVersion, string $contravariant 115, ], [ - 'Parameter #1 $size (int) of method OverridingFinalMethod\FixedArray::setSize() is not ' . $contravariantMessage . ' with parameter #1 $size (mixed) of method SplFixedArray::setSize().', + 'Parameter #1 $size (int) of method OverridingFinalMethod\FixedArray::setSize() is not ' . $contravariantMessage . ' with parameter #1 $size (mixed) of method SplFixedArray::setSize().', 125, ], [ @@ -120,7 +129,11 @@ public function testOverridingFinalMethod(int $phpVersion, string $contravariant 280, ], [ - 'Parameter #1 $index (int) of method OverridingFinalMethod\FixedArrayOffsetExists::offsetExists() is not ' . $contravariantMessage . ' with parameter #1 $offset (mixed) of method ArrayAccess::offsetExists().', + 'Method OverridingFinalMethod\ExtendsFinalWithAnnotation::doFoo() overrides @final method OverridingFinalMethod\FinalWithAnnotation::doFoo().', + 303, + ], + [ + 'Parameter #1 $index (int) of method OverridingFinalMethod\FixedArrayOffsetExists::offsetExists() is not ' . $contravariantMessage . ' with parameter #1 $index (mixed) of method SplFixedArray::offsetExists().', 313, ], ]; @@ -217,7 +230,7 @@ public function dataParameterContravariance(): array /** * @dataProvider dataParameterContravariance - * @param mixed[] $expectedErrors + * @param list $expectedErrors */ public function testParameterContravariance( string $file, @@ -275,7 +288,7 @@ public function dataReturnTypeCovariance(): array /** * @dataProvider dataReturnTypeCovariance - * @param mixed[] $expectedErrors + * @param list $expectedErrors */ public function testReturnTypeCovariance( int $phpVersion, @@ -420,7 +433,7 @@ public function dataLessOverridenParametersWithVariadic(): array /** * @dataProvider dataLessOverridenParametersWithVariadic - * @param mixed[] $errors + * @param list $errors */ public function testLessOverridenParametersWithVariadic(int $phpVersionId, array $errors): void { @@ -449,7 +462,7 @@ public function dataParameterTypeWidening(): array /** * @dataProvider dataParameterTypeWidening - * @param mixed[] $errors + * @param list $errors */ public function testParameterTypeWidening(int $phpVersionId, array $errors): void { @@ -472,37 +485,37 @@ public function dataTentativeReturnTypes(): array 80100, [ [ - 'Return type mixed of method TentativeReturnTypes\Foo::getIterator() is not covariant with tentative return type Traversable of method IteratorAggregate::getIterator().', + 'Return type mixed of method TentativeReturnTypes\Foo::getIterator() is not covariant with tentative return type Traversable of method IteratorAggregate::getIterator().', 8, 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.', ], [ - 'Return type string of method TentativeReturnTypes\Lorem::getIterator() is not covariant with tentative return type Traversable of method IteratorAggregate::getIterator().', + 'Return type string of method TentativeReturnTypes\Lorem::getIterator() is not covariant with tentative return type Traversable of method IteratorAggregate::getIterator().', 40, 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.', ], [ - 'Return type mixed of method TentativeReturnTypes\UntypedIterator::current() is not covariant with tentative return type mixed of method Iterator::current().', + 'Return type mixed of method TentativeReturnTypes\UntypedIterator::current() is not covariant with tentative return type mixed of method Iterator::current().', 75, 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.', ], [ - 'Return type mixed of method TentativeReturnTypes\UntypedIterator::next() is not covariant with tentative return type void of method Iterator::next().', + 'Return type mixed of method TentativeReturnTypes\UntypedIterator::next() is not covariant with tentative return type void of method Iterator::next().', 79, 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.', ], [ - 'Return type mixed of method TentativeReturnTypes\UntypedIterator::key() is not covariant with tentative return type mixed of method Iterator::key().', + 'Return type mixed of method TentativeReturnTypes\UntypedIterator::key() is not covariant with tentative return type mixed of method Iterator::key().', 83, 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.', ], [ - 'Return type mixed of method TentativeReturnTypes\UntypedIterator::valid() is not covariant with tentative return type bool of method Iterator::valid().', + 'Return type mixed of method TentativeReturnTypes\UntypedIterator::valid() is not covariant with tentative return type bool of method Iterator::valid().', 87, 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.', ], [ - 'Return type mixed of method TentativeReturnTypes\UntypedIterator::rewind() is not covariant with tentative return type void of method Iterator::rewind().', + 'Return type mixed of method TentativeReturnTypes\UntypedIterator::rewind() is not covariant with tentative return type void of method Iterator::rewind().', 91, 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.', ], @@ -513,13 +526,16 @@ public function dataTentativeReturnTypes(): array /** * @dataProvider dataTentativeReturnTypes - * @param mixed[] $errors + * @param list $errors */ public function testTentativeReturnTypes(int $phpVersionId, array $errors): void { if (PHP_VERSION_ID < 80100) { $errors = []; } + if ($phpVersionId > PHP_VERSION_ID) { + $this->markTestSkipped(); + } $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/tentative-return-types.php'], $errors); @@ -555,4 +571,259 @@ public function testBug6104(): void $this->analyse([__DIR__ . '/data/bug-6104.php'], []); } + public function testBug9391(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-9391.php'], []); + } + + public function testBugWithIndirectPrototype(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/overriding-indirect-prototype.php'], [ + [ + 'Return type mixed of method OverridingIndirectPrototype\Baz::doFoo() is not covariant with return type string of method OverridingIndirectPrototype\Bar::doFoo().', + 28, + ], + ]); + } + + public function testBug10043(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-10043.php'], [ + [ + 'Method Bug10043\C::foo() overrides final method Bug10043\B::foo().', + 17, + ], + ]); + } + + public function testBug7859(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-7859.php'], [ + [ + 'Method Bug7859\ExtendingClassImplementingSomeInterface::getList() overrides method Bug7859\ImplementingSomeInterface::getList() but misses parameter #2 $b.', + 21, + ], + [ + 'Method Bug7859\ExtendingClassNotImplementingSomeInterface::getList() overrides method Bug7859\NotImplementingSomeInterface::getList() but misses parameter #2 $b.', + 37, + ], + ]); + } + + public function testBug8081(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-8081.php'], [ + [ + 'Return type mixed of method Bug8081\three::foo() is not covariant with return type array of method Bug8081\two::foo().', + 21, + ], + ]); + } + + public function testBug8500(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-8500.php'], [ + [ + 'Return type mixed of method Bug8500\DBOHB::test() is not covariant with return type Bug8500\DBOA of method Bug8500\DBOHA::test().', + 30, + ], + ]); + } + + public function testBug9014(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-9014.php'], [ + [ + 'Method Bug9014\Bar::test() overrides method Bug9014\Foo::test() but misses parameter #2 $test.', + 16, + ], + [ + 'Return type mixed of method Bug9014\extended::renderForUser() is not covariant with return type string of method Bug9014\middle::renderForUser().', + 42, + ], + ]); + } + + public function testBug9135(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-9135.php'], [ + [ + 'Method Bug9135\Sub::sayHello() overrides @final method Bug9135\HelloWorld::sayHello().', + 15, + ], + ]); + } + + public function testBug10101(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-10101.php'], [ + [ + 'Return type mixed of method Bug10101\B::next() is not covariant with return type void of method Bug10101\A::next().', + 10, + ], + ]); + } + + public function testBug9615(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $tipText = 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.'; + + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-9615.php'], [ + [ + 'Return type mixed of method Bug9615\ExpectComplaintsHere::accept() is not covariant with tentative return type bool of method FilterIterator>::accept().', + 19, + $tipText, + ], + [ + 'Return type mixed of method Bug9615\ExpectComplaintsHere::getChildren() is not covariant with tentative return type RecursiveIterator|null of method RecursiveIterator::getChildren().', + 20, + $tipText, + ], + ]); + } + + public function testBug10149(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $errors = []; + if (PHP_VERSION_ID >= 80300) { + $errors = [ + [ + 'Method Bug10149\StdSat::__get() has #[\Override] attribute but does not override any method.', + 10, + ], + ]; + } + $this->analyse([__DIR__ . '/data/bug-10149.php'], $errors); + } + + public function testTraits(): void + { + $errors = []; + if (PHP_VERSION_ID >= 80000) { + $errors = [ + [ + 'Parameter #1 $i (int) of method OverridingTraitMethods\Bar::doBar() is not contravariant with parameter #1 $i (string) of method OverridingTraitMethods\Foo::doBar().', + 27, + ], + [ + 'Parameter #1 $i (int) of method OverridingTraitMethods\Baz::doBar() is not contravariant with parameter #1 $i (string) of method OverridingTraitMethods\FooPrivate::doBar().', + 45, + ], + [ + 'Static method OverridingTraitMethods\Ipsum::doBar() overrides non-static method OverridingTraitMethods\Foo::doBar().', + 65, + ], + [ + 'Non-static method OverridingTraitMethods\Dolor::doBar() overrides static method OverridingTraitMethods\FooStatic::doBar().', + 80, + ], + ]; + } + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/overriding-trait-methods.php'], $errors); + } + + public function testOverrideAttribute(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/override-attribute.php'], [ + [ + 'Method OverrideAttribute\Bar::test2() has #[\Override] attribute but does not override any method.', + 24, + ], + [ + 'Method OverrideAttribute\ChildOfParentWithConstructor::__construct() has #[\Override] attribute but does not override any method.', + 42, + ], + ]); + } + + public function dataCheckMissingOverrideAttribute(): iterable + { + yield [false, 80000, []]; + yield [true, 80000, []]; + yield [false, 80300, []]; + yield [true, 80300, [ + [ + 'Method CheckMissingOverrideAttr\Bar::doFoo() overrides method CheckMissingOverrideAttr\Foo::doFoo() but is missing the #[\Override] attribute.', + 18, + ], + [ + 'Method CheckMissingOverrideAttr\ChildOfParentWithAbstractConstructor::__construct() overrides method CheckMissingOverrideAttr\ParentWithAbstractConstructor::__construct() but is missing the #[\Override] attribute.', + 49, + ], + ]]; + } + + /** + * @dataProvider dataCheckMissingOverrideAttribute + * @param list $errors + */ + public function testCheckMissingOverrideAttribute(bool $checkMissingOverrideMethodAttribute, int $phpVersionId, array $errors): void + { + $this->checkMissingOverrideMethodAttribute = $checkMissingOverrideMethodAttribute; + $this->phpVersionId = $phpVersionId; + $this->analyse([__DIR__ . '/data/check-missing-override-attr.php'], $errors); + } + + public function testBug10153(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $errors = []; + if (PHP_VERSION_ID >= 80000) { + $errors = [ + [ + 'Return type Bug10153\MyClass2|null of method Bug10153\MyClass2::drc() is not covariant with return type Bug10153\MyClass2 of method Bug10153\MyTrait::drc().', + 24, + ], + ]; + } + $this->analyse([__DIR__ . '/data/bug-10153.php'], $errors); + } + + public function testBug10165(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-10165.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 114bf4f4c6..7564510e11 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -18,9 +18,11 @@ class ReturnTypeRuleTest extends RuleTestCase private bool $checkUnionTypes = true; + private bool $checkBenevolentUnionTypes = false; + protected function getRule(): Rule { - return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnionTypes, $this->checkExplicitMixed, false))); + return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnionTypes, $this->checkExplicitMixed, false, true, $this->checkBenevolentUnionTypes))); } public function testReturnTypeRule(): void @@ -412,6 +414,7 @@ public function testBug3117(): void [ 'Method Bug3117\SimpleTemporal::adjustInto() should return T of Bug3117\Temporal but returns $this(Bug3117\SimpleTemporal).', 35, + 'Type $this(Bug3117\SimpleTemporal) is not always the same as T. It breaks the contract for some argument types, typically subtypes.', ], ]); } @@ -437,14 +440,17 @@ public function testBug4590(): void [ 'Method Bug4590\\Controller::test1() should return Bug4590\\OkResponse> but returns Bug4590\\OkResponse.', 39, + 'Template type T on class Bug4590\OkResponse is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', ], [ 'Method Bug4590\\Controller::test2() should return Bug4590\\OkResponse> but returns Bug4590\\OkResponse.', 47, + 'Template type T on class Bug4590\OkResponse is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', ], [ 'Method Bug4590\\Controller::test3() should return Bug4590\\OkResponse> but returns Bug4590\\OkResponse.', 55, + 'Template type T on class Bug4590\OkResponse is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', ], ]); } @@ -555,7 +561,7 @@ public function dataBug5218(): array /** * @dataProvider dataBug5218 - * @param mixed[] $errors + * @param list $errors */ public function testBug5218(bool $checkExplicitMixed, array $errors): void { @@ -732,6 +738,8 @@ public function testTaggedUnions(): void [ 'Method TaggedUnionReturnCheck\HelloWorld::sayHello() should return array{updated: false, id: null}|array{updated: true, id: int} but returns array{updated: false, id: 5}.', 12, + "• Type #1 from the union: Offset 'id' (null) does not accept type int. +• Type #2 from the union: Offset 'updated' (true) does not accept type false.", ], ]); } @@ -746,4 +754,274 @@ public function testBug7904(): void $this->analyse([__DIR__ . '/data/bug-7904.php'], []); } + public function testBug7996(): void + { + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/../../Analyser/data/bug-7996.php'], []); + } + + public function testBug6358(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6358.php'], [ + [ + 'Method Bug6358\HelloWorld::sayHello() should return list but returns array{1: stdClass}.', + 14, + 'array{1: stdClass} is not a list.', + ], + ]); + } + + public function testBug8071(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8071.php'], [ + [ + // there should be no errors + 'Method Bug8071\Inheritance::inherit() should return array but returns array.', + 17, + 'Type string is not always the same as TValues. It breaks the contract for some argument types, typically subtypes.', + ], + ]); + } + + public function testBug3499(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-3499.php'], []); + } + + public function testBug8174(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8174.php'], [ + [ + "Method Bug8174\HelloWorld::filterList() should return list but returns array, '23423'>.", + 21, + "array, '23423'> might not be a list.", + ], + ]); + } + + public function testBug7519(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7519.php'], []); + } + + public function testBug8223(): void + { + $this->checkBenevolentUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-8223.php'], [ + [ + 'Method Bug8223\HelloWorld::sayHello() should return DateTimeImmutable but returns (DateTimeImmutable|false).', + 11, + ], + [ + 'Method Bug8223\HelloWorld::sayHello2() should return array but returns array.', + 21, + ], + ]); + } + + public function testBug8146bErrors(): void + { + $this->checkBenevolentUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-8146b-errors.php'], [ + [ + "Method Bug8146bError\LocationFixtures::getData() should return array, coordinates: array{lat: float, lng: float}}>> but returns array{Bács-Kiskun: array{Ágasegyháza: array{constituencies: array{'Bács-Kiskun 4.', true, false, Bug8146bError\X, null}, coordinates: array{lat: 46.8386043, lng: 19.4502899}}, Akasztó: array{constituencies: array{'Bács-Kiskun 3.'}, coordinates: array{lat: 46.6898175, lng: 19.205086}}, Apostag: array{constituencies: array{'Bács-Kiskun 3.'}, coordinates: array{lat: 46.8812652, lng: 18.9648478}}, Bácsalmás: array{constituencies: array{'Bács-Kiskun 5.'}, coordinates: array{lat: 46.1250396, lng: 19.3357509}}, Bácsbokod: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 46.1234737, lng: 19.155708}}, Bácsborsód: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 46.0989373, lng: 19.1566725}}, Bácsszentgyörgy: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 45.9746039, lng: 19.0398066}}, Bácsszőlős: array{constituencies: array{'Bács-Kiskun 5.'}, coordinates: array{lat: 46.1352003, lng: 19.4215997}}, ...}, Baranya: non-empty-array|(literal-string&non-falsy-string), float|(literal-string&non-falsy-string)>>>, Békés: array{Almáskamarás: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.4617785, lng: 21.092448}}, Battonya: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.2902462, lng: 21.0199215}}, Békés: array{constituencies: array{'Békés 2.'}, coordinates: array{lat: 46.6704899, lng: 21.0434996}}, Békéscsaba: array{constituencies: array{'Békés 1.'}, coordinates: array{lat: 46.6735939, lng: 21.0877309}}, Békéssámson: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.4208677, lng: 20.6176498}}, Békésszentandrás: array{constituencies: array{'Békés 2.'}, coordinates: array{lat: 46.8715996, lng: 20.48336}}, Bélmegyer: array{constituencies: array{'Békés 3.'}, coordinates: array{lat: 46.8726019, lng: 21.1832832}}, Biharugra: array{constituencies: array{'Békés 3.'}, coordinates: array{lat: 46.9691009, lng: 21.5987651}}, ...}, Borsod-Abaúj-Zemplén: non-empty-array|(literal-string&non-falsy-string), float|(literal-string&non-falsy-string)>>>, Budapest: array{Budapest I. ker.: array{constituencies: array{'Budapest 01.'}, coordinates: array{lat: 47.4968219, lng: 19.037458}}, Budapest II. ker.: array{constituencies: array{'Budapest 03.', 'Budapest 04.'}, coordinates: array{lat: 47.5393329, lng: 18.986934}}, Budapest III. ker.: array{constituencies: array{'Budapest 04.', 'Budapest 10.'}, coordinates: array{lat: 47.5671768, lng: 19.0368517}}, Budapest IV. ker.: array{constituencies: array{'Budapest 11.', 'Budapest 12.'}, coordinates: array{lat: 47.5648915, lng: 19.0913149}}, Budapest V. ker.: array{constituencies: array{'Budapest 01.'}, coordinates: array{lat: 47.5002319, lng: 19.0520181}}, Budapest VI. ker.: array{constituencies: array{'Budapest 05.'}, coordinates: array{lat: 47.509863, lng: 19.0625813}}, Budapest VII. ker.: array{constituencies: array{'Budapest 05.'}, coordinates: array{lat: 47.5027289, lng: 19.073376}}, Budapest VIII. ker.: array{constituencies: array{'Budapest 01.', 'Budapest 06.'}, coordinates: array{lat: 47.4894184, lng: 19.070668}}, ...}, Csongrád-Csanád: array{Algyő: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.3329625, lng: 20.207889}}, Ambrózfalva: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.3501417, lng: 20.7313995}}, Apátfalva: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.173317, lng: 20.5800472}}, Árpádhalom: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.6158286, lng: 20.547733}}, Ásotthalom: array{constituencies: array{'Csongrád-Csanád 2.'}, coordinates: array{lat: 46.1995983, lng: 19.7833756}}, Baks: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.5518708, lng: 20.1064166}}, Balástya: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.4261828, lng: 20.004933}}, Bordány: array{constituencies: array{'Csongrád-Csanád 2.'}, coordinates: array{lat: 46.3194213, lng: 19.9227063}}, ...}, Fejér: array{Aba: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 47.0328193, lng: 18.522359}}, Adony: array{constituencies: array{'Fejér 4.'}, coordinates: array{lat: 47.119831, lng: 18.8612469}}, Alap: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 46.8075763, lng: 18.684028}}, Alcsútdoboz: array{constituencies: array{'Fejér 3.'}, coordinates: array{lat: 47.4277067, lng: 18.6030325}}, Alsószentiván: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 46.7910573, lng: 18.732161}}, Bakonycsernye: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.321719, lng: 18.0907379}}, Bakonykúti: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.2458464, lng: 18.195769}}, Balinka: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.3135736, lng: 18.1907168}}, ...}, Győr-Moson-Sopron: array{Abda: array{constituencies: array{'Győr-Moson-Sopron 5.'}, coordinates: array{lat: 47.6962149, lng: 17.5445786}}, Acsalag: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.676095, lng: 17.1977771}}, Ágfalva: array{constituencies: array{'Győr-Moson-Sopron 4.'}, coordinates: array{lat: 47.688862, lng: 16.5110233}}, Agyagosszergény: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.608545, lng: 16.9409912}}, Árpás: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5134127, lng: 17.3931579}}, Ásványráró: array{constituencies: array{'Győr-Moson-Sopron 5.'}, coordinates: array{lat: 47.8287695, lng: 17.499195}}, Babót: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5752269, lng: 17.0758604}}, Bágyogszovát: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5866036, lng: 17.3617273}}, ...}, ...}.", + 12, + "Offset 'constituencies' (non-empty-list) does not accept type array{'Bács-Kiskun 4.', true, false, Bug8146bError\X, null}.", + ], + ]); + } + + public function testBug8573(): void + { + $this->analyse([__DIR__ . '/data/bug-8573.php'], []); + } + + public function testBug8879(): void + { + $this->analyse([__DIR__ . '/data/bug-8879.php'], []); + } + + public function testBug9011(): void + { + $errors = []; + if (PHP_VERSION_ID < 80000) { + $errors = [ + [ + 'Method Bug9011\HelloWorld::getX() should return array but returns false.', + 16, + ], + ]; + } + + $this->analyse([__DIR__ . '/data/bug-9011.php'], $errors); + } + + public function testMagicSerialization(): void + { + $this->analyse([__DIR__ . '/data/magic-serialization.php'], [ + [ + 'Method MagicSerialization\WrongSignature::__serialize() should return array but returns string.', + 23, + ], + [ + 'Method MagicSerialization\WrongSignature::__unserialize() with return type void returns string but should not return anything.', + 28, + ], + ]); + } + + public function testBug7574(): void + { + $this->analyse([__DIR__ . '/../Classes/data/bug-7574.php'], []); + } + + public function testMagicSignatures(): void + { + $this->analyse([__DIR__ . '/data/magic-signatures.php'], [ + [ + 'Method MagicSignatures\WrongSignature::__isset() should return bool but returns string.', + 39, + ], + [ + 'Method MagicSignatures\WrongSignature::__clone() with return type void returns string but should not return anything.', + 43, + ], + [ + 'Method MagicSignatures\WrongSignature::__debugInfo() should return array|null but returns string.', + 47, + ], + [ + 'Method MagicSignatures\WrongSignature::__set() with return type void returns string but should not return anything.', + 51, + ], + [ + 'Method MagicSignatures\WrongSignature::__set_state() should return object but returns string.', + 55, + ], + [ + 'Method MagicSignatures\WrongSignature::__sleep() should return array but returns string.', + 59, + ], + [ + 'Method MagicSignatures\WrongSignature::__unset() with return type void returns string but should not return anything.', + 63, + ], + [ + 'Method MagicSignatures\WrongSignature::__wakeup() with return type void returns string but should not return anything.', + 67, + ], + ]); + } + + public function testLists(): void + { + $this->analyse([__DIR__ . '/data/return-list.php'], [ + [ + "Method ReturnList\Foo::getList1() should return list but returns array{0?: 'foo', 1?: 'bar'}.", + 10, + "array{0?: 'foo', 1?: 'bar'} might not be a list.", + ], + [ + "Method ReturnList\Foo::getList2() should return list but returns array{0?: 'foo', 1?: 'bar'}.", + 19, + "array{0?: 'foo', 1?: 'bar'} might not be a list.", + ], + ]); + } + + public function testConditionalListRule(): void + { + $this->analyse([__DIR__ . '/data/return-list-rule.php'], []); + } + + public function testBug6856(): void + { + $this->analyse([__DIR__ . '/data/bug-6856.php'], []); + } + + public function testRuleError(): void + { + $this->analyse([__DIR__ . '/data/return-rule-error.php'], [ + [ + "Method ReturnRuleError\Bar::processNode() should return list but returns array{'foo'}.", + 47, + 'Rules can no longer return plain strings. See: https://phpstan.org/blog/using-rule-error-builder', + ], + [ + 'Method ReturnRuleError\Baz::processNode() should return list but returns array{PHPStan\Rules\RuleError}.', + 66, + 'Error is missing an identifier. See: https://phpstan.org/blog/using-rule-error-builder', + ], + [ + 'Method ReturnRuleError\Lorem::processNode() should return list but returns array{1: PHPStan\Rules\IdentifierRuleError}.', + 88, + 'array{1: PHPStan\Rules\IdentifierRuleError} is not a list.', + ], + ]); + } + + public function testBug6175(): void + { + $this->analyse([__DIR__ . '/data/bug-6175.php'], []); + } + + public function testBug9766(): void + { + $this->checkBenevolentUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-9766.php'], []); + } + + public function testWrongListTip(): void + { + $this->analyse([__DIR__ . '/data/wrong-list-tip.php'], [ + [ + 'Method WrongListTip\Test::doFoo() should return list but returns list.', + 23, + ], + [ + 'Method WrongListTip\Test2::doFoo() should return non-empty-array but returns non-empty-array.', + 44, + ], + [ + 'Method WrongListTip\Test3::doFoo() should return non-empty-list but returns array.', + 67, + "• array might not be a list.\n• array might be empty.", + ], + ]); + } + + public function testArrowFunctionReturningVoidClosure(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->analyse([__DIR__ . '/data/arrow-function-returning-void-closure.php'], []); + } + + public function testBug6653(): void + { + $this->analyse([__DIR__ . '/data/bug-6653.php'], []); + } + + public function testBug10291(): void + { + $this->analyse([__DIR__ . '/data/bug-10291.php'], []); + } + + public function testBug5008(): void + { + $this->analyse([__DIR__ . '/data/bug-5008.php'], []); + } + + public function testArrayPushPreservesList(): void + { + $this->analyse([__DIR__ . '/data/array-push-preserves-list.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php b/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php index 3976917672..8b026d8abf 100644 --- a/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php +++ b/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -20,10 +22,19 @@ class StaticMethodCallableRuleTest extends RuleTestCase protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, false, false); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false); return new StaticMethodCallableRule( - new StaticMethodCallCheck($reflectionProvider, $ruleLevelHelper, new ClassCaseSensitivityCheck($reflectionProvider, true), true, true), + new StaticMethodCallCheck( + $reflectionProvider, + $ruleLevelHelper, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, + true, + ), new PhpVersion($this->phpVersion), ); } @@ -94,4 +105,14 @@ public function testRule(): void ]); } + public function testBug8752(): void + { + $this->analyse([__DIR__ . '/../../Analyser/data/bug-8752.php'], []); + } + + public function testCallsOnGenericClassString(): void + { + $this->analyse([__DIR__ . '/../Comparison/data/impossible-method-exists-on-generic-class-string.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/VirtualNullsafeMethodCallTest.php b/tests/PHPStan/Rules/Methods/VirtualNullsafeMethodCallTest.php new file mode 100644 index 0000000000..bfb55be65b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/VirtualNullsafeMethodCallTest.php @@ -0,0 +1,56 @@ + + */ +class VirtualNullsafeMethodCallTest extends RuleTestCase +{ + + /** + * @return Rule + */ + protected function getRule(): Rule + { + return new /** @implements Rule */ class implements Rule { + + public function getNodeType(): string + { + return MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node->getAttribute('virtualNullsafeMethodCall') === true) { + return [RuleErrorBuilder::message('Nullable method call detected')->identifier('ruleTest.VirtualNullsafeMethod')->build()]; + } + + return [RuleErrorBuilder::message('Regular method call detected')->identifier('ruleTest.VirtualNullsafeMethod')->build()]; + } + + }; + } + + public function testAttribute(): void + { + $this->analyse([ __DIR__ . '/data/virtual-nullsafe-method-call.php'], [ + [ + 'Regular method call detected', + 3, + ], + [ + 'Nullable method call detected', + 4, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/abstract-private-method.php b/tests/PHPStan/Rules/Methods/data/abstract-private-method.php new file mode 100644 index 0000000000..afb7d91eba --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/abstract-private-method.php @@ -0,0 +1,27 @@ +sayHello(); + $this->sayWorld(); + } + + abstract private function sayPrivate() : void; + abstract protected function sayProtected() : void; + abstract public function sayPublic() : void; +} + +trait fooTrait{ + abstract private function sayPrivate() : void; + abstract protected function sayProtected() : void; + abstract public function sayPublic() : void; +} + +interface fooInterface { + abstract private function sayPrivate() : void; + abstract protected function sayProtected() : void; + abstract public function sayPublic() : void; +} diff --git a/tests/PHPStan/Rules/Methods/data/array-cast-list-types.php b/tests/PHPStan/Rules/Methods/data/array-cast-list-types.php new file mode 100644 index 0000000000..6e0c308721 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/array-cast-list-types.php @@ -0,0 +1,29 @@ + $var + */ + public function foo($var): void {} + + /** + * @param literal-string $literalString + * @param non-empty-string $nonEmptyString + * @param non-falsy-string $nonFalsyString + * @param numeric-string $numericString + * @param resource $resource + */ + public function bar(string $literalString, string $nonEmptyString, string $nonFalsyString, string $numericString, $resource) { + $this->foo((array) true); + $this->foo((array) $literalString); + $this->foo((array) 1.0); + $this->foo((array) 1); + $this->foo((array) $resource); + $this->foo((array) (fn () => 'closure')); + $this->foo((array) $nonEmptyString); + $this->foo((array) $nonFalsyString); + $this->foo((array) $numericString); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/array-push-preserves-list.php b/tests/PHPStan/Rules/Methods/data/array-push-preserves-list.php new file mode 100644 index 0000000000..d892ab794d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/array-push-preserves-list.php @@ -0,0 +1,83 @@ + $a + * @return list + */ + public function doFoo(array $a): array + { + array_push($a, ...$a); + + return $a; + } + + /** + * @param list $a + * @return list + */ + public function doFoo2(array $a): array + { + array_push($a, ...[1, 2, 3]); + + return $a; + } + + /** + * @param list $a + * @return list + */ + public function doFoo3(array $a): array + { + $b = [1, 2, 3]; + array_push($b, ...$a); + + return $b; + } + +} + +class Bar +{ + + /** + * @param list $a + * @return list + */ + public function doFoo(array $a): array + { + array_unshift($a, ...$a); + + return $a; + } + + /** + * @param list $a + * @return list + */ + public function doFoo2(array $a): array + { + array_unshift($a, ...[1, 2, 3]); + + return $a; + } + + /** + * @param list $a + * @return list + */ + public function doFoo3(array $a): array + { + $b = [1, 2, 3]; + array_unshift($b, ...$a); + + return $b; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/arrow-function-returning-void-closure.php b/tests/PHPStan/Rules/Methods/data/arrow-function-returning-void-closure.php new file mode 100644 index 0000000000..3cfb9dc48a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/arrow-function-returning-void-closure.php @@ -0,0 +1,19 @@ + $this->returnVoid(); + } + + public function returnVoid(): void + { + + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10043.php b/tests/PHPStan/Rules/Methods/data/bug-10043.php new file mode 100644 index 0000000000..9980c932ae --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10043.php @@ -0,0 +1,18 @@ +{$name}; + } +} + +class StdSat extends \stdClass +{ + use WarnDynamicPropertyTrait; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10153.php b/tests/PHPStan/Rules/Methods/data/bug-10153.php new file mode 100644 index 0000000000..e2b5f06840 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10153.php @@ -0,0 +1,28 @@ + */ + abstract public function foo(): Collection; +} + +class Baz +{ + /** @use FooTrait */ + use FooTrait; + + /** @return Collection */ + public function foo(): Collection + { + /** @var Collection */ + return new Collection(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10208.php b/tests/PHPStan/Rules/Methods/data/bug-10208.php new file mode 100644 index 0000000000..abc33152ae --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10208.php @@ -0,0 +1,24 @@ +key = null; + + return $this; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10291.php b/tests/PHPStan/Rules/Methods/data/bug-10291.php new file mode 100644 index 0000000000..cb23fe92cf --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10291.php @@ -0,0 +1,25 @@ +myrand(); + } + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-10956.php b/tests/PHPStan/Rules/Methods/data/bug-10956.php new file mode 100644 index 0000000000..459d5bd6c2 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10956.php @@ -0,0 +1,19 @@ +foo->getClone(); + } +} + + diff --git a/tests/PHPStan/Rules/Methods/data/bug-4008.php b/tests/PHPStan/Rules/Methods/data/bug-4008.php index 7d93147d5f..68414bb3cb 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-4008.php +++ b/tests/PHPStan/Rules/Methods/data/bug-4008.php @@ -29,3 +29,12 @@ class OtherGenericClass{} abstract class BaseModel{} class Model extends BaseModel{} + +/** + * @template T of Model + * @extends GenericClass + */ +class ChildGenericGenericClass extends GenericClass +{ + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4244.php b/tests/PHPStan/Rules/Methods/data/bug-4244.php new file mode 100644 index 0000000000..b5374297f7 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4244.php @@ -0,0 +1,6 @@ +value = $value; + } + + final public static function fromString(string $value): self + { + return new static($value); + } +} + +final class ClassB extends ClassC +{ +} + +final class ClassA +{ + public function classB(): ClassB + { + return ClassB::fromString("any"); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4758.php b/tests/PHPStan/Rules/Methods/data/bug-4758.php new file mode 100644 index 0000000000..e45329dd1f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4758.php @@ -0,0 +1,26 @@ + + */ + public function doStuff(): array + { + return [[]]; + } +} + +trait TraitTwo +{ + use TraitOne { + TraitOne::doStuff as doStuffFromTraitOne; + } +} + +class SomeController +{ + use TraitTwo; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5008.php b/tests/PHPStan/Rules/Methods/data/bug-5008.php new file mode 100644 index 0000000000..f14d3aef2b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5008.php @@ -0,0 +1,14 @@ + $b; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5089.php b/tests/PHPStan/Rules/Methods/data/bug-5089.php index e3578f2235..eb5306ece8 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-5089.php +++ b/tests/PHPStan/Rules/Methods/data/bug-5089.php @@ -16,6 +16,6 @@ public function encode(string $foo): array public function test(): void { - assertType('*NEVER*', $this->encode('foo')); + assertType('never', $this->encode('foo')); } } diff --git a/tests/PHPStan/Rules/Methods/data/bug-5372.php b/tests/PHPStan/Rules/Methods/data/bug-5372.php index 74cd3d334e..34d339385e 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-5372.php +++ b/tests/PHPStan/Rules/Methods/data/bug-5372.php @@ -60,7 +60,7 @@ public function doFoo(string $classString) assertType('Bug5372\Collection', $col); $newCol = $col->map(static fn(string $var): string => $var . 'bar'); - assertType('Bug5372\Collection', $newCol); + assertType('Bug5372\Collection', $newCol); $this->takesStrings($newCol); $newCol = $col->map(static fn(string $var): string => $classString); @@ -77,7 +77,7 @@ public function doBar(string $literalString) { $col = new Collection(['foo', 'bar']); $newCol = $col->map(static fn(string $var): string => $literalString); - assertType('Bug5372\Collection', $newCol); + assertType('Bug5372\Collection', $newCol); $this->takesStrings($newCol); $newCol = $col->map2(static fn(string $var): string => $literalString); diff --git a/tests/PHPStan/Rules/Methods/data/bug-5518.php b/tests/PHPStan/Rules/Methods/data/bug-5518.php new file mode 100644 index 0000000000..749501df95 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5518.php @@ -0,0 +1,28 @@ + */ +interface TypeNonEmptyString extends TypeParse +{ +} + +interface Params +{ + /** + * @param TypeParse $type + * @template T + */ + public function get(TypeParse ...$type): void; +} + +class Test { + public function exec(Params $params, TypeNonEmptyString $string): void { + $params->get($string); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5623.php b/tests/PHPStan/Rules/Methods/data/bug-5623.php new file mode 100644 index 0000000000..3c9277e611 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5623.php @@ -0,0 +1,16 @@ +format(DateTimeInterface::ATOM); + } + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5757.php b/tests/PHPStan/Rules/Methods/data/bug-5757.php index 64fa9b4de3..d0b84715f0 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-5757.php +++ b/tests/PHPStan/Rules/Methods/data/bug-5757.php @@ -22,7 +22,7 @@ class Foo public function doFoo() { - assertType('iterable>', Helper::chunk([1], 3)); + assertType('iterable>', Helper::chunk([1], 3)); assertType('iterable>', Helper::chunk([], 3)); } diff --git a/tests/PHPStan/Rules/Methods/data/bug-5781.php b/tests/PHPStan/Rules/Methods/data/bug-5781.php new file mode 100644 index 0000000000..3fba572fe6 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5781.php @@ -0,0 +1,20 @@ + $class + * @return void + */ + public static function invokeController(string $class): void + { + if (/* Http::methodIs ("post") && */ method_exists($class, "methodPost")) { + $class::methodPost(); // Call to an undefined static method ControllerInterface::methodPost() + } + } +} + +interface ControllerInterface +{ + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6175.php b/tests/PHPStan/Rules/Methods/data/bug-6175.php new file mode 100644 index 0000000000..b077a74e34 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6175.php @@ -0,0 +1,36 @@ +foo()); + } +} + +class Model +{ + use RefsTrait; + + /** + * @return B|C + */ + public function foo2() + { + return new A(); // @phpstan-ignore-line + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6358.php b/tests/PHPStan/Rules/Methods/data/bug-6358.php new file mode 100644 index 0000000000..cab6391c6e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6358.php @@ -0,0 +1,16 @@ + + */ + public function sayHello(): array + { + return [1 => new stdClass]; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6371.php b/tests/PHPStan/Rules/Methods/data/bug-6371.php new file mode 100644 index 0000000000..aa1d09f62f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6371.php @@ -0,0 +1,25 @@ + $hw + * @return void + */ +function foo (HelloWorld $hw): void { + $hw->compare(1, 'foo'); + $hw->compare(true, false); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-6462.php b/tests/PHPStan/Rules/Methods/data/bug-6462.php new file mode 100644 index 0000000000..4717ce76c4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6462.php @@ -0,0 +1,35 @@ +|false + */ + public function sayHello() + { + $test = $this->getTest(); + return $this->filterEvent('sayHello', $test); + } + + /** + * @template TValue of mixed + * @param TValue $value + * @return TValue + */ + private function filterEvent(string $eventName, $value) + { + // do event + return $value; + } + + /** + * @return array|false + */ + private function getTest() + { + $failure = random_int(0, PHP_INT_MAX) % 2 ? true : false; + if ($failure === true) { + return false; + } + return ['foo' => 123]; + } +} + +class HelloWorld2 +{ + /** + * @return array|false + */ + public function sayHello() + { + $test = $this->getTest(); + return $this->filterEvent('sayHello', $test); + } + + /** + * @template TValue of mixed + * @param TValue $value + * @return TValue + */ + private function filterEvent(string $eventName, $value) + { + // do event + return $value; + } + + /** + * @return array|false + */ + private function getTest() + { + $failure = random_int(0, PHP_INT_MAX) % 2 ? true : false; + if ($failure === true) { + return false; + } + return ['foo' => 123]; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6856.php b/tests/PHPStan/Rules/Methods/data/bug-6856.php new file mode 100644 index 0000000000..4422538425 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6856.php @@ -0,0 +1,52 @@ + + */ + use TraitA { + a as renamed; + } + + public function a(): ClassB { + return $this->renamed(); + } + + public function b(): ClassB { + return $this->test(); + } +} + +class ClassB { + // empty +} + +function (ClassA $a): void { + assertType(ClassB::class, $a->a()); + assertType(ClassB::class, $a->renamed()); + assertType(ClassB::class, $a->test()); + assertType(ClassB::class, $a->b()); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-6922b.php b/tests/PHPStan/Rules/Methods/data/bug-6922b.php new file mode 100644 index 0000000000..ccb086c07c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6922b.php @@ -0,0 +1,26 @@ +isFirstOptionActive() === false || + $configuration?->isSecondOptionActive() === false) + { + // .... + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-7519.php b/tests/PHPStan/Rules/Methods/data/bug-7519.php new file mode 100644 index 0000000000..c1020b84f8 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7519.php @@ -0,0 +1,30 @@ + + * + * @extends FilterIterator + */ +class A extends FilterIterator { + public function accept(): bool { + return true; + } + + public function key() { + $key = parent::key(); + + return $key; + } + + public function current() { + $current = parent::current(); + + return $current; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-7859.php b/tests/PHPStan/Rules/Methods/data/bug-7859.php new file mode 100644 index 0000000000..0cb1fb7f60 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7859.php @@ -0,0 +1,40 @@ +> $items + * + * @return array + * + * @template TKey of array-key + * @template TValues of scalar|null + */ + public static function inherit(array $items): array + { + return array_reduce( + $items, + [self::class, 'callBack'], + ) ?? []; + } + + /** + * @param array|null $carry + * @param array $current + * + * @return array + * + * @template TKey of array-key + * @template TValues of scalar|null + */ + private static function callBack(array|null $carry, array $current): array + { + if ($carry === null) { + return $current; + } + + foreach ($carry as $key => $value) { + if ($value !== null) { + continue; + } + + $carry[$key] = $current[$key]; + } + + return $carry; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8081.php b/tests/PHPStan/Rules/Methods/data/bug-8081.php new file mode 100644 index 0000000000..575ef69d38 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8081.php @@ -0,0 +1,24 @@ + + */ + public function foo() { + return []; + } +} + +class two extends one { + public function foo(): array { + return []; + } +} + +class three extends two { + public function foo() { + return []; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8146b-errors.php b/tests/PHPStan/Rules/Methods/data/bug-8146b-errors.php new file mode 100644 index 0000000000..27509dcc96 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8146b-errors.php @@ -0,0 +1,5544 @@ +, coordinates: array{lat: float, lng: float}}>> */ + public function getData(): array + { + return [ + 'Bács-Kiskun' => [ + 'Ágasegyháza' => [ + 'constituencies' => ['Bács-Kiskun 4.', true, false, new X(), null], // expected type errors + 'coordinates' => ['lat' => 46.8386043, 'lng' => 19.4502899], + ], + 'Akasztó' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6898175, 'lng' => 19.205086], + ], + 'Apostag' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.8812652, 'lng' => 18.9648478], + ], + 'Bácsalmás' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.1250396, 'lng' => 19.3357509], + ], + 'Bácsbokod' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1234737, 'lng' => 19.155708], + ], + 'Bácsborsód' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0989373, 'lng' => 19.1566725], + ], + 'Bácsszentgyörgy' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 45.9746039, 'lng' => 19.0398066], + ], + 'Bácsszőlős' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.1352003, 'lng' => 19.4215997], + ], + 'Baja' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1817951, 'lng' => 18.9543051], + ], + 'Ballószög' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.8619947, 'lng' => 19.5726144], + ], + 'Balotaszállás' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.3512041, 'lng' => 19.5403558], + ], + 'Bátmonostor' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1057304, 'lng' => 18.9238311], + ], + 'Bátya' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.4891741, 'lng' => 18.9579127], + ], + 'Bócsa' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6113504, 'lng' => 19.4826419], + ], + 'Borota' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2657107, 'lng' => 19.2233598], + ], + 'Bugac' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6883076, 'lng' => 19.6833655], + ], + 'Bugacpusztaháza' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7022143, 'lng' => 19.6356538], + ], + 'Császártöltés' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4222869, 'lng' => 19.1815532], + ], + 'Csátalja' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0363238, 'lng' => 18.9469006], + ], + 'Csávoly' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1912599, 'lng' => 19.1451178], + ], + 'Csengőd' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.71532, 'lng' => 19.2660933], + ], + 'Csikéria' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.121679, 'lng' => 19.473777], + ], + 'Csólyospálos' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.4180837, 'lng' => 19.8402638], + ], + 'Dávod' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 45.9976187, 'lng' => 18.9176479], + ], + 'Drágszél' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.4653889, 'lng' => 19.0382659], + ], + 'Dunaegyháza' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.8383215, 'lng' => 18.9605216], + ], + 'Dunafalva' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.081562, 'lng' => 18.7782526], + ], + 'Dunapataj' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6422106, 'lng' => 18.9989393], + ], + 'Dunaszentbenedek' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.593856, 'lng' => 18.8935322], + ], + 'Dunatetétlen' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.7578624, 'lng' => 19.0932563], + ], + 'Dunavecse' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.9133047, 'lng' => 18.9731873], + ], + 'Dusnok' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.3893659, 'lng' => 18.960842], + ], + 'Érsekcsanád' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.2541554, 'lng' => 18.9835293], + ], + 'Érsekhalma' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.3472701, 'lng' => 19.1247379], + ], + 'Fajsz' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.4157936, 'lng' => 18.9191954], + ], + 'Felsőlajos' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0647473, 'lng' => 19.4944348], + ], + 'Felsőszentiván' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1966179, 'lng' => 19.1873616], + ], + 'Foktő' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5268759, 'lng' => 18.9196874], + ], + 'Fülöpháza' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.8914016, 'lng' => 19.4432493], + ], + 'Fülöpjakab' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.742058, 'lng' => 19.7227232], + ], + 'Fülöpszállás' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8195701, 'lng' => 19.2372115], + ], + 'Gara' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0349999, 'lng' => 19.0393411], + ], + 'Gátér' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.680435, 'lng' => 19.9596412], + ], + 'Géderlak' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6072512, 'lng' => 18.9135762], + ], + 'Hajós' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4001409, 'lng' => 19.1193255], + ], + 'Harkakötöny' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4634053, 'lng' => 19.6069951], + ], + 'Harta' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6960997, 'lng' => 19.0328195], + ], + 'Helvécia' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.8360977, 'lng' => 19.620438], + ], + 'Hercegszántó' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 45.9482057, 'lng' => 18.9389127], + ], + 'Homokmégy' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.4892762, 'lng' => 19.0730421], + ], + 'Imrehegy' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4867668, 'lng' => 19.3056372], + ], + 'Izsák' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8020009, 'lng' => 19.3546225], + ], + 'Jakabszállás' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7602785, 'lng' => 19.6055301], + ], + 'Jánoshalma' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2974544, 'lng' => 19.3250656], + ], + 'Jászszentlászló' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.5672659, 'lng' => 19.7590541], + ], + 'Kalocsa' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5281229, 'lng' => 18.9840376], + ], + 'Kaskantyú' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6711891, 'lng' => 19.3895391], + ], + 'Katymár' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0344636, 'lng' => 19.2087609], + ], + 'Kecel' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.5243135, 'lng' => 19.2451963], + ], + 'Kecskemét' => [ + 'constituencies' => ['Bács-Kiskun 2.', 'Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.8963711, 'lng' => 19.6896861], + ], + 'Kelebia' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.1958608, 'lng' => 19.6066291], + ], + 'Kéleshalom' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.3641795, 'lng' => 19.2831241], + ], + 'Kerekegyháza' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.9385747, 'lng' => 19.4770208], + ], + 'Kiskőrös' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6224967, 'lng' => 19.2874568], + ], + 'Kiskunfélegyháza' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7112802, 'lng' => 19.8515196], + ], + 'Kiskunhalas' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.4354409, 'lng' => 19.4834284], + ], + 'Kiskunmajsa' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.4904848, 'lng' => 19.7366569], + ], + 'Kisszállás' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2791272, 'lng' => 19.4908079], + ], + 'Kömpöc' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.4640167, 'lng' => 19.8665681], + ], + 'Kunadacs' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.956503, 'lng' => 19.2880496], + ], + 'Kunbaja' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.0848391, 'lng' => 19.4213713], + ], + 'Kunbaracs' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.9891493, 'lng' => 19.3999584], + ], + 'Kunfehértó' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.362671, 'lng' => 19.4141949], + ], + 'Kunpeszér' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0611502, 'lng' => 19.2753764], + ], + 'Kunszállás' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7627801, 'lng' => 19.7532925], + ], + 'Kunszentmiklós' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0244473, 'lng' => 19.1235997], + ], + 'Ladánybene' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0344239, 'lng' => 19.456807], + ], + 'Lajosmizse' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0248225, 'lng' => 19.5559232], + ], + 'Lakitelek' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8710339, 'lng' => 19.9930216], + ], + 'Madaras' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0554833, 'lng' => 19.2633403], + ], + 'Mátételke' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1614675, 'lng' => 19.2802263], + ], + 'Mélykút' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2132295, 'lng' => 19.3814176], + ], + 'Miske' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.4434918, 'lng' => 19.0315752], + ], + 'Móricgát' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6233704, 'lng' => 19.6885382], + ], + 'Nagybaracska' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.0444015, 'lng' => 18.9048387], + ], + 'Nemesnádudvar' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.3348444, 'lng' => 19.0542114], + ], + 'Nyárlőrinc' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8611255, 'lng' => 19.8773125], + ], + 'Ordas' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6364524, 'lng' => 18.9504602], + ], + 'Öregcsertő' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.515272, 'lng' => 19.1090595], + ], + 'Orgovány' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.7497582, 'lng' => 19.4746024], + ], + 'Páhi' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.7136232, 'lng' => 19.3856937], + ], + 'Pálmonostora' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6265115, 'lng' => 19.9425525], + ], + 'Petőfiszállás' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.6243457, 'lng' => 19.8596537], + ], + 'Pirtó' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.5139604, 'lng' => 19.4301958], + ], + 'Rém' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.2470804, 'lng' => 19.1416684], + ], + 'Solt' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.8021967, 'lng' => 19.0108147], + ], + 'Soltszentimre' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.769786, 'lng' => 19.2840433], + ], + 'Soltvadkert' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5789287, 'lng' => 19.3938029], + ], + 'Sükösd' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.2832039, 'lng' => 18.9942907], + ], + 'Szabadszállás' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.8763076, 'lng' => 19.2232539], + ], + 'Szakmár' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5543652, 'lng' => 19.0742847], + ], + 'Szalkszentmárton' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 46.9754928, 'lng' => 19.0171018], + ], + 'Szank' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.5557842, 'lng' => 19.6668956], + ], + 'Szentkirály' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.9169398, 'lng' => 19.9175371], + ], + 'Szeremle' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1436504, 'lng' => 18.8810207], + ], + 'Tabdi' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.6818019, 'lng' => 19.3042672], + ], + 'Tass' => [ + 'constituencies' => ['Bács-Kiskun 1.'], + 'coordinates' => ['lat' => 47.0184485, 'lng' => 19.0281253], + ], + 'Tataháza' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.173167, 'lng' => 19.3024716], + ], + 'Tázlár' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.5509533, 'lng' => 19.5159844], + ], + 'Tiszaalpár' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8140236, 'lng' => 19.9936556], + ], + 'Tiszakécske' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.9358726, 'lng' => 20.0969279], + ], + 'Tiszaug' => [ + 'constituencies' => ['Bács-Kiskun 4.'], + 'coordinates' => ['lat' => 46.8537215, 'lng' => 20.052921], + ], + 'Tompa' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.2060507, 'lng' => 19.5389553], + ], + 'Újsolt' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.8706098, 'lng' => 19.1186222], + ], + 'Újtelek' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5911716, 'lng' => 19.0564597], + ], + 'Uszód' => [ + 'constituencies' => ['Bács-Kiskun 3.'], + 'coordinates' => ['lat' => 46.5704972, 'lng' => 18.9038275], + ], + 'Városföld' => [ + 'constituencies' => ['Bács-Kiskun 2.'], + 'coordinates' => ['lat' => 46.8174844, 'lng' => 19.7597893], + ], + 'Vaskút' => [ + 'constituencies' => ['Bács-Kiskun 6.'], + 'coordinates' => ['lat' => 46.1080968, 'lng' => 18.9861524], + ], + 'Zsana' => [ + 'constituencies' => ['Bács-Kiskun 5.'], + 'coordinates' => ['lat' => 46.3802847, 'lng' => 19.6600846], + ], + ], + 'Baranya' => [ + 'Abaliget' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1428711, 'lng' => 18.1152298], + ], + 'Adorjás' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8509119, 'lng' => 18.0617924], + ], + 'Ág' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2962836, 'lng' => 18.2023275], + ], + 'Almamellék' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1603198, 'lng' => 17.8765681], + ], + 'Almáskeresztúr' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1199547, 'lng' => 17.8958453], + ], + 'Alsómocsolád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.313518, 'lng' => 18.2481993], + ], + 'Alsószentmárton' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.7912208, 'lng' => 18.3065816], + ], + 'Apátvarasd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1856469, 'lng' => 18.47932], + ], + 'Aranyosgadány' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.007757, 'lng' => 18.1195466], + ], + 'Áta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9367366, 'lng' => 18.2985608], + ], + 'Babarc' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0042229, 'lng' => 18.5527511], + ], + 'Babarcszőlős' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.898699, 'lng' => 18.1360284], + ], + 'Bakóca' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2074891, 'lng' => 18.0002016], + ], + 'Bakonya' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0850942, 'lng' => 18.082286], + ], + 'Baksa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9554293, 'lng' => 18.0909794], + ], + 'Bánfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.994691, 'lng' => 17.8798792], + ], + 'Bár' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0482419, 'lng' => 18.7119502], + ], + 'Baranyahídvég' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8461886, 'lng' => 18.0229597], + ], + 'Baranyajenő' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2734519, 'lng' => 18.0469416], + ], + 'Baranyaszentgyörgy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2461345, 'lng' => 18.0119839], + ], + 'Basal' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0734372, 'lng' => 17.7832659], + ], + 'Belvárdgyula' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9750659, 'lng' => 18.4288438], + ], + 'Beremend' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.7877528, 'lng' => 18.4322322], + ], + 'Berkesd' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0766759, 'lng' => 18.4078442], + ], + 'Besence' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8956421, 'lng' => 17.9654588], + ], + 'Bezedek' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8653948, 'lng' => 18.5854023], + ], + 'Bicsérd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0216488, 'lng' => 18.0779429], + ], + 'Bikal' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3329154, 'lng' => 18.2845332], + ], + 'Birján' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0007461, 'lng' => 18.3739733], + ], + 'Bisse' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9082449, 'lng' => 18.2603363], + ], + 'Boda' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0796449, 'lng' => 18.0477749], + ], + 'Bodolyabér' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.196906, 'lng' => 18.1189705], + ], + 'Bogád' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0858618, 'lng' => 18.3215439], + ], + 'Bogádmindszent' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9069292, 'lng' => 18.0382456], + ], + 'Bogdása' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8756825, 'lng' => 17.7892759], + ], + 'Boldogasszonyfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1826055, 'lng' => 17.8379176], + ], + 'Bóly' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9654045, 'lng' => 18.5166166], + ], + 'Borjád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9356423, 'lng' => 18.4708549], + ], + 'Bosta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9500492, 'lng' => 18.2104193], + ], + 'Botykapeterd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0499466, 'lng' => 17.8662441], + ], + 'Bükkösd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1100188, 'lng' => 17.9925218], + ], + 'Bürüs' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9653278, 'lng' => 17.7591739], + ], + 'Csányoszró' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8810774, 'lng' => 17.9101381], + ], + 'Csarnóta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8949174, 'lng' => 18.2163121], + ], + 'Csebény' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1893582, 'lng' => 17.9275209], + ], + 'Cserdi' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0808529, 'lng' => 17.9911191], + ], + 'Cserkút' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0756664, 'lng' => 18.1340119], + ], + 'Csertő' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.093457, 'lng' => 17.8034587], + ], + 'Csonkamindszent' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0518017, 'lng' => 17.9658056], + ], + 'Cún' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8122974, 'lng' => 18.0678543], + ], + 'Dencsháza' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.993512, 'lng' => 17.8347772], + ], + 'Dinnyeberki' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0972962, 'lng' => 17.9563165], + ], + 'Diósviszló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8774861, 'lng' => 18.1640495], + ], + 'Drávacsehi' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8130167, 'lng' => 18.1666181], + ], + 'Drávacsepely' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8308297, 'lng' => 18.1352308], + ], + 'Drávafok' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8860365, 'lng' => 17.7636317], + ], + 'Drávaiványi' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8470684, 'lng' => 17.8159164], + ], + 'Drávakeresztúr' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8386967, 'lng' => 17.7580104], + ], + 'Drávapalkonya' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8033438, 'lng' => 18.1790753], + ], + 'Drávapiski' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8396577, 'lng' => 18.0989657], + ], + 'Drávaszabolcs' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.803275, 'lng' => 18.2093234], + ], + 'Drávaszerdahely' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8363562, 'lng' => 18.1638527], + ], + 'Drávasztára' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8230964, 'lng' => 17.8220692], + ], + 'Dunaszekcső' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0854783, 'lng' => 18.7542203], + ], + 'Egerág' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9834452, 'lng' => 18.3039561], + ], + 'Egyházasharaszti' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8097356, 'lng' => 18.3314381], + ], + 'Egyházaskozár' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3319023, 'lng' => 18.3178591], + ], + 'Ellend' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0580138, 'lng' => 18.3760682], + ], + 'Endrőc' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9296401, 'lng' => 17.7621758], + ], + 'Erdősmárok' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.055568, 'lng' => 18.5458091], + ], + 'Erdősmecske' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1768439, 'lng' => 18.5109755], + ], + 'Erzsébet' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1004339, 'lng' => 18.4587621], + ], + 'Fazekasboda' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1230108, 'lng' => 18.4850924], + ], + 'Feked' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1626797, 'lng' => 18.5588015], + ], + 'Felsőegerszeg' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2539122, 'lng' => 18.1335751], + ], + 'Felsőszentmárton' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8513101, 'lng' => 17.7034033], + ], + 'Garé' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9180881, 'lng' => 18.1956808], + ], + 'Gerde' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9904428, 'lng' => 18.0255496], + ], + 'Gerényes' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.3070289, 'lng' => 18.1848981], + ], + 'Geresdlak' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1107897, 'lng' => 18.5268599], + ], + 'Gilvánfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9184356, 'lng' => 17.9622098], + ], + 'Gödre' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2899579, 'lng' => 17.9723779], + ], + 'Görcsöny' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9709725, 'lng' => 18.133486], + ], + 'Görcsönydoboka' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0709275, 'lng' => 18.6275109], + ], + 'Gordisa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.7970748, 'lng' => 18.2354868], + ], + 'Gyód' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 45.9979549, 'lng' => 18.1781638], + ], + 'Gyöngyfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9601196, 'lng' => 17.9506649], + ], + 'Gyöngyösmellék' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9868644, 'lng' => 17.7014751], + ], + 'Harkány' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8534053, 'lng' => 18.2348372], + ], + 'Hásságy' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0330172, 'lng' => 18.388848], + ], + 'Hegyhátmaróc' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3109929, 'lng' => 18.3362487], + ], + 'Hegyszentmárton' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9036373, 'lng' => 18.086797], + ], + 'Helesfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0894523, 'lng' => 17.9770167], + ], + 'Hetvehely' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1332155, 'lng' => 18.0432466], + ], + 'Hidas' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2574631, 'lng' => 18.4937015], + ], + 'Himesháza' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0797595, 'lng' => 18.5805933], + ], + 'Hirics' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8247516, 'lng' => 17.9934259], + ], + 'Hobol' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0197823, 'lng' => 17.7724266], + ], + 'Homorúd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.981847, 'lng' => 18.7887766], + ], + 'Horváthertelend' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1751748, 'lng' => 17.9272893], + ], + 'Hosszúhetény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1583167, 'lng' => 18.3520974], + ], + 'Husztót' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1711511, 'lng' => 18.0932139], + ], + 'Ibafa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1552456, 'lng' => 17.9179873], + ], + 'Illocska' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.800591, 'lng' => 18.5233576], + ], + 'Ipacsfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8345382, 'lng' => 18.2055561], + ], + 'Ivánbattyán' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9077809, 'lng' => 18.4176354], + ], + 'Ivándárda' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.831643, 'lng' => 18.5922589], + ], + 'Kacsóta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0390809, 'lng' => 17.9544689], + ], + 'Kákics' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9028359, 'lng' => 17.8568313], + ], + 'Kárász' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2667559, 'lng' => 18.3188548], + ], + 'Kásád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.7793743, 'lng' => 18.3991912], + ], + 'Katádfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9970924, 'lng' => 17.8692171], + ], + 'Kátoly' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0634292, 'lng' => 18.4496796], + ], + 'Kékesd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1007579, 'lng' => 18.4720006], + ], + 'Kémes' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8241919, 'lng' => 18.1031607], + ], + 'Kemse' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8237775, 'lng' => 17.9119613], + ], + 'Keszü' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 46.0160053, 'lng' => 18.1918765], + ], + 'Kétújfalu' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9643465, 'lng' => 17.7128738], + ], + 'Királyegyháza' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9975029, 'lng' => 17.9670799], + ], + 'Kisasszonyfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9467478, 'lng' => 18.0062386], + ], + 'Kisbeszterce' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2054937, 'lng' => 18.033257], + ], + 'Kisbudmér' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9132933, 'lng' => 18.4468642], + ], + 'Kisdér' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9397014, 'lng' => 18.1280256], + ], + 'Kisdobsza' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0279686, 'lng' => 17.654966], + ], + 'Kishajmás' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2000972, 'lng' => 18.0807394], + ], + 'Kisharsány' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8597428, 'lng' => 18.3628602], + ], + 'Kisherend' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9657006, 'lng' => 18.3308199], + ], + 'Kisjakabfalva' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8961294, 'lng' => 18.4347874], + ], + 'Kiskassa' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9532763, 'lng' => 18.3984025], + ], + 'Kislippó' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8309942, 'lng' => 18.5387451], + ], + 'Kisnyárád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0369956, 'lng' => 18.5642298], + ], + 'Kisszentmárton' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8245119, 'lng' => 18.0223384], + ], + 'Kistamási' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0118086, 'lng' => 17.7210893], + ], + 'Kistapolca' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8215113, 'lng' => 18.383003], + ], + 'Kistótfalu' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9080691, 'lng' => 18.3097841], + ], + 'Kisvaszar' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2748571, 'lng' => 18.2126962], + ], + 'Köblény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2948258, 'lng' => 18.303697], + ], + 'Kökény' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 45.9995372, 'lng' => 18.2057648], + ], + 'Kölked' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9489796, 'lng' => 18.7058024], + ], + 'Komló' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.1929788, 'lng' => 18.2512139], + ], + 'Kórós' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8666591, 'lng' => 18.0818986], + ], + 'Kovácshida' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8322528, 'lng' => 18.1852847], + ], + 'Kovácsszénája' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1714525, 'lng' => 18.1099753], + ], + 'Kővágószőlős' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0824433, 'lng' => 18.1242335], + ], + 'Kővágótöttös' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0859181, 'lng' => 18.1005597], + ], + 'Kozármisleny' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0412574, 'lng' => 18.2872228], + ], + 'Lánycsók' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0073964, 'lng' => 18.624077], + ], + 'Lapáncsa' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8187417, 'lng' => 18.4965793], + ], + 'Liget' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2346633, 'lng' => 18.1924669], + ], + 'Lippó' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.863493, 'lng' => 18.5702136], + ], + 'Liptód' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.044203, 'lng' => 18.5153709], + ], + 'Lothárd' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0015129, 'lng' => 18.3534664], + ], + 'Lovászhetény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1573687, 'lng' => 18.4736022], + ], + 'Lúzsok' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8386895, 'lng' => 17.9448893], + ], + 'Mágocs' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3507989, 'lng' => 18.2282954], + ], + 'Magyarbóly' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8424536, 'lng' => 18.4905327], + ], + 'Magyaregregy' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2497645, 'lng' => 18.3080926], + ], + 'Magyarhertelend' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1887919, 'lng' => 18.1496193], + ], + 'Magyarlukafa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1692382, 'lng' => 17.7566367], + ], + 'Magyarmecske' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9444333, 'lng' => 17.963957], + ], + 'Magyarsarlós' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0412482, 'lng' => 18.3527956], + ], + 'Magyarszék' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1966719, 'lng' => 18.1955889], + ], + 'Magyartelek' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9438384, 'lng' => 17.9834231], + ], + 'Majs' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9090894, 'lng' => 18.59764], + ], + 'Mánfa' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.1620219, 'lng' => 18.2424376], + ], + 'Maráza' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0767639, 'lng' => 18.5102704], + ], + 'Márfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8597093, 'lng' => 18.184506], + ], + 'Máriakéménd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0275242, 'lng' => 18.4616888], + ], + 'Markóc' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8633597, 'lng' => 17.7628134], + ], + 'Marócsa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9143499, 'lng' => 17.8155625], + ], + 'Márok' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8776725, 'lng' => 18.5052153], + ], + 'Martonfa' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1162762, 'lng' => 18.373108], + ], + 'Matty' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.7959854, 'lng' => 18.2646823], + ], + 'Máza' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2674701, 'lng' => 18.3987184], + ], + 'Mecseknádasd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.22466, 'lng' => 18.4653855], + ], + 'Mecsekpölöske' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2232838, 'lng' => 18.2117379], + ], + 'Mekényes' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3905907, 'lng' => 18.3338629], + ], + 'Merenye' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.069313, 'lng' => 17.6981454], + ], + 'Meződ' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2898147, 'lng' => 18.1028572], + ], + 'Mindszentgodisa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2270491, 'lng' => 18.070952], + ], + 'Mohács' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0046295, 'lng' => 18.6794304], + ], + 'Molvány' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0294158, 'lng' => 17.7455964], + ], + 'Monyoród' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0115276, 'lng' => 18.4781726], + ], + 'Mozsgó' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1148249, 'lng' => 17.8457585], + ], + 'Nagybudmér' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9378397, 'lng' => 18.4443309], + ], + 'Nagycsány' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.871837, 'lng' => 17.9441308], + ], + 'Nagydobsza' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0290366, 'lng' => 17.6672107], + ], + 'Nagyhajmás' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.372206, 'lng' => 18.2898052], + ], + 'Nagyharsány' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8466947, 'lng' => 18.3947776], + ], + 'Nagykozár' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.067814, 'lng' => 18.316561], + ], + 'Nagynyárád' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9447148, 'lng' => 18.578055], + ], + 'Nagypall' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1474016, 'lng' => 18.4539234], + ], + 'Nagypeterd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0459728, 'lng' => 17.8979423], + ], + 'Nagytótfalu' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8638406, 'lng' => 18.3426767], + ], + 'Nagyváty' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0617075, 'lng' => 17.93209], + ], + 'Nemeske' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.020198, 'lng' => 17.7129695], + ], + 'Nyugotszenterzsébet' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0747959, 'lng' => 17.9096635], + ], + 'Óbánya' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2220338, 'lng' => 18.4084838], + ], + 'Ócsárd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9341296, 'lng' => 18.1533436], + ], + 'Ófalu' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2210918, 'lng' => 18.534029], + ], + 'Okorág' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9262423, 'lng' => 17.8761913], + ], + 'Okorvölgy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.15235, 'lng' => 18.0600392], + ], + 'Olasz' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0128298, 'lng' => 18.4122965], + ], + 'Old' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.7893924, 'lng' => 18.3526547], + ], + 'Orfű' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.1504207, 'lng' => 18.1423992], + ], + 'Oroszló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2201904, 'lng' => 18.122659], + ], + 'Ózdfalu' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9288431, 'lng' => 18.0210679], + ], + 'Palé' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2603608, 'lng' => 18.0690432], + ], + 'Palkonya' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8968607, 'lng' => 18.3899099], + ], + 'Palotabozsok' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1275672, 'lng' => 18.6416844], + ], + 'Páprád' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8927275, 'lng' => 18.0103745], + ], + 'Patapoklosi' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0753051, 'lng' => 17.7415323], + ], + 'Pécs' => [ + 'constituencies' => ['Baranya 2.', 'Baranya 1.'], + 'coordinates' => ['lat' => 46.0727345, 'lng' => 18.232266], + ], + 'Pécsbagota' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9906469, 'lng' => 18.0728758], + ], + 'Pécsdevecser' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9585177, 'lng' => 18.3839237], + ], + 'Pécsudvard' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 46.0108323, 'lng' => 18.2750737], + ], + 'Pécsvárad' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1591341, 'lng' => 18.4185199], + ], + 'Pellérd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.034172, 'lng' => 18.1551531], + ], + 'Pereked' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0940085, 'lng' => 18.3768639], + ], + 'Peterd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9726228, 'lng' => 18.3606704], + ], + 'Pettend' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0001576, 'lng' => 17.7011535], + ], + 'Piskó' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8112973, 'lng' => 17.9384454], + ], + 'Pócsa' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9100922, 'lng' => 18.4699792], + ], + 'Pogány' => [ + 'constituencies' => ['Baranya 1.'], + 'coordinates' => ['lat' => 45.9827333, 'lng' => 18.2568939], + ], + 'Rádfalva' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8598624, 'lng' => 18.1252323], + ], + 'Regenye' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.969783, 'lng' => 18.1685228], + ], + 'Romonya' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0871177, 'lng' => 18.3391112], + ], + 'Rózsafa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0227215, 'lng' => 17.8889708], + ], + 'Sámod' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8536384, 'lng' => 18.0384521], + ], + 'Sárok' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8414254, 'lng' => 18.6119412], + ], + 'Sásd' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2563232, 'lng' => 18.1024778], + ], + 'Sátorhely' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9417452, 'lng' => 18.6330768], + ], + 'Sellye' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.873291, 'lng' => 17.8494986], + ], + 'Siklós' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8555814, 'lng' => 18.2979721], + ], + 'Siklósbodony' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9105251, 'lng' => 18.1202589], + ], + 'Siklósnagyfalu' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.820428, 'lng' => 18.3636246], + ], + 'Somberek' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0812348, 'lng' => 18.6586781], + ], + 'Somogyapáti' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0920041, 'lng' => 17.7506787], + ], + 'Somogyhárságy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1623103, 'lng' => 17.7731873], + ], + 'Somogyhatvan' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1120284, 'lng' => 17.7126553], + ], + 'Somogyviszló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1146313, 'lng' => 17.7636375], + ], + 'Sósvertike' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8340815, 'lng' => 17.8614028], + ], + 'Sumony' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9675435, 'lng' => 17.9146319], + ], + 'Szabadszentkirály' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0059012, 'lng' => 18.0435247], + ], + 'Szágy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2244706, 'lng' => 17.9469817], + ], + 'Szajk' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9921175, 'lng' => 18.5328986], + ], + 'Szalánta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9471908, 'lng' => 18.2376181], + ], + 'Szalatnak' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2903675, 'lng' => 18.2809735], + ], + 'Szaporca' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8135724, 'lng' => 18.1045054], + ], + 'Szárász' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3487743, 'lng' => 18.3727487], + ], + 'Szászvár' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2739639, 'lng' => 18.3774781], + ], + 'Szava' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9024581, 'lng' => 18.1738569], + ], + 'Szebény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1296283, 'lng' => 18.5879918], + ], + 'Szederkény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9986735, 'lng' => 18.4530663], + ], + 'Székelyszabar' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0471326, 'lng' => 18.6012321], + ], + 'Szellő' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.0744167, 'lng' => 18.4609549], + ], + 'Szemely' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.0083381, 'lng' => 18.3256717], + ], + 'Szentdénes' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0079644, 'lng' => 17.9271651], + ], + 'Szentegát' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9754975, 'lng' => 17.8244079], + ], + 'Szentkatalin' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.174384, 'lng' => 18.0505714], + ], + 'Szentlászló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1540417, 'lng' => 17.8331512], + ], + 'Szentlőrinc' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0403123, 'lng' => 17.9897756], + ], + 'Szigetvár' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0487727, 'lng' => 17.7983466], + ], + 'Szilágy' => [ + 'constituencies' => ['Baranya 2.'], + 'coordinates' => ['lat' => 46.1009525, 'lng' => 18.4065405], + ], + 'Szilvás' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9616358, 'lng' => 18.1981701], + ], + 'Szőke' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9604273, 'lng' => 18.1867423], + ], + 'Szőkéd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9645154, 'lng' => 18.2884592], + ], + 'Szörény' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9683861, 'lng' => 17.6819713], + ], + 'Szulimán' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1264433, 'lng' => 17.805449], + ], + 'Szűr' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.099254, 'lng' => 18.5809615], + ], + 'Tarrós' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2806564, 'lng' => 18.1425225], + ], + 'Tékes' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2866262, 'lng' => 18.1744149], + ], + 'Teklafalu' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9493136, 'lng' => 17.7287585], + ], + 'Tengeri' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9263477, 'lng' => 18.087938], + ], + 'Tésenfa' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8127763, 'lng' => 18.1178921], + ], + 'Téseny' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9515499, 'lng' => 18.0479966], + ], + 'Tófű' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.3094872, 'lng' => 18.3576794], + ], + 'Tormás' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2309543, 'lng' => 17.9937201], + ], + 'Tótszentgyörgy' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0521798, 'lng' => 17.7178541], + ], + 'Töttös' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9150433, 'lng' => 18.5407584], + ], + 'Túrony' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9054082, 'lng' => 18.2309533], + ], + 'Udvar' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.900472, 'lng' => 18.6594842], + ], + 'Újpetre' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.934779, 'lng' => 18.3636323], + ], + 'Vajszló' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8592442, 'lng' => 17.9868205], + ], + 'Várad' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9743574, 'lng' => 17.7456586], + ], + 'Varga' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2475508, 'lng' => 18.1424694], + ], + 'Vásárosbéc' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.1825351, 'lng' => 17.7246441], + ], + 'Vásárosdombó' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.3064752, 'lng' => 18.1334675], + ], + 'Vázsnok' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.2653395, 'lng' => 18.1253751], + ], + 'Vejti' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8096089, 'lng' => 17.9682522], + ], + 'Vékény' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.2695945, 'lng' => 18.3423454], + ], + 'Velény' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9807601, 'lng' => 18.0514344], + ], + 'Véménd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1551161, 'lng' => 18.6190866], + ], + 'Versend' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9953039, 'lng' => 18.5115869], + ], + 'Villány' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8700399, 'lng' => 18.453201], + ], + 'Villánykövesd' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.8823189, 'lng' => 18.425812], + ], + 'Vokány' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 45.9133714, 'lng' => 18.3364685], + ], + 'Zádor' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.9623692, 'lng' => 17.6579278], + ], + 'Zaláta' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 45.8111976, 'lng' => 17.8901202], + ], + 'Zengővárkony' => [ + 'constituencies' => ['Baranya 3.'], + 'coordinates' => ['lat' => 46.1728638, 'lng' => 18.4320077], + ], + 'Zók' => [ + 'constituencies' => ['Baranya 4.'], + 'coordinates' => ['lat' => 46.0104261, 'lng' => 18.0965422], + ], + ], + 'Békés' => [ + 'Almáskamarás' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4617785, 'lng' => 21.092448], + ], + 'Battonya' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.2902462, 'lng' => 21.0199215], + ], + 'Békés' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.6704899, 'lng' => 21.0434996], + ], + 'Békéscsaba' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.6735939, 'lng' => 21.0877309], + ], + 'Békéssámson' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4208677, 'lng' => 20.6176498], + ], + 'Békésszentandrás' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8715996, 'lng' => 20.48336], + ], + 'Bélmegyer' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8726019, 'lng' => 21.1832832], + ], + 'Biharugra' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.9691009, 'lng' => 21.5987651], + ], + 'Bucsa' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 47.2047017, 'lng' => 20.9970391], + ], + 'Csabacsűd' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8244161, 'lng' => 20.6485242], + ], + 'Csabaszabadi' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.574811, 'lng' => 20.951145], + ], + 'Csanádapáca' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5409397, 'lng' => 20.8852553], + ], + 'Csárdaszállás' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8647568, 'lng' => 20.9374853], + ], + 'Csorvás' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.6308376, 'lng' => 20.8340929], + ], + 'Dévaványa' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 47.0313217, 'lng' => 20.9595443], + ], + 'Doboz' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.7343152, 'lng' => 21.2420659], + ], + 'Dombegyház' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3415879, 'lng' => 21.1342664], + ], + 'Dombiratos' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4195218, 'lng' => 21.1178789], + ], + 'Ecsegfalva' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 47.14789, 'lng' => 20.9239261], + ], + 'Elek' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.5291929, 'lng' => 21.2487556], + ], + 'Füzesgyarmat' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 47.1051107, 'lng' => 21.2108329], + ], + 'Gádoros' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.6667476, 'lng' => 20.5961159], + ], + 'Gerendás' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.5969212, 'lng' => 20.8593687], + ], + 'Geszt' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8831763, 'lng' => 21.5794915], + ], + 'Gyomaendrőd' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.9317797, 'lng' => 20.8113125], + ], + 'Gyula' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.6473027, 'lng' => 21.2784255], + ], + 'Hunya' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.812869, 'lng' => 20.8458337], + ], + 'Kamut' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.7619186, 'lng' => 20.9798143], + ], + 'Kardos' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.7941712, 'lng' => 20.715629], + ], + 'Kardoskút' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.498573, 'lng' => 20.7040158], + ], + 'Kaszaper' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4598817, 'lng' => 20.8251944], + ], + 'Kertészsziget' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 47.1542945, 'lng' => 21.0610234], + ], + 'Kétegyháza' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.5417887, 'lng' => 21.1810736], + ], + 'Kétsoprony' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.7208319, 'lng' => 20.8870273], + ], + 'Kevermes' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4167579, 'lng' => 21.1818484], + ], + 'Kisdombegyház' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3693244, 'lng' => 21.0996778], + ], + 'Kondoros' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.7574628, 'lng' => 20.7972363], + ], + 'Körösladány' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.9607513, 'lng' => 21.0767574], + ], + 'Körösnagyharsány' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 47.0080391, 'lng' => 21.6417355], + ], + 'Köröstarcsa' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8780314, 'lng' => 21.02402], + ], + 'Körösújfalu' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.9659419, 'lng' => 21.3988486], + ], + 'Kötegyán' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.738284, 'lng' => 21.481692], + ], + 'Kunágota' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4234015, 'lng' => 21.0467553], + ], + 'Lőkösháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4297019, 'lng' => 21.2318793], + ], + 'Magyarbánhegyes' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4577279, 'lng' => 20.968734], + ], + 'Magyardombegyház' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3794548, 'lng' => 21.0743712], + ], + 'Medgyesbodzás' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5186797, 'lng' => 20.9596371], + ], + 'Medgyesegyháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4967576, 'lng' => 21.0271996], + ], + 'Méhkerék' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.7735176, 'lng' => 21.4435935], + ], + 'Mezőberény' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.825687, 'lng' => 21.0243614], + ], + 'Mezőgyán' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8709809, 'lng' => 21.5257366], + ], + 'Mezőhegyes' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3172449, 'lng' => 20.8173892], + ], + 'Mezőkovácsháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4093003, 'lng' => 20.9112692], + ], + 'Murony' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.760463, 'lng' => 21.0411739], + ], + 'Nagybánhegyes' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.460095, 'lng' => 20.902578], + ], + 'Nagykamarás' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4727168, 'lng' => 21.1213871], + ], + 'Nagyszénás' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.6722161, 'lng' => 20.6734381], + ], + 'Okány' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8982798, 'lng' => 21.3467384], + ], + 'Örménykút' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.830573, 'lng' => 20.7344497], + ], + 'Orosháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5684222, 'lng' => 20.6544927], + ], + 'Pusztaföldvár' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5251751, 'lng' => 20.8024526], + ], + 'Pusztaottlaka' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.5386606, 'lng' => 21.0060316], + ], + 'Sarkad' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.7374245, 'lng' => 21.3810771], + ], + 'Sarkadkeresztúr' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8107081, 'lng' => 21.3841932], + ], + 'Szabadkígyós' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.601522, 'lng' => 21.0753003], + ], + 'Szarvas' => [ + 'constituencies' => ['Békés 2.'], + 'coordinates' => ['lat' => 46.8635641, 'lng' => 20.5526535], + ], + 'Szeghalom' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 47.0239347, 'lng' => 21.1666571], + ], + 'Tarhos' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8132012, 'lng' => 21.2109597], + ], + 'Telekgerendás' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.6566167, 'lng' => 20.9496242], + ], + 'Tótkomlós' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.4107596, 'lng' => 20.7363644], + ], + 'Újkígyós' => [ + 'constituencies' => ['Békés 1.'], + 'coordinates' => ['lat' => 46.5899757, 'lng' => 21.0242728], + ], + 'Újszalonta' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.8128247, 'lng' => 21.4908762], + ], + 'Végegyháza' => [ + 'constituencies' => ['Békés 4.'], + 'coordinates' => ['lat' => 46.3882623, 'lng' => 20.8699923], + ], + 'Vésztő' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.9244546, 'lng' => 21.2628502], + ], + 'Zsadány' => [ + 'constituencies' => ['Békés 3.'], + 'coordinates' => ['lat' => 46.9230248, 'lng' => 21.4873156], + ], + ], + 'Borsod-Abaúj-Zemplén' => [ + 'Abaújalpár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3065157, 'lng' => 21.232147], + ], + 'Abaújkér' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3033478, 'lng' => 21.2013068], + ], + 'Abaújlak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4051818, 'lng' => 20.9548056], + ], + 'Abaújszántó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2792184, 'lng' => 21.1874523], + ], + 'Abaújszolnok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3730791, 'lng' => 20.9749255], + ], + 'Abaújvár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5266538, 'lng' => 21.3150208], + ], + 'Abod' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3928646, 'lng' => 20.7923344], + ], + 'Aggtelek' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4686657, 'lng' => 20.5040699], + ], + 'Alacska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2157484, 'lng' => 20.6502945], + ], + 'Alsóberecki' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3437614, 'lng' => 21.6905164], + ], + 'Alsódobsza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1799523, 'lng' => 21.0026817], + ], + 'Alsógagy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4052855, 'lng' => 21.0255485], + ], + 'Alsóregmec' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4634336, 'lng' => 21.6181953], + ], + 'Alsószuha' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3726027, 'lng' => 20.5044038], + ], + 'Alsótelekes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4105212, 'lng' => 20.6547156], + ], + 'Alsóvadász' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2401438, 'lng' => 20.9043765], + ], + 'Alsózsolca' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.0748263, 'lng' => 20.8850624], + ], + 'Arka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3562385, 'lng' => 21.252529], + ], + 'Arló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1746548, 'lng' => 20.2560308], + ], + 'Arnót' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.1319962, 'lng' => 20.859401], + ], + 'Ároktő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7284812, 'lng' => 20.9423131], + ], + 'Aszaló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2177554, 'lng' => 20.9624804], + ], + 'Baktakék' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3675199, 'lng' => 21.0288911], + ], + 'Balajt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3210349, 'lng' => 20.7866111], + ], + 'Bánhorváti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2260139, 'lng' => 20.504815], + ], + 'Bánréve' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2986902, 'lng' => 20.3560194], + ], + 'Baskó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3326787, 'lng' => 21.336418], + ], + 'Becskeháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5294979, 'lng' => 20.8354743], + ], + 'Bekecs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1534102, 'lng' => 21.1762263], + ], + 'Berente' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2385836, 'lng' => 20.6700776], + ], + 'Beret' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3458722, 'lng' => 21.0235103], + ], + 'Berzék' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0240535, 'lng' => 20.9528886], + ], + 'Bőcs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0442332, 'lng' => 20.9683874], + ], + 'Bodroghalom' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3009977, 'lng' => 21.707044], + ], + 'Bodrogkeresztúr' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1630176, 'lng' => 21.3595899], + ], + 'Bodrogkisfalud' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1789303, 'lng' => 21.3617788], + ], + 'Bodrogolaszi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2867085, 'lng' => 21.5160527], + ], + 'Bódvalenke' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5424028, 'lng' => 20.8041838], + ], + 'Bódvarákó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5111514, 'lng' => 20.7358047], + ], + 'Bódvaszilas' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5377629, 'lng' => 20.7312757], + ], + 'Bogács' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9030764, 'lng' => 20.5312356], + ], + 'Boldogkőújfalu' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3193629, 'lng' => 21.242022], + ], + 'Boldogkőváralja' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3380634, 'lng' => 21.2367554], + ], + 'Boldva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.218091, 'lng' => 20.7886144], + ], + 'Borsodbóta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2121829, 'lng' => 20.3960602], + ], + 'Borsodgeszt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9559428, 'lng' => 20.6944004], + ], + 'Borsodivánka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.701045, 'lng' => 20.6547148], + ], + 'Borsodnádasd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1191717, 'lng' => 20.2529566], + ], + 'Borsodszentgyörgy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1892068, 'lng' => 20.2073894], + ], + 'Borsodszirák' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2610318, 'lng' => 20.7676252], + ], + 'Bózsva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4743356, 'lng' => 21.468268], + ], + 'Bükkábrány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8884157, 'lng' => 20.6810544], + ], + 'Bükkaranyos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9866329, 'lng' => 20.7794609], + ], + 'Bükkmogyorósd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1291531, 'lng' => 20.3563552], + ], + 'Bükkszentkereszt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0668164, 'lng' => 20.6324773], + ], + 'Bükkzsérc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9587559, 'lng' => 20.5025627], + ], + 'Büttös' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4783127, 'lng' => 21.0110122], + ], + 'Cigánd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2558937, 'lng' => 21.8889241], + ], + 'Csenyéte' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4345165, 'lng' => 21.0412334], + ], + 'Cserépfalu' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9413093, 'lng' => 20.5347083], + ], + 'Cserépváralja' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9325883, 'lng' => 20.5598918], + ], + 'Csernely' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1438586, 'lng' => 20.3390005], + ], + 'Csincse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8883234, 'lng' => 20.768705], + ], + 'Csobád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2796877, 'lng' => 21.0269782], + ], + 'Csobaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0485163, 'lng' => 21.3382189], + ], + 'Csokvaomány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1666711, 'lng' => 20.3744746], + ], + 'Damak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3168034, 'lng' => 20.8216124], + ], + 'Dámóc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3748294, 'lng' => 22.0336128], + ], + 'Debréte' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.5000066, 'lng' => 20.8661035], + ], + 'Dédestapolcsány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1804582, 'lng' => 20.4850166], + ], + 'Detek' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3336841, 'lng' => 21.0176305], + ], + 'Domaháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1836193, 'lng' => 20.1055583], + ], + 'Dövény' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3469512, 'lng' => 20.5431344], + ], + 'Dubicsány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2837745, 'lng' => 20.4940325], + ], + 'Edelény' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2934391, 'lng' => 20.7385817], + ], + 'Egerlövő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7203221, 'lng' => 20.6175935], + ], + 'Égerszög' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.442896, 'lng' => 20.5875195], + ], + 'Emőd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9380038, 'lng' => 20.8154444], + ], + 'Encs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3259442, 'lng' => 21.1133006], + ], + 'Erdőbénye' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2662769, 'lng' => 21.3547995], + ], + 'Erdőhorváti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3158739, 'lng' => 21.4272709], + ], + 'Fáj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4219028, 'lng' => 21.0747972], + ], + 'Fancsal' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3552347, 'lng' => 21.064671], + ], + 'Farkaslyuk' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1876627, 'lng' => 20.3086509], + ], + 'Felsőberecki' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3595718, 'lng' => 21.6950761], + ], + 'Felsődobsza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2555859, 'lng' => 21.0764245], + ], + 'Felsőgagy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4289932, 'lng' => 21.0128468], + ], + 'Felsőkelecsény' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3600051, 'lng' => 20.5939689], + ], + 'Felsőnyárád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3299583, 'lng' => 20.5995966], + ], + 'Felsőregmec' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4915243, 'lng' => 21.6056225], + ], + 'Felsőtelekes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4058831, 'lng' => 20.6352386], + ], + 'Felsővadász' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3709811, 'lng' => 20.9195765], + ], + 'Felsőzsolca' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.1041265, 'lng' => 20.8595396], + ], + 'Filkeháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4960919, 'lng' => 21.4888024], + ], + 'Fony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3910341, 'lng' => 21.2865504], + ], + 'Forró' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3233535, 'lng' => 21.0880493], + ], + 'Fulókércs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4308674, 'lng' => 21.1049891], + ], + 'Füzér' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.539654, 'lng' => 21.4547936], + ], + 'Füzérkajata' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5182556, 'lng' => 21.5000318], + ], + 'Füzérkomlós' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5126205, 'lng' => 21.4532344], + ], + 'Füzérradvány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.483741, 'lng' => 21.530474], + ], + 'Gadna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4006289, 'lng' => 20.9296444], + ], + 'Gagyapáti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.409096, 'lng' => 21.0017182], + ], + 'Gagybátor' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.433303, 'lng' => 20.94859], + ], + 'Gagyvendégi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4285166, 'lng' => 20.972405], + ], + 'Galvács' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4190767, 'lng' => 20.7767621], + ], + 'Garadna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4174625, 'lng' => 21.17463], + ], + 'Gelej' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.828655, 'lng' => 20.7755503], + ], + 'Gesztely' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1026673, 'lng' => 20.9654647], + ], + 'Gibárt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3153245, 'lng' => 21.1603909], + ], + 'Girincs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9691368, 'lng' => 20.9846965], + ], + 'Golop' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2374312, 'lng' => 21.1893372], + ], + 'Gömörszőlős' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3730427, 'lng' => 20.4276758], + ], + 'Gönc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4727097, 'lng' => 21.2735417], + ], + 'Göncruszka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4488786, 'lng' => 21.239774], + ], + 'Györgytarló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2053902, 'lng' => 21.6316333], + ], + 'Halmaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2464584, 'lng' => 20.9983349], + ], + 'Hangács' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2896949, 'lng' => 20.8314625], + ], + 'Hangony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2290868, 'lng' => 20.198029], + ], + 'Háromhuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3780662, 'lng' => 21.4283347], + ], + 'Harsány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9679177, 'lng' => 20.7418041], + ], + 'Hegymeg' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3314259, 'lng' => 20.8614048], + ], + 'Hejce' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4234865, 'lng' => 21.2816978], + ], + 'Hejőbába' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9059201, 'lng' => 20.9452436], + ], + 'Hejőkeresztúr' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9610209, 'lng' => 20.8772681], + ], + 'Hejőkürt' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8564708, 'lng' => 20.9930661], + ], + 'Hejőpapi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8972354, 'lng' => 20.9054713], + ], + 'Hejőszalonta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9388389, 'lng' => 20.8822344], + ], + 'Hercegkút' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3340476, 'lng' => 21.5301233], + ], + 'Hernádbűd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2966038, 'lng' => 21.137896], + ], + 'Hernádcéce' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3587807, 'lng' => 21.1976117], + ], + 'Hernádkak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0892117, 'lng' => 20.9635617], + ], + 'Hernádkércs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2420151, 'lng' => 21.0501362], + ], + 'Hernádnémeti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0716822, 'lng' => 20.9742345], + ], + 'Hernádpetri' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4815086, 'lng' => 21.1622472], + ], + 'Hernádszentandrás' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2890724, 'lng' => 21.0949074], + ], + 'Hernádszurdok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.48169, 'lng' => 21.2071561], + ], + 'Hernádvécse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4406714, 'lng' => 21.1687099], + ], + 'Hét' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.282992, 'lng' => 20.3875674], + ], + 'Hidasnémeti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5029778, 'lng' => 21.2293013], + ], + 'Hidvégardó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5598883, 'lng' => 20.8395348], + ], + 'Hollóháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5393716, 'lng' => 21.4144474], + ], + 'Homrogd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2834505, 'lng' => 20.9125329], + ], + 'Igrici' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8673926, 'lng' => 20.8831705], + ], + 'Imola' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4201572, 'lng' => 20.5516409], + ], + 'Ináncs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2861362, 'lng' => 21.0681971], + ], + 'Irota' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3964482, 'lng' => 20.8752667], + ], + 'Izsófalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3087892, 'lng' => 20.6536072], + ], + 'Jákfalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3316408, 'lng' => 20.569496], + ], + 'Járdánháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1551033, 'lng' => 20.2477262], + ], + 'Jósvafő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4826254, 'lng' => 20.5504479], + ], + 'Kács' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9574786, 'lng' => 20.6145847], + ], + 'Kánó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4276397, 'lng' => 20.5991681], + ], + 'Kány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.5151651, 'lng' => 21.0143542], + ], + 'Karcsa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3131571, 'lng' => 21.7953512], + ], + 'Karos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3312141, 'lng' => 21.7406654], + ], + 'Kazincbarcika' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2489437, 'lng' => 20.6189771], + ], + 'Kázsmárk' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2728658, 'lng' => 20.9760294], + ], + 'Kéked' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5447244, 'lng' => 21.3500526], + ], + 'Kelemér' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3551802, 'lng' => 20.4296357], + ], + 'Kenézlő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2004193, 'lng' => 21.5311235], + ], + 'Keresztéte' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4989547, 'lng' => 20.950696], + ], + 'Kesznyéten' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9694339, 'lng' => 21.0413905], + ], + 'Királd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2393694, 'lng' => 20.3764361], + ], + 'Kiscsécs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9678112, 'lng' => 21.011133], + ], + 'Kisgyőr' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0096251, 'lng' => 20.6874073], + ], + 'Kishuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4503449, 'lng' => 21.4814089], + ], + 'Kiskinizs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2508135, 'lng' => 21.0345918], + ], + 'Kisrozvágy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3491303, 'lng' => 21.9390758], + ], + 'Kissikátor' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1946631, 'lng' => 20.1302306], + ], + 'Kistokaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0397115, 'lng' => 20.8410079], + ], + 'Komjáti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5452009, 'lng' => 20.7618268], + ], + 'Komlóska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3404486, 'lng' => 21.4622875], + ], + 'Kondó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1880491, 'lng' => 20.6438586], + ], + 'Korlát' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3779667, 'lng' => 21.2457327], + ], + 'Köröm' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9842491, 'lng' => 20.9545886], + ], + 'Kovácsvágás' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.45352, 'lng' => 21.5283164], + ], + 'Krasznokvajda' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4705256, 'lng' => 20.9714153], + ], + 'Kupa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3316226, 'lng' => 20.9145594], + ], + 'Kurityán' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.310505, 'lng' => 20.62573], + ], + 'Lácacséke' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3664002, 'lng' => 21.9934562], + ], + 'Ládbesenyő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3432268, 'lng' => 20.7859308], + ], + 'Lak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3480907, 'lng' => 20.8662135], + ], + 'Legyesbénye' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1564545, 'lng' => 21.1530692], + ], + 'Léh' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2906948, 'lng' => 20.9807054], + ], + 'Lénárddaróc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1486722, 'lng' => 20.3728301], + ], + 'Litka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4544802, 'lng' => 21.0584273], + ], + 'Mád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1922445, 'lng' => 21.2759773], + ], + 'Makkoshotyka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3571928, 'lng' => 21.5164187], + ], + 'Mályi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0175678, 'lng' => 20.8292414], + ], + 'Mályinka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1545567, 'lng' => 20.4958901], + ], + 'Martonyi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4702379, 'lng' => 20.7660532], + ], + 'Megyaszó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1875185, 'lng' => 21.0547033], + ], + 'Méra' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3565901, 'lng' => 21.1469291], + ], + 'Meszes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.438651, 'lng' => 20.7950688], + ], + 'Mezőcsát' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8207081, 'lng' => 20.9051607], + ], + 'Mezőkeresztes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8262301, 'lng' => 20.6884043], + ], + 'Mezőkövesd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8074617, 'lng' => 20.5698525], + ], + 'Mezőnagymihály' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8062776, 'lng' => 20.7308177], + ], + 'Mezőnyárád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8585625, 'lng' => 20.6764688], + ], + 'Mezőzombor' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1501209, 'lng' => 21.2575954], + ], + 'Mikóháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4617944, 'lng' => 21.592572], + ], + 'Miskolc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.', 'Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1034775, 'lng' => 20.7784384], + ], + 'Mogyoróska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3759799, 'lng' => 21.3296401], + ], + 'Monaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3061021, 'lng' => 20.9348205], + ], + 'Monok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2099439, 'lng' => 21.149252], + ], + 'Múcsony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2758139, 'lng' => 20.6716209], + ], + 'Muhi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9778997, 'lng' => 20.9293321], + ], + 'Nagybarca' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2476865, 'lng' => 20.5280319], + ], + 'Nagycsécs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9601505, 'lng' => 20.9482798], + ], + 'Nagyhuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4290026, 'lng' => 21.492424], + ], + 'Nagykinizs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2344766, 'lng' => 21.0335706], + ], + 'Nagyrozvágy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3404683, 'lng' => 21.9228458], + ], + 'Négyes' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7013, 'lng' => 20.7040224], + ], + 'Nekézseny' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1689694, 'lng' => 20.4291357], + ], + 'Nemesbikk' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8876867, 'lng' => 20.9661155], + ], + 'Novajidrány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.396674, 'lng' => 21.1688256], + ], + 'Nyékládháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9933002, 'lng' => 20.8429935], + ], + 'Nyésta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3702622, 'lng' => 20.9514276], + ], + 'Nyíri' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4986982, 'lng' => 21.440883], + ], + 'Nyomár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.275559, 'lng' => 20.8198353], + ], + 'Olaszliszka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2419377, 'lng' => 21.4279754], + ], + 'Onga' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1194769, 'lng' => 20.9065655], + ], + 'Ónod' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0024425, 'lng' => 20.9146535], + ], + 'Ormosbánya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3322064, 'lng' => 20.6493181], + ], + 'Oszlár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.8740321, 'lng' => 21.0332202], + ], + 'Ózd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2241439, 'lng' => 20.2888698], + ], + 'Pácin' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3306334, 'lng' => 21.8337743], + ], + 'Pálháza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4717353, 'lng' => 21.507078], + ], + 'Pamlény' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.493024, 'lng' => 20.9282949], + ], + 'Pányok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5298401, 'lng' => 21.3478472], + ], + 'Parasznya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1688229, 'lng' => 20.6402064], + ], + 'Pere' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2845544, 'lng' => 21.1211586], + ], + 'Perecse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.5027869, 'lng' => 20.9845634], + ], + 'Perkupa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4712725, 'lng' => 20.6862819], + ], + 'Prügy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0824191, 'lng' => 21.2428751], + ], + 'Pusztafalu' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5439277, 'lng' => 21.4860599], + ], + 'Pusztaradvány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4679248, 'lng' => 21.1338715], + ], + 'Putnok' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2939007, 'lng' => 20.4333508], + ], + 'Radostyán' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1787774, 'lng' => 20.6532017], + ], + 'Ragály' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4041753, 'lng' => 20.5211463], + ], + 'Rakaca' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4617206, 'lng' => 20.8848555], + ], + 'Rakacaszend' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4611034, 'lng' => 20.8378744], + ], + 'Rásonysápberencs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.304802, 'lng' => 20.9934828], + ], + 'Rátka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2156932, 'lng' => 21.2267141], + ], + 'Regéc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.392191, 'lng' => 21.3436481], + ], + 'Répáshuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0507939, 'lng' => 20.5254934], + ], + 'Révleányvár' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3230427, 'lng' => 22.0416695], + ], + 'Ricse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3251432, 'lng' => 21.9687588], + ], + 'Rudabánya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3747405, 'lng' => 20.6206118], + ], + 'Rudolftelep' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3092868, 'lng' => 20.6711602], + ], + 'Sajóbábony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1742691, 'lng' => 20.734572], + ], + 'Sajóecseg' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.190065, 'lng' => 20.772827], + ], + 'Sajógalgóc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2929878, 'lng' => 20.5323886], + ], + 'Sajóhídvég' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0026817, 'lng' => 20.9495863], + ], + 'Sajóivánka' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2654174, 'lng' => 20.5799268], + ], + 'Sajókápolna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1952827, 'lng' => 20.6848853], + ], + 'Sajókaza' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2864119, 'lng' => 20.5851277], + ], + 'Sajókeresztúr' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.1694996, 'lng' => 20.7768886], + ], + 'Sajólád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0402765, 'lng' => 20.9024513], + ], + 'Sajólászlófalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1848765, 'lng' => 20.6736002], + ], + 'Sajómercse' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2461305, 'lng' => 20.414773], + ], + 'Sajónémeti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.270659, 'lng' => 20.3811845], + ], + 'Sajóörös' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9515653, 'lng' => 21.0219599], + ], + 'Sajópálfala' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.163139, 'lng' => 20.8458093], + ], + 'Sajópetri' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 48.0351497, 'lng' => 20.8878767], + ], + 'Sajópüspöki' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.280186, 'lng' => 20.3400614], + ], + 'Sajósenye' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1960682, 'lng' => 20.8185281], + ], + 'Sajószentpéter' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2188772, 'lng' => 20.7092248], + ], + 'Sajószöged' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9458004, 'lng' => 20.9946112], + ], + 'Sajóvámos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1802021, 'lng' => 20.8298154], + ], + 'Sajóvelezd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2714818, 'lng' => 20.4593985], + ], + 'Sály' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9527979, 'lng' => 20.6597197], + ], + 'Sárazsadány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2684871, 'lng' => 21.497789], + ], + 'Sárospatak' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3196929, 'lng' => 21.5687308], + ], + 'Sáta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.1876567, 'lng' => 20.3914051], + ], + 'Sátoraljaújhely' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3960601, 'lng' => 21.6551122], + ], + 'Selyeb' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3381582, 'lng' => 20.9541317], + ], + 'Semjén' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3521396, 'lng' => 21.9671011], + ], + 'Serényfalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3071589, 'lng' => 20.3852844], + ], + 'Sima' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2996969, 'lng' => 21.3030527], + ], + 'Sóstófalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.156243, 'lng' => 20.9870638], + ], + 'Szakácsi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3820531, 'lng' => 20.8614571], + ], + 'Szakáld' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9431182, 'lng' => 20.908997], + ], + 'Szalaszend' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3859709, 'lng' => 21.1243501], + ], + 'Szalonna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4500484, 'lng' => 20.7394926], + ], + 'Szászfa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4704359, 'lng' => 20.9418168], + ], + 'Szegi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.1953737, 'lng' => 21.3795562], + ], + 'Szegilong' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2162488, 'lng' => 21.3965639], + ], + 'Szemere' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4661495, 'lng' => 21.099542], + ], + 'Szendrő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4046962, 'lng' => 20.7282046], + ], + 'Szendrőlád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3433366, 'lng' => 20.7419436], + ], + 'Szentistván' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7737632, 'lng' => 20.6579694], + ], + 'Szentistvánbaksa' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.2227558, 'lng' => 21.0276456], + ], + 'Szerencs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1590429, 'lng' => 21.2048872], + ], + 'Szikszó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1989312, 'lng' => 20.9298039], + ], + 'Szin' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4972791, 'lng' => 20.6601922], + ], + 'Szinpetri' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4847097, 'lng' => 20.625043], + ], + 'Szirmabesenyő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 1.'], + 'coordinates' => ['lat' => 48.1509585, 'lng' => 20.7957903], + ], + 'Szögliget' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5215045, 'lng' => 20.6770697], + ], + 'Szőlősardó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.443484, 'lng' => 20.6278686], + ], + 'Szomolya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8919105, 'lng' => 20.4949334], + ], + 'Szuhafő' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4082703, 'lng' => 20.4515974], + ], + 'Szuhakálló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2835218, 'lng' => 20.6523991], + ], + 'Szuhogy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3842029, 'lng' => 20.6731282], + ], + 'Taktabáj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0621903, 'lng' => 21.3112131], + ], + 'Taktaharkány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0876121, 'lng' => 21.129918], + ], + 'Taktakenéz' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0508677, 'lng' => 21.2167146], + ], + 'Taktaszada' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1103437, 'lng' => 21.1735733], + ], + 'Tállya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2352295, 'lng' => 21.2260996], + ], + 'Tarcal' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1311328, 'lng' => 21.3418021], + ], + 'Tard' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8784711, 'lng' => 20.598937], + ], + 'Tardona' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.1699442, 'lng' => 20.531454], + ], + 'Telkibánya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4854061, 'lng' => 21.3574907], + ], + 'Teresztenye' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4463436, 'lng' => 20.6031689], + ], + 'Tibolddaróc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9206758, 'lng' => 20.6355357], + ], + 'Tiszabábolna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.689752, 'lng' => 20.813906], + ], + 'Tiszacsermely' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2336812, 'lng' => 21.7945686], + ], + 'Tiszadorogma' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.6839826, 'lng' => 20.8661184], + ], + 'Tiszakarád' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2061184, 'lng' => 21.7213149], + ], + 'Tiszakeszi' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.7879554, 'lng' => 20.9904672], + ], + 'Tiszaladány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0621067, 'lng' => 21.4101619], + ], + 'Tiszalúc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0358262, 'lng' => 21.0648204], + ], + 'Tiszapalkonya' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.8849204, 'lng' => 21.0557818], + ], + 'Tiszatardos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.0406385, 'lng' => 21.379655], + ], + 'Tiszatarján' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.8329217, 'lng' => 21.0014346], + ], + 'Tiszaújváros' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 47.9159846, 'lng' => 21.0427447], + ], + 'Tiszavalk' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.6888504, 'lng' => 20.751499], + ], + 'Tokaj' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1172148, 'lng' => 21.4089015], + ], + 'Tolcsva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2841513, 'lng' => 21.4488452], + ], + 'Tomor' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.3258904, 'lng' => 20.8823733], + ], + 'Tornabarakony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4922432, 'lng' => 20.8192157], + ], + 'Tornakápolna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4616855, 'lng' => 20.617706], + ], + 'Tornanádaska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5611186, 'lng' => 20.7846392], + ], + 'Tornaszentandrás' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.5226438, 'lng' => 20.7790226], + ], + 'Tornaszentjakab' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.5244312, 'lng' => 20.8729813], + ], + 'Tornyosnémeti' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.5202757, 'lng' => 21.2506927], + ], + 'Trizs' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4251253, 'lng' => 20.4958645], + ], + 'Újcsanálos' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 6.'], + 'coordinates' => ['lat' => 48.1380468, 'lng' => 21.0036907], + ], + 'Uppony' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.2155013, 'lng' => 20.434654], + ], + 'Vadna' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2733247, 'lng' => 20.5552218], + ], + 'Vágáshuta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4264605, 'lng' => 21.545222], + ], + 'Vajdácska' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3196383, 'lng' => 21.6541401], + ], + 'Vámosújfalu' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2575496, 'lng' => 21.4524394], + ], + 'Varbó' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 2.'], + 'coordinates' => ['lat' => 48.1631678, 'lng' => 20.6217693], + ], + 'Varbóc' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.4644075, 'lng' => 20.6450152], + ], + 'Vatta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 7.'], + 'coordinates' => ['lat' => 47.9228447, 'lng' => 20.7389995], + ], + 'Vilmány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4166062, 'lng' => 21.2302229], + ], + 'Vilyvitány' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4952223, 'lng' => 21.5589737], + ], + 'Viss' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.2176861, 'lng' => 21.5069652], + ], + 'Viszló' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.4939386, 'lng' => 20.8862569], + ], + 'Vizsoly' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.3845496, 'lng' => 21.2158416], + ], + 'Zádorfalva' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3860789, 'lng' => 20.4852484], + ], + 'Zalkod' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.1857296, 'lng' => 21.4592752], + ], + 'Zemplénagárd' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.36024, 'lng' => 22.0709646], + ], + 'Ziliz' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 4.'], + 'coordinates' => ['lat' => 48.2511796, 'lng' => 20.7922106], + ], + 'Zsujta' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 5.'], + 'coordinates' => ['lat' => 48.4997896, 'lng' => 21.2789138], + ], + 'Zubogy' => [ + 'constituencies' => ['Borsod-Abaúj-Zemplén 3.'], + 'coordinates' => ['lat' => 48.3792388, 'lng' => 20.5758141], + ], + ], + 'Budapest' => [ + 'Budapest I. ker.' => [ + 'constituencies' => ['Budapest 01.'], + 'coordinates' => ['lat' => 47.4968219, 'lng' => 19.037458], + ], + 'Budapest II. ker.' => [ + 'constituencies' => ['Budapest 03.', 'Budapest 04.'], + 'coordinates' => ['lat' => 47.5393329, 'lng' => 18.986934], + ], + 'Budapest III. ker.' => [ + 'constituencies' => ['Budapest 04.', 'Budapest 10.'], + 'coordinates' => ['lat' => 47.5671768, 'lng' => 19.0368517], + ], + 'Budapest IV. ker.' => [ + 'constituencies' => ['Budapest 11.', 'Budapest 12.'], + 'coordinates' => ['lat' => 47.5648915, 'lng' => 19.0913149], + ], + 'Budapest V. ker.' => [ + 'constituencies' => ['Budapest 01.'], + 'coordinates' => ['lat' => 47.5002319, 'lng' => 19.0520181], + ], + 'Budapest VI. ker.' => [ + 'constituencies' => ['Budapest 05.'], + 'coordinates' => ['lat' => 47.509863, 'lng' => 19.0625813], + ], + 'Budapest VII. ker.' => [ + 'constituencies' => ['Budapest 05.'], + 'coordinates' => ['lat' => 47.5027289, 'lng' => 19.073376], + ], + 'Budapest VIII. ker.' => [ + 'constituencies' => ['Budapest 01.', 'Budapest 06.'], + 'coordinates' => ['lat' => 47.4894184, 'lng' => 19.070668], + ], + 'Budapest IX. ker.' => [ + 'constituencies' => ['Budapest 01.', 'Budapest 06.'], + 'coordinates' => ['lat' => 47.4649279, 'lng' => 19.0916229], + ], + 'Budapest X. ker.' => [ + 'constituencies' => ['Budapest 09.', 'Budapest 14.'], + 'coordinates' => ['lat' => 47.4820909, 'lng' => 19.1575028], + ], + 'Budapest XI. ker.' => [ + 'constituencies' => ['Budapest 02.', 'Budapest 18.'], + 'coordinates' => ['lat' => 47.4593099, 'lng' => 19.0187389], + ], + 'Budapest XII. ker.' => [ + 'constituencies' => ['Budapest 03.'], + 'coordinates' => ['lat' => 47.4991199, 'lng' => 18.990459], + ], + 'Budapest XIII. ker.' => [ + 'constituencies' => ['Budapest 11.', 'Budapest 07.'], + 'coordinates' => ['lat' => 47.5355105, 'lng' => 19.0709266], + ], + 'Budapest XIV. ker.' => [ + 'constituencies' => ['Budapest 08.', 'Budapest 13.'], + 'coordinates' => ['lat' => 47.5224569, 'lng' => 19.114709], + ], + 'Budapest XV. ker.' => [ + 'constituencies' => ['Budapest 12.'], + 'coordinates' => ['lat' => 47.5589, 'lng' => 19.1193], + ], + 'Budapest XVI. ker.' => [ + 'constituencies' => ['Budapest 13.'], + 'coordinates' => ['lat' => 47.5183029, 'lng' => 19.191941], + ], + 'Budapest XVII. ker.' => [ + 'constituencies' => ['Budapest 14.'], + 'coordinates' => ['lat' => 47.4803, 'lng' => 19.2667001], + ], + 'Budapest XVIII. ker.' => [ + 'constituencies' => ['Budapest 15.'], + 'coordinates' => ['lat' => 47.4281229, 'lng' => 19.2098429], + ], + 'Budapest XIX. ker.' => [ + 'constituencies' => ['Budapest 09.', 'Budapest 16.'], + 'coordinates' => ['lat' => 47.4457289, 'lng' => 19.1430149], + ], + 'Budapest XX. ker.' => [ + 'constituencies' => ['Budapest 16.'], + 'coordinates' => ['lat' => 47.4332879, 'lng' => 19.1193169], + ], + 'Budapest XXI. ker.' => [ + 'constituencies' => ['Budapest 17.'], + 'coordinates' => ['lat' => 47.4243579, 'lng' => 19.066142], + ], + 'Budapest XXII. ker.' => [ + 'constituencies' => ['Budapest 18.'], + 'coordinates' => ['lat' => 47.425, 'lng' => 19.031667], + ], + 'Budapest XXIII. ker.' => [ + 'constituencies' => ['Budapest 17.'], + 'coordinates' => ['lat' => 47.3939599, 'lng' => 19.122523], + ], + ], + 'Csongrád-Csanád' => [ + 'Algyő' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3329625, 'lng' => 20.207889], + ], + 'Ambrózfalva' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3501417, 'lng' => 20.7313995], + ], + 'Apátfalva' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.173317, 'lng' => 20.5800472], + ], + 'Árpádhalom' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6158286, 'lng' => 20.547733], + ], + 'Ásotthalom' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.1995983, 'lng' => 19.7833756], + ], + 'Baks' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5518708, 'lng' => 20.1064166], + ], + 'Balástya' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.4261828, 'lng' => 20.004933], + ], + 'Bordány' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3194213, 'lng' => 19.9227063], + ], + 'Csanádalberti' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3267872, 'lng' => 20.7068631], + ], + 'Csanádpalota' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2407708, 'lng' => 20.7228873], + ], + 'Csanytelek' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6014883, 'lng' => 20.1114379], + ], + 'Csengele' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5411505, 'lng' => 19.8644533], + ], + 'Csongrád-Csanád' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.7084264, 'lng' => 20.1436061], + ], + 'Derekegyház' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.580238, 'lng' => 20.3549845], + ], + 'Deszk' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.2179603, 'lng' => 20.2404106], + ], + 'Dóc' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.437292, 'lng' => 20.1363129], + ], + 'Domaszék' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2466283, 'lng' => 19.9990365], + ], + 'Eperjes' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.7076258, 'lng' => 20.5621489], + ], + 'Fábiánsebestyén' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6748615, 'lng' => 20.455037], + ], + 'Felgyő' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6616513, 'lng' => 20.1097394], + ], + 'Ferencszállás' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.2158295, 'lng' => 20.3553359], + ], + 'Földeák' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3184223, 'lng' => 20.4929019], + ], + 'Forráskút' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3655956, 'lng' => 19.9089055], + ], + 'Hódmezővásárhely' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.4181262, 'lng' => 20.3300315], + ], + 'Királyhegyes' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2717114, 'lng' => 20.6126302], + ], + 'Kistelek' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.4694781, 'lng' => 19.9804365], + ], + 'Kiszombor' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.1856953, 'lng' => 20.4265486], + ], + 'Klárafalva' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.220953, 'lng' => 20.3255224], + ], + 'Kövegy' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2246141, 'lng' => 20.6840764], + ], + 'Kübekháza' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.1500892, 'lng' => 20.276983], + ], + 'Magyarcsanád' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.1698824, 'lng' => 20.6132706], + ], + 'Makó' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2219071, 'lng' => 20.4809265], + ], + 'Maroslele' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2698362, 'lng' => 20.3418589], + ], + 'Mártély' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.4682451, 'lng' => 20.2416146], + ], + 'Mindszent' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5227585, 'lng' => 20.1895798], + ], + 'Mórahalom' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2179218, 'lng' => 19.88372], + ], + 'Nagyér' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3703008, 'lng' => 20.729605], + ], + 'Nagylak' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.1737713, 'lng' => 20.7111982], + ], + 'Nagymágocs' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5857132, 'lng' => 20.4833875], + ], + 'Nagytőke' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.7552639, 'lng' => 20.2860999], + ], + 'Óföldeák' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.2985957, 'lng' => 20.4369086], + ], + 'Ópusztaszer' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.4957061, 'lng' => 20.0665358], + ], + 'Öttömös' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2808756, 'lng' => 19.6826038], + ], + 'Pitvaros' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.3194853, 'lng' => 20.7385996], + ], + 'Pusztamérges' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3280134, 'lng' => 19.6849699], + ], + 'Pusztaszer' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5515959, 'lng' => 19.9870098], + ], + 'Röszke' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.1873773, 'lng' => 20.037455], + ], + 'Ruzsa' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2890678, 'lng' => 19.7481121], + ], + 'Sándorfalva' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.3635951, 'lng' => 20.1032227], + ], + 'Szatymaz' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.3426558, 'lng' => 20.0391941], + ], + 'Szeged' => [ + 'constituencies' => ['Csongrád-Csanád 2.', 'Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.2530102, 'lng' => 20.1414253], + ], + 'Szegvár' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.5816447, 'lng' => 20.2266415], + ], + 'Székkutas' => [ + 'constituencies' => ['Csongrád-Csanád 4.'], + 'coordinates' => ['lat' => 46.5063976, 'lng' => 20.537673], + ], + 'Szentes' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.654789, 'lng' => 20.2637492], + ], + 'Tiszasziget' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.1720458, 'lng' => 20.1618289], + ], + 'Tömörkény' => [ + 'constituencies' => ['Csongrád-Csanád 3.'], + 'coordinates' => ['lat' => 46.6166243, 'lng' => 20.0436896], + ], + 'Újszentiván' => [ + 'constituencies' => ['Csongrád-Csanád 1.'], + 'coordinates' => ['lat' => 46.1859286, 'lng' => 20.1835123], + ], + 'Üllés' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3355015, 'lng' => 19.8489644], + ], + 'Zákányszék' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.2752726, 'lng' => 19.8883111], + ], + 'Zsombó' => [ + 'constituencies' => ['Csongrád-Csanád 2.'], + 'coordinates' => ['lat' => 46.3284014, 'lng' => 19.9766186], + ], + ], + 'Fejér' => [ + 'Aba' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0328193, 'lng' => 18.522359], + ], + 'Adony' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.119831, 'lng' => 18.8612469], + ], + 'Alap' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8075763, 'lng' => 18.684028], + ], + 'Alcsútdoboz' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4277067, 'lng' => 18.6030325], + ], + 'Alsószentiván' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.7910573, 'lng' => 18.732161], + ], + 'Bakonycsernye' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.321719, 'lng' => 18.0907379], + ], + 'Bakonykúti' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2458464, 'lng' => 18.195769], + ], + 'Balinka' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3135736, 'lng' => 18.1907168], + ], + 'Baracs' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9049033, 'lng' => 18.8752931], + ], + 'Baracska' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2824737, 'lng' => 18.7598901], + ], + 'Beloiannisz' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.183143, 'lng' => 18.8245727], + ], + 'Besnyő' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.1892568, 'lng' => 18.7936832], + ], + 'Bicske' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4911792, 'lng' => 18.6370142], + ], + 'Bodajk' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3209663, 'lng' => 18.2339242], + ], + 'Bodmér' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4489857, 'lng' => 18.5383832], + ], + 'Cece' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.7698199, 'lng' => 18.6336808], + ], + 'Csabdi' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.5229299, 'lng' => 18.6085371], + ], + 'Csákberény' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3506861, 'lng' => 18.3265064], + ], + 'Csákvár' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3941468, 'lng' => 18.4602445], + ], + 'Csókakő' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3533961, 'lng' => 18.2693867], + ], + 'Csór' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2049913, 'lng' => 18.2557813], + ], + 'Csősz' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0382791, 'lng' => 18.414533], + ], + 'Daruszentmiklós' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.87194, 'lng' => 18.8568642], + ], + 'Dég' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8707664, 'lng' => 18.4445717], + ], + 'Dunaújváros' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 46.9619059, 'lng' => 18.9355227], + ], + 'Előszállás' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8276091, 'lng' => 18.8280627], + ], + 'Enying' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9326943, 'lng' => 18.2414807], + ], + 'Ercsi' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.2482238, 'lng' => 18.8912626], + ], + 'Etyek' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4467098, 'lng' => 18.751179], + ], + 'Fehérvárcsurgó' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2904264, 'lng' => 18.2645262], + ], + 'Felcsút' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4541851, 'lng' => 18.5865775], + ], + 'Füle' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.0535367, 'lng' => 18.2480871], + ], + 'Gánt' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3902121, 'lng' => 18.387061], + ], + 'Gárdony' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.196537, 'lng' => 18.6115195], + ], + 'Gyúró' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3700577, 'lng' => 18.7384824], + ], + 'Hantos' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9943127, 'lng' => 18.6989263], + ], + 'Igar' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.7757642, 'lng' => 18.5137348], + ], + 'Iszkaszentgyörgy' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2399338, 'lng' => 18.2987232], + ], + 'Isztimér' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2787058, 'lng' => 18.1955966], + ], + 'Iváncsa' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.153376, 'lng' => 18.8270434], + ], + 'Jenő' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1047531, 'lng' => 18.2453199], + ], + 'Kajászó' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3234883, 'lng' => 18.7221054], + ], + 'Káloz' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9568415, 'lng' => 18.4853961], + ], + 'Kápolnásnyék' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2398554, 'lng' => 18.6764288], + ], + 'Kincsesbánya' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2632477, 'lng' => 18.2764679], + ], + 'Kisapostag' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8940766, 'lng' => 18.9323135], + ], + 'Kisláng' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9598173, 'lng' => 18.3860884], + ], + 'Kőszárhegy' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.0926048, 'lng' => 18.341234], + ], + 'Kulcs' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.0541246, 'lng' => 18.9197178], + ], + 'Lajoskomárom' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.841585, 'lng' => 18.3355393], + ], + 'Lepsény' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9918514, 'lng' => 18.2469618], + ], + 'Lovasberény' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3109278, 'lng' => 18.5527924], + ], + 'Magyaralmás' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2913027, 'lng' => 18.3245512], + ], + 'Mány' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.5321762, 'lng' => 18.6555811], + ], + 'Martonvásár' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3164516, 'lng' => 18.7877558], + ], + 'Mátyásdomb' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9228626, 'lng' => 18.3470929], + ], + 'Mezőfalva' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9323938, 'lng' => 18.7771045], + ], + 'Mezőkomárom' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8276482, 'lng' => 18.2934472], + ], + 'Mezőszentgyörgy' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9920267, 'lng' => 18.2795568], + ], + 'Mezőszilas' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8166957, 'lng' => 18.4754679], + ], + 'Moha' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2437717, 'lng' => 18.3313907], + ], + 'Mór' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.374928, 'lng' => 18.2036035], + ], + 'Nadap' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2585056, 'lng' => 18.6167437], + ], + 'Nádasdladány' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1341786, 'lng' => 18.2394077], + ], + 'Nagykarácsony' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8706425, 'lng' => 18.7725518], + ], + 'Nagylók' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9764964, 'lng' => 18.64115], + ], + 'Nagyveleg' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.361797, 'lng' => 18.111061], + ], + 'Nagyvenyim' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 46.9571015, 'lng' => 18.8576229], + ], + 'Óbarok' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4922397, 'lng' => 18.5681206], + ], + 'Pákozd' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2172004, 'lng' => 18.5430768], + ], + 'Pátka' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2752462, 'lng' => 18.4950339], + ], + 'Pázmánd' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.283645, 'lng' => 18.654854], + ], + 'Perkáta' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.0482285, 'lng' => 18.784294], + ], + 'Polgárdi' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.0601257, 'lng' => 18.2993645], + ], + 'Pusztaszabolcs' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.1408918, 'lng' => 18.7601638], + ], + 'Pusztavám' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.4297438, 'lng' => 18.2317401], + ], + 'Rácalmás' => [ + 'constituencies' => ['Fejér 4.'], + 'coordinates' => ['lat' => 47.0243223, 'lng' => 18.9350709], + ], + 'Ráckeresztúr' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2729155, 'lng' => 18.8330106], + ], + 'Sárbogárd' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.879104, 'lng' => 18.6213353], + ], + 'Sáregres' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.783236, 'lng' => 18.5935136], + ], + 'Sárkeresztes' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.2517488, 'lng' => 18.3541822], + ], + 'Sárkeresztúr' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0025252, 'lng' => 18.5479461], + ], + 'Sárkeszi' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1582764, 'lng' => 18.284968], + ], + 'Sárosd' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0414738, 'lng' => 18.6488144], + ], + 'Sárszentágota' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.9706742, 'lng' => 18.5634969], + ], + 'Sárszentmihály' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1537282, 'lng' => 18.3235014], + ], + 'Seregélyes' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.1100586, 'lng' => 18.5788431], + ], + 'Soponya' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0120427, 'lng' => 18.4543505], + ], + 'Söréd' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.322683, 'lng' => 18.280508], + ], + 'Sukoró' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2425436, 'lng' => 18.6022803], + ], + 'Szabadbattyán' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1175572, 'lng' => 18.3681061], + ], + 'Szabadegyháza' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0770131, 'lng' => 18.6912379], + ], + 'Szabadhídvég' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.8210159, 'lng' => 18.2798938], + ], + 'Szár' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4791911, 'lng' => 18.5158147], + ], + 'Székesfehérvár' => [ + 'constituencies' => ['Fejér 2.', 'Fejér 1.'], + 'coordinates' => ['lat' => 47.1860262, 'lng' => 18.4221358], + ], + 'Tabajd' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4045316, 'lng' => 18.6302011], + ], + 'Tác' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 47.0794264, 'lng' => 18.403381], + ], + 'Tordas' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3440943, 'lng' => 18.7483302], + ], + 'Újbarok' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4791337, 'lng' => 18.5585574], + ], + 'Úrhida' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.1298384, 'lng' => 18.3321437], + ], + 'Vajta' => [ + 'constituencies' => ['Fejér 5.'], + 'coordinates' => ['lat' => 46.7227758, 'lng' => 18.6618091], + ], + 'Vál' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3624339, 'lng' => 18.6766737], + ], + 'Velence' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.2300924, 'lng' => 18.6506424], + ], + 'Vereb' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.318485, 'lng' => 18.6197301], + ], + 'Vértesacsa' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.3700218, 'lng' => 18.5792793], + ], + 'Vértesboglár' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.4291347, 'lng' => 18.5235823], + ], + 'Zámoly' => [ + 'constituencies' => ['Fejér 2.'], + 'coordinates' => ['lat' => 47.3168103, 'lng' => 18.408371], + ], + 'Zichyújfalu' => [ + 'constituencies' => ['Fejér 3.'], + 'coordinates' => ['lat' => 47.1291991, 'lng' => 18.6692222], + ], + ], + 'Győr-Moson-Sopron' => [ + 'Abda' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.6962149, 'lng' => 17.5445786], + ], + 'Acsalag' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.676095, 'lng' => 17.1977771], + ], + 'Ágfalva' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.688862, 'lng' => 16.5110233], + ], + 'Agyagosszergény' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.608545, 'lng' => 16.9409912], + ], + 'Árpás' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5134127, 'lng' => 17.3931579], + ], + 'Ásványráró' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8287695, 'lng' => 17.499195], + ], + 'Babót' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5752269, 'lng' => 17.0758604], + ], + 'Bágyogszovát' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5866036, 'lng' => 17.3617273], + ], + 'Bakonygyirót' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4181388, 'lng' => 17.8055502], + ], + 'Bakonypéterd' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4667076, 'lng' => 17.7967619], + ], + 'Bakonyszentlászló' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.3892006, 'lng' => 17.8032754], + ], + 'Barbacs' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6455476, 'lng' => 17.297216], + ], + 'Beled' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4662675, 'lng' => 17.0959263], + ], + 'Bezenye' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9609867, 'lng' => 17.216211], + ], + 'Bezi' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6737572, 'lng' => 17.3921093], + ], + 'Bodonhely' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5655752, 'lng' => 17.4072124], + ], + 'Bogyoszló' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5609657, 'lng' => 17.1850606], + ], + 'Bőny' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6516279, 'lng' => 17.8703841], + ], + 'Börcs' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.6862052, 'lng' => 17.4988893], + ], + 'Bősárkány' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6881947, 'lng' => 17.2507143], + ], + 'Cakóháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.6967121, 'lng' => 17.2863758], + ], + 'Cirák' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4779219, 'lng' => 17.0282338], + ], + 'Csáfordjánosfa' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4151998, 'lng' => 16.9510595], + ], + 'Csapod' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5162077, 'lng' => 16.9234546], + ], + 'Csér' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4169765, 'lng' => 16.9330737], + ], + 'Csikvánd' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4666335, 'lng' => 17.4546305], + ], + 'Csorna' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6103234, 'lng' => 17.2462444], + ], + 'Darnózseli' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8493957, 'lng' => 17.4273958], + ], + 'Dénesfa' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4558445, 'lng' => 17.0335351], + ], + 'Dör' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5979168, 'lng' => 17.2991911], + ], + 'Dunakiliti' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9659588, 'lng' => 17.2882641], + ], + 'Dunaremete' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8761957, 'lng' => 17.4375005], + ], + 'Dunaszeg' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7692554, 'lng' => 17.5407805], + ], + 'Dunaszentpál' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7771623, 'lng' => 17.5043978], + ], + 'Dunasziget' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9359671, 'lng' => 17.3617867], + ], + 'Ebergőc' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5635832, 'lng' => 16.81167], + ], + 'Écs' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5604415, 'lng' => 17.7072193], + ], + 'Edve' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4551126, 'lng' => 17.135508], + ], + 'Egyed' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5192845, 'lng' => 17.3396861], + ], + 'Egyházasfalu' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.46243, 'lng' => 16.7679871], + ], + 'Enese' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6461219, 'lng' => 17.4235267], + ], + 'Farád' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6064483, 'lng' => 17.2003347], + ], + 'Fehértó' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6759514, 'lng' => 17.3453497], + ], + 'Feketeerdő' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9355702, 'lng' => 17.2783691], + ], + 'Felpéc' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5225976, 'lng' => 17.5993517], + ], + 'Fenyőfő' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.3490387, 'lng' => 17.7656259], + ], + 'Fertőboz' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.633426, 'lng' => 16.6998899], + ], + 'Fertőd' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.61818, 'lng' => 16.8741418], + ], + 'Fertőendréd' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6054618, 'lng' => 16.9085891], + ], + 'Fertőhomok' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6196363, 'lng' => 16.7710445], + ], + 'Fertőrákos' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.7209654, 'lng' => 16.6488128], + ], + 'Fertőszentmiklós' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5895578, 'lng' => 16.8730712], + ], + 'Fertőszéplak' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6172442, 'lng' => 16.8405708], + ], + 'Gönyű' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.7334344, 'lng' => 17.8243403], + ], + 'Gyalóka' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4427372, 'lng' => 16.696223], + ], + 'Gyarmat' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4604024, 'lng' => 17.4964917], + ], + 'Gyömöre' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4982876, 'lng' => 17.564804], + ], + 'Győr' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.', 'Győr-Moson-Sopron 1.'], + 'coordinates' => ['lat' => 47.6874569, 'lng' => 17.6503974], + ], + 'Győrasszonyfa' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4950098, 'lng' => 17.8072327], + ], + 'Győrladamér' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7545651, 'lng' => 17.5633004], + ], + 'Gyóró' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4916519, 'lng' => 17.0236667], + ], + 'Győrság' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5751529, 'lng' => 17.7515893], + ], + 'Győrsövényház' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6909394, 'lng' => 17.3734235], + ], + 'Győrszemere' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.551813, 'lng' => 17.5635661], + ], + 'Győrújbarát' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6076284, 'lng' => 17.6389745], + ], + 'Győrújfalu' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.722197, 'lng' => 17.6054524], + ], + 'Győrzámoly' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7434268, 'lng' => 17.5770199], + ], + 'Halászi' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8903231, 'lng' => 17.3256673], + ], + 'Harka' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6339566, 'lng' => 16.5986264], + ], + 'Hédervár' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.831062, 'lng' => 17.4541026], + ], + 'Hegyeshalom' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9117445, 'lng' => 17.156071], + ], + 'Hegykő' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6188466, 'lng' => 16.7940292], + ], + 'Hidegség' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6253847, 'lng' => 16.740935], + ], + 'Himod' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5200248, 'lng' => 17.0064434], + ], + 'Hövej' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5524954, 'lng' => 17.0166402], + ], + 'Ikrény' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6539897, 'lng' => 17.5281764], + ], + 'Iván' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.445549, 'lng' => 16.9096056], + ], + 'Jánossomorja' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7847917, 'lng' => 17.1298642], + ], + 'Jobaháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5799316, 'lng' => 17.1886952], + ], + 'Kajárpéc' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4888221, 'lng' => 17.6350057], + ], + 'Kapuvár' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5912437, 'lng' => 17.0301952], + ], + 'Károlyháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8032696, 'lng' => 17.3446363], + ], + 'Kimle' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8172115, 'lng' => 17.3676625], + ], + 'Kisbabot' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5551791, 'lng' => 17.4149558], + ], + 'Kisbajcs' => [ + 'constituencies' => ['Győr-Moson-Sopron 1.'], + 'coordinates' => ['lat' => 47.7450615, 'lng' => 17.6800942], + ], + 'Kisbodak' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8963234, 'lng' => 17.4196192], + ], + 'Kisfalud' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.2041959, 'lng' => 18.494568], + ], + 'Kóny' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6307264, 'lng' => 17.3596093], + ], + 'Kópháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6385359, 'lng' => 16.6451629], + ], + 'Koroncó' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5999604, 'lng' => 17.5284792], + ], + 'Kunsziget' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7385858, 'lng' => 17.5176565], + ], + 'Lázi' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4661979, 'lng' => 17.8346909], + ], + 'Lébény' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7360651, 'lng' => 17.3905652], + ], + 'Levél' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8949275, 'lng' => 17.2001946], + ], + 'Lipót' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8615868, 'lng' => 17.4603528], + ], + 'Lövő' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5107966, 'lng' => 16.7898395], + ], + 'Maglóca' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6625685, 'lng' => 17.2751221], + ], + 'Magyarkeresztúr' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5200063, 'lng' => 17.1660121], + ], + 'Máriakálnok' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8596905, 'lng' => 17.3237666], + ], + 'Markotabödöge' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6815136, 'lng' => 17.3116772], + ], + 'Mecsér' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.796671, 'lng' => 17.4744842], + ], + 'Mérges' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6012809, 'lng' => 17.4438455], + ], + 'Mezőörs' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.568844, 'lng' => 17.8821253], + ], + 'Mihályi' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5142703, 'lng' => 17.0958265], + ], + 'Mórichida' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5127896, 'lng' => 17.4218174], + ], + 'Mosonmagyaróvár' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8681469, 'lng' => 17.2689169], + ], + 'Mosonszentmiklós' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7294576, 'lng' => 17.4242231], + ], + 'Mosonszolnok' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8511108, 'lng' => 17.1735793], + ], + 'Mosonudvar' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8435379, 'lng' => 17.224348], + ], + 'Nagybajcs' => [ + 'constituencies' => ['Győr-Moson-Sopron 1.'], + 'coordinates' => ['lat' => 47.7639168, 'lng' => 17.686613], + ], + 'Nagycenk' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6081549, 'lng' => 16.6979223], + ], + 'Nagylózs' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5654858, 'lng' => 16.76965], + ], + 'Nagyszentjános' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.7100868, 'lng' => 17.8681808], + ], + 'Nemeskér' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.483855, 'lng' => 16.8050771], + ], + 'Nyalka' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5443407, 'lng' => 17.8091081], + ], + 'Nyúl' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5832389, 'lng' => 17.6862095], + ], + 'Osli' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6385609, 'lng' => 17.0755158], + ], + 'Öttevény' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7255506, 'lng' => 17.4899552], + ], + 'Páli' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4774264, 'lng' => 17.1695082], + ], + 'Pannonhalma' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.549497, 'lng' => 17.7552412], + ], + 'Pásztori' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5553919, 'lng' => 17.2696728], + ], + 'Pázmándfalu' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5710798, 'lng' => 17.7810865], + ], + 'Pér' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6111604, 'lng' => 17.8049747], + ], + 'Pereszteg' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.594289, 'lng' => 16.7354028], + ], + 'Petőháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5965785, 'lng' => 16.8954138], + ], + 'Pinnye' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5855193, 'lng' => 16.7706082], + ], + 'Potyond' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.549377, 'lng' => 17.1821874], + ], + 'Püski' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8846385, 'lng' => 17.4070152], + ], + 'Pusztacsalád' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4853081, 'lng' => 16.9013644], + ], + 'Rábacsanak' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5256113, 'lng' => 17.2902872], + ], + 'Rábacsécsény' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5879598, 'lng' => 17.4227941], + ], + 'Rábakecöl' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4324946, 'lng' => 17.1126349], + ], + 'Rábapatona' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.6314656, 'lng' => 17.4797584], + ], + 'Rábapordány' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5574649, 'lng' => 17.3262502], + ], + 'Rábasebes' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4392738, 'lng' => 17.2423807], + ], + 'Rábaszentandrás' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4596327, 'lng' => 17.3272097], + ], + 'Rábaszentmihály' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5775103, 'lng' => 17.4312379], + ], + 'Rábaszentmiklós' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5381909, 'lng' => 17.417513], + ], + 'Rábatamási' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5893387, 'lng' => 17.1699767], + ], + 'Rábcakapi' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7079835, 'lng' => 17.2755839], + ], + 'Rajka' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.9977901, 'lng' => 17.1983996], + ], + 'Ravazd' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5162349, 'lng' => 17.7512699], + ], + 'Répceszemere' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4282026, 'lng' => 16.9738943], + ], + 'Répcevis' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4427966, 'lng' => 16.6731972], + ], + 'Rétalap' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6072246, 'lng' => 17.9071507], + ], + 'Röjtökmuzsaj' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5543502, 'lng' => 16.8363467], + ], + 'Románd' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4484049, 'lng' => 17.7909987], + ], + 'Sarród' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6315873, 'lng' => 16.8613408], + ], + 'Sikátor' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4370828, 'lng' => 17.8510581], + ], + 'Sobor' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4768368, 'lng' => 17.3752902], + ], + 'Sokorópátka' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4892381, 'lng' => 17.6953943], + ], + 'Sopron' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.6816619, 'lng' => 16.5844795], + ], + 'Sopronhorpács' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4831854, 'lng' => 16.7359058], + ], + 'Sopronkövesd' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.5460504, 'lng' => 16.7432859], + ], + 'Sopronnémeti' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5364397, 'lng' => 17.2070182], + ], + 'Szakony' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4262848, 'lng' => 16.7154462], + ], + 'Szany' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4620733, 'lng' => 17.3027671], + ], + 'Szárföld' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5933239, 'lng' => 17.1221243], + ], + 'Szerecseny' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4628425, 'lng' => 17.5536197], + ], + 'Szil' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.501622, 'lng' => 17.233297], + ], + 'Szilsárkány' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5396552, 'lng' => 17.2545808], + ], + 'Táp' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5168299, 'lng' => 17.8292989], + ], + 'Tápszentmiklós' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4930151, 'lng' => 17.8524913], + ], + 'Tarjánpuszta' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5062161, 'lng' => 17.7869857], + ], + 'Tárnokréti' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.7217546, 'lng' => 17.3078226], + ], + 'Tényő' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.5407376, 'lng' => 17.6490009], + ], + 'Tét' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5198967, 'lng' => 17.5108553], + ], + 'Töltéstava' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.6273335, 'lng' => 17.7343778], + ], + 'Újkér' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4573295, 'lng' => 16.8187647], + ], + 'Újrónafő' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8101728, 'lng' => 17.2015241], + ], + 'Und' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.488856, 'lng' => 16.6961552], + ], + 'Vadosfa' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4986805, 'lng' => 17.1287654], + ], + 'Vág' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4469264, 'lng' => 17.2121765], + ], + 'Vámosszabadi' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.7571476, 'lng' => 17.6507532], + ], + 'Várbalog' => [ + 'constituencies' => ['Győr-Moson-Sopron 5.'], + 'coordinates' => ['lat' => 47.8347267, 'lng' => 17.0720923], + ], + 'Vásárosfalu' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.4537986, 'lng' => 17.1158473], + ], + 'Vének' => [ + 'constituencies' => ['Győr-Moson-Sopron 1.'], + 'coordinates' => ['lat' => 47.7392272, 'lng' => 17.7556608], + ], + 'Veszkény' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5969056, 'lng' => 17.0891913], + ], + 'Veszprémvarsány' => [ + 'constituencies' => ['Győr-Moson-Sopron 2.'], + 'coordinates' => ['lat' => 47.4290248, 'lng' => 17.8287245], + ], + 'Vitnyéd' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.5863882, 'lng' => 16.9832151], + ], + 'Völcsej' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.496503, 'lng' => 16.7604595], + ], + 'Zsebeháza' => [ + 'constituencies' => ['Győr-Moson-Sopron 3.'], + 'coordinates' => ['lat' => 47.511293, 'lng' => 17.191017], + ], + 'Zsira' => [ + 'constituencies' => ['Győr-Moson-Sopron 4.'], + 'coordinates' => ['lat' => 47.4580482, 'lng' => 16.6766466], + ], + ], + 'Hajdú-Bihar' => [ + 'Álmosd' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.4167788, 'lng' => 21.9806107], + ], + 'Ártánd' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1241958, 'lng' => 21.7568167], + ], + 'Bagamér' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.4498231, 'lng' => 21.9942012], + ], + 'Bakonszeg' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1900613, 'lng' => 21.4442102], + ], + 'Balmazújváros' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.6145296, 'lng' => 21.3417333], + ], + 'Báránd' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.2936964, 'lng' => 21.2288584], + ], + 'Bedő' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1634194, 'lng' => 21.7502785], + ], + 'Berekböszörmény' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0615952, 'lng' => 21.6782301], + ], + 'Berettyóújfalu' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2196438, 'lng' => 21.5362812], + ], + 'Bihardancsháza' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2291246, 'lng' => 21.3159659], + ], + 'Biharkeresztes' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1301236, 'lng' => 21.7219423], + ], + 'Biharnagybajom' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2108104, 'lng' => 21.2302309], + ], + 'Bihartorda' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.215994, 'lng' => 21.3526252], + ], + 'Bocskaikert' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6435949, 'lng' => 21.659878], + ], + 'Bojt' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1927968, 'lng' => 21.7327485], + ], + 'Csökmő' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0315111, 'lng' => 21.2892817], + ], + 'Darvas' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1017037, 'lng' => 21.3374554], + ], + 'Debrecen' => [ + 'constituencies' => ['Hajdú-Bihar 3.', 'Hajdú-Bihar 1.', 'Hajdú-Bihar 2.'], + 'coordinates' => ['lat' => 47.5316049, 'lng' => 21.6273124], + ], + 'Derecske' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3533886, 'lng' => 21.5658524], + ], + 'Ebes' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.4709086, 'lng' => 21.490457], + ], + 'Egyek' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.6258313, 'lng' => 20.8907463], + ], + 'Esztár' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2837051, 'lng' => 21.7744117], + ], + 'Földes' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.2896801, 'lng' => 21.3633025], + ], + 'Folyás' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.8086696, 'lng' => 21.1371809], + ], + 'Fülöp' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.5981409, 'lng' => 22.0546557], + ], + 'Furta' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1300357, 'lng' => 21.460144], + ], + 'Gáborján' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2360716, 'lng' => 21.6622765], + ], + 'Görbeháza' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.8200025, 'lng' => 21.2359976], + ], + 'Hajdúbagos' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3947066, 'lng' => 21.6643329], + ], + 'Hajdúböszörmény' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.6718908, 'lng' => 21.5126637], + ], + 'Hajdúdorog' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.8166047, 'lng' => 21.4980694], + ], + 'Hajdúhadház' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6802292, 'lng' => 21.6675179], + ], + 'Hajdúnánás' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.843004, 'lng' => 21.4242691], + ], + 'Hajdúsámson' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6049148, 'lng' => 21.7597325], + ], + 'Hajdúszoboszló' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.4435369, 'lng' => 21.3965516], + ], + 'Hajdúszovát' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.3903463, 'lng' => 21.4764161], + ], + 'Hencida' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2507004, 'lng' => 21.6989732], + ], + 'Hortobágy' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.5868751, 'lng' => 21.1560332], + ], + 'Hosszúpályi' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3947673, 'lng' => 21.7346539], + ], + 'Kaba' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.3565391, 'lng' => 21.2726765], + ], + 'Kismarja' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2463277, 'lng' => 21.8214627], + ], + 'Kokad' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.4054409, 'lng' => 21.9336174], + ], + 'Komádi' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0055271, 'lng' => 21.4944772], + ], + 'Konyár' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3213954, 'lng' => 21.6691634], + ], + 'Körösszakál' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0178012, 'lng' => 21.5932398], + ], + 'Körösszegapáti' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0396539, 'lng' => 21.6317831], + ], + 'Létavértes' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.3835171, 'lng' => 21.8798767], + ], + 'Magyarhomorog' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0222187, 'lng' => 21.5480518], + ], + 'Mezőpeterd' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.165025, 'lng' => 21.6200633], + ], + 'Mezősas' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1104156, 'lng' => 21.5671344], + ], + 'Mikepércs' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.4406335, 'lng' => 21.6366773], + ], + 'Monostorpályi' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.3984198, 'lng' => 21.7764527], + ], + 'Nádudvar' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.4259381, 'lng' => 21.1616779], + ], + 'Nagyhegyes' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.539228, 'lng' => 21.345552], + ], + 'Nagykereki' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1863168, 'lng' => 21.7922805], + ], + 'Nagyrábé' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2043078, 'lng' => 21.3306582], + ], + 'Nyírábrány' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.541423, 'lng' => 22.0128317], + ], + 'Nyíracsád' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6039774, 'lng' => 21.9715154], + ], + 'Nyíradony' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.6899404, 'lng' => 21.9085991], + ], + 'Nyírmártonfalva' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.5862503, 'lng' => 21.8964914], + ], + 'Pocsaj' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2851817, 'lng' => 21.8122198], + ], + 'Polgár' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.8679381, 'lng' => 21.1141038], + ], + 'Püspökladány' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.3216529, 'lng' => 21.1185953], + ], + 'Sáp' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2549739, 'lng' => 21.3555868], + ], + 'Sáránd' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.4062312, 'lng' => 21.6290631], + ], + 'Sárrétudvari' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.2406806, 'lng' => 21.1866058], + ], + 'Szentpéterszeg' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2386719, 'lng' => 21.6178971], + ], + 'Szerep' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.2278774, 'lng' => 21.1407795], + ], + 'Téglás' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.7109686, 'lng' => 21.6727776], + ], + 'Tépe' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.32046, 'lng' => 21.5714076], + ], + 'Tetétlen' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.3148595, 'lng' => 21.3069162], + ], + 'Tiszacsege' => [ + 'constituencies' => ['Hajdú-Bihar 5.'], + 'coordinates' => ['lat' => 47.6997085, 'lng' => 20.9917041], + ], + 'Tiszagyulaháza' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.942524, 'lng' => 21.1428152], + ], + 'Told' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1180165, 'lng' => 21.6413048], + ], + 'Újiráz' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 46.9870862, 'lng' => 21.3556353], + ], + 'Újléta' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.4650261, 'lng' => 21.8733489], + ], + 'Újszentmargita' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.7266767, 'lng' => 21.1047788], + ], + 'Újtikos' => [ + 'constituencies' => ['Hajdú-Bihar 6.'], + 'coordinates' => ['lat' => 47.9176202, 'lng' => 21.171571], + ], + 'Vámospércs' => [ + 'constituencies' => ['Hajdú-Bihar 3.'], + 'coordinates' => ['lat' => 47.525345, 'lng' => 21.8992474], + ], + 'Váncsod' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.2011182, 'lng' => 21.6400459], + ], + 'Vekerd' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.0959975, 'lng' => 21.4017741], + ], + 'Zsáka' => [ + 'constituencies' => ['Hajdú-Bihar 4.'], + 'coordinates' => ['lat' => 47.1340418, 'lng' => 21.4307824], + ], + ], + 'Heves' => [ + 'Abasár' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7989023, 'lng' => 20.0036779], + ], + 'Adács' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6922284, 'lng' => 19.9779484], + ], + 'Aldebrő' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7891428, 'lng' => 20.2302555], + ], + 'Andornaktálya' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8499325, 'lng' => 20.4105243], + ], + 'Apc' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7933298, 'lng' => 19.6955737], + ], + 'Átány' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.6156875, 'lng' => 20.3620368], + ], + 'Atkár' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7209651, 'lng' => 19.8912361], + ], + 'Balaton' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 46.8302679, 'lng' => 17.7340438], + ], + 'Bátor' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.99076, 'lng' => 20.2627351], + ], + 'Bekölce' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0804457, 'lng' => 20.268156], + ], + 'Bélapátfalva' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0578657, 'lng' => 20.3500536], + ], + 'Besenyőtelek' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.6994693, 'lng' => 20.4300342], + ], + 'Boconád' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6414895, 'lng' => 20.1877312], + ], + 'Bodony' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.9420912, 'lng' => 20.0199927], + ], + 'Boldog' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6031287, 'lng' => 19.687521], + ], + 'Bükkszék' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.9915393, 'lng' => 20.1765126], + ], + 'Bükkszenterzsébet' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0532811, 'lng' => 20.1622924], + ], + 'Bükkszentmárton' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0715382, 'lng' => 20.3310312], + ], + 'Csány' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6474142, 'lng' => 19.8259607], + ], + 'Demjén' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8317294, 'lng' => 20.3313872], + ], + 'Detk' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7489442, 'lng' => 20.0983332], + ], + 'Domoszló' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8288666, 'lng' => 20.1172988], + ], + 'Dormánd' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.7203119, 'lng' => 20.4174779], + ], + 'Ecséd' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7307237, 'lng' => 19.7684767], + ], + 'Eger' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.9025348, 'lng' => 20.3772284], + ], + 'Egerbakta' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.9341404, 'lng' => 20.2918134], + ], + 'Egerbocs' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0263467, 'lng' => 20.2598999], + ], + 'Egercsehi' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0545478, 'lng' => 20.261522], + ], + 'Egerfarmos' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.7177802, 'lng' => 20.5358914], + ], + 'Egerszalók' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8702275, 'lng' => 20.3241673], + ], + 'Egerszólát' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8902473, 'lng' => 20.2669774], + ], + 'Erdőkövesd' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0391241, 'lng' => 20.1013656], + ], + 'Erdőtelek' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6852656, 'lng' => 20.3115369], + ], + 'Erk' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6101796, 'lng' => 20.076668], + ], + 'Fedémes' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0320282, 'lng' => 20.1878653], + ], + 'Feldebrő' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8128253, 'lng' => 20.2363322], + ], + 'Felsőtárkány' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.9734513, 'lng' => 20.41906], + ], + 'Füzesabony' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.7495339, 'lng' => 20.4150668], + ], + 'Gyöngyös' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7772651, 'lng' => 19.9294927], + ], + 'Gyöngyöshalász' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7413068, 'lng' => 19.9227242], + ], + 'Gyöngyösoroszi' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8263987, 'lng' => 19.8928817], + ], + 'Gyöngyöspata' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8140904, 'lng' => 19.7923335], + ], + 'Gyöngyössolymos' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8160489, 'lng' => 19.9338831], + ], + 'Gyöngyöstarján' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8132903, 'lng' => 19.8664265], + ], + 'Halmajugra' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7634173, 'lng' => 20.0523104], + ], + 'Hatvan' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6656965, 'lng' => 19.676666], + ], + 'Heréd' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7081485, 'lng' => 19.6327042], + ], + 'Heves' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.5971694, 'lng' => 20.280156], + ], + 'Hevesaranyos' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0109153, 'lng' => 20.2342809], + ], + 'Hevesvezekény' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.5570546, 'lng' => 20.3580453], + ], + 'Hort' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6890439, 'lng' => 19.7842632], + ], + 'Istenmezeje' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0845673, 'lng' => 20.0515347], + ], + 'Ivád' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 48.0203013, 'lng' => 20.0612654], + ], + 'Kál' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7318239, 'lng' => 20.2608866], + ], + 'Kápolna' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7584202, 'lng' => 20.2459749], + ], + 'Karácsond' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7282318, 'lng' => 20.0282488], + ], + 'Kerecsend' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.7947277, 'lng' => 20.3444695], + ], + 'Kerekharaszt' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.6623104, 'lng' => 19.6253721], + ], + 'Kisfüzes' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.9881653, 'lng' => 20.1267373], + ], + 'Kisköre' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.4984608, 'lng' => 20.4973609], + ], + 'Kisnána' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8506469, 'lng' => 20.1457821], + ], + 'Kömlő' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 46.1929788, 'lng' => 18.2512139], + ], + 'Kompolt' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.7415463, 'lng' => 20.2406377], + ], + 'Lőrinci' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7390261, 'lng' => 19.6756557], + ], + 'Ludas' => [ + 'constituencies' => ['Heves 3.'], + 'coordinates' => ['lat' => 47.7300788, 'lng' => 20.0910629], + ], + 'Maklár' => [ + 'constituencies' => ['Heves 1.'], + 'coordinates' => ['lat' => 47.8054074, 'lng' => 20.410901], + ], + 'Markaz' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.8222206, 'lng' => 20.0582311], + ], + 'Mátraballa' => [ + 'constituencies' => ['Heves 2.'], + 'coordinates' => ['lat' => 47.9843833, 'lng' => 20.0225017], + ], + + ], + ]; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8174.php b/tests/PHPStan/Rules/Methods/data/bug-8174.php new file mode 100644 index 0000000000..e7305490ae --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8174.php @@ -0,0 +1,23 @@ + $list + * @return list + */ + public function filterList(array $list): array { + $filtered = array_filter($list, function ($elem) { + return $elem === '23423'; + }); + assertType("array, '23423'>", $filtered); // this is not a list + assertType("list<'23423'>", array_values($filtered)); // this is a list + + // why am I allowed to return not a list then? + return $filtered; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8223.php b/tests/PHPStan/Rules/Methods/data/bug-8223.php new file mode 100644 index 0000000000..679f123c67 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8223.php @@ -0,0 +1,30 @@ +modify($modify); + } + + /** + * @return array<\DateTimeImmutable> + */ + public function sayHello2(string $modify): array + { + $date = new \DateTimeImmutable(); + + return [$date->modify($modify)]; + } + + public function test() + { + $r = new HelloWorld(); + + $r->sayHello('ss'); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8296.php b/tests/PHPStan/Rules/Methods/data/bug-8296.php new file mode 100644 index 0000000000..b257780358 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8296.php @@ -0,0 +1,29 @@ + new stdClass(), + "b" => true + ]; + self::continueDump($dummy); + + $string = 12345; + self::stringByRef($string); + } + + /** + * @phpstan-param array $objects + * @phpstan-param-out array $objects + */ + private static function continueDump(array &$objects) : void{ + + } + + private static function stringByRef(string &$string) : void{ + + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8500.php b/tests/PHPStan/Rules/Methods/data/bug-8500.php new file mode 100644 index 0000000000..b10f9c7def --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8500.php @@ -0,0 +1,33 @@ + $data + */ + public static function __set_state(array $data): static + { + $obj = new static(); + + return $obj; + } +} + +class B extends A +{ + public static function __set_state(array $data): static + { + $obj = parent::__set_state($data); + + return $obj; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8713.php b/tests/PHPStan/Rules/Methods/data/bug-8713.php new file mode 100644 index 0000000000..49aa966c10 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8713.php @@ -0,0 +1,13 @@ += 8.0 + +namespace Bug8713; + +class Foo +{ + public function foo(): void + { + $query = "SELECT * FROM `foo`"; + $pdo = new \PDO("dsn"); + $pdo->query(query: $query); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8879.php b/tests/PHPStan/Rules/Methods/data/bug-8879.php new file mode 100644 index 0000000000..a59e0c866c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8879.php @@ -0,0 +1,15 @@ +someTest('foo'); + } + return; + } +} + +class A +{ + use SomeTrait; + + public function someTest(string $foo, string $bar): void {} +} + +class B +{ + use SomeTrait; + + public function someTest(string $foo): void {} +} + +class Test +{ + public function test(): void + { + $a = new A(); + $a->test(); + $b = new B(); + $b->test(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9009.php b/tests/PHPStan/Rules/Methods/data/bug-9009.php new file mode 100644 index 0000000000..a60ca63003 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9009.php @@ -0,0 +1,26 @@ +addHook($fx); + } +} + +(new Hook())->addHookDynamic(function (Hook $hook) { + return new \stdClass(); +}); diff --git a/tests/PHPStan/Rules/Methods/data/bug-9011.php b/tests/PHPStan/Rules/Methods/data/bug-9011.php new file mode 100644 index 0000000000..8f5195809f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9011.php @@ -0,0 +1,18 @@ + + */ + public function __debugInfo(): array + { + return [ + 'a' => 1, + ]; + } + + /** + * @return array + */ + public function __serialize(): array + { + return [ + 'a' => $this->a + ]; + } +} + +class B extends A +{ + private $b; + + public function __debugInfo(): array + { + return [ + 'b' => 2, + ]; + } + + public function __serialize(): array + { + return [ + 'b' => $this->b + ]; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9542.php b/tests/PHPStan/Rules/Methods/data/bug-9542.php new file mode 100644 index 0000000000..f5f5380660 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9542.php @@ -0,0 +1,54 @@ +getMessage(); + } + + public function testClass(TranslatableInterface $translatable): void + { + if ($translatable::class !== TranslatableMessage::class) { + assertType('Bug9542\TranslatableInterface', $translatable); + return; + } + + assertType('Bug9542\TranslatableMessage', $translatable); + $translatable->getMessage(); + } + + public function testClassReverse(TranslatableInterface $translatable): void + { + if (TranslatableMessage::class !== $translatable::class) { + assertType('Bug9542\TranslatableInterface', $translatable); + return; + } + + assertType('Bug9542\TranslatableMessage', $translatable); + $translatable->getMessage(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9571-phpdocs.php b/tests/PHPStan/Rules/Methods/data/bug-9571-phpdocs.php new file mode 100644 index 0000000000..e8732082d3 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9571-phpdocs.php @@ -0,0 +1,23 @@ + $properties + * + * @return $this + */ + public function setDefaults(array $properties) + { + return $this; + } +} + +class FactoryTestDefMock +{ + use DiContainerTrait { + setDefaults as _setDefaults; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9571.php b/tests/PHPStan/Rules/Methods/data/bug-9571.php new file mode 100644 index 0000000000..e9c1e40f55 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9571.php @@ -0,0 +1,19 @@ +baseConstructor(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9577.php b/tests/PHPStan/Rules/Methods/data/bug-9577.php new file mode 100644 index 0000000000..2214d45b33 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9577.php @@ -0,0 +1,40 @@ += 8.1 + +namespace Bug9577IllegalConstructorStaticCall; + +trait StringableMessageTrait +{ + public function __construct( + private readonly \Stringable $StringableMessage, + int $code = 0, + ?\Throwable $previous = null, + ) { + parent::__construct((string) $StringableMessage, $code, $previous); + } + + public function getStringableMessage(): \Stringable + { + return $this->StringableMessage; + } +} + +class SpecializedException extends \RuntimeException +{ + use StringableMessageTrait { + StringableMessageTrait::__construct as __traitConstruct; + } + + public function __construct( + private readonly object $aService, + \Stringable $StringableMessage, + int $code = 0, + ?\Throwable $previous = null, + ) { + $this->__traitConstruct($StringableMessage, $code, $previous); + } + + public function getService(): object + { + return $this->aService; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9615.php b/tests/PHPStan/Rules/Methods/data/bug-9615.php new file mode 100644 index 0000000000..87e1faadf9 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9615.php @@ -0,0 +1,21 @@ + $items + */ + public function __construct( + private iterable $items, + ) { + // empty + } + + /** + * @return iterable + */ + protected function getItems(): iterable { + return $this->items; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9905.php b/tests/PHPStan/Rules/Methods/data/bug-9905.php new file mode 100644 index 0000000000..c018929266 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9905.php @@ -0,0 +1,22 @@ + 'user', 'extra' => 'readonly']; + } +} + +class NeverExtra implements Foo { + /** @return array{field: string} */ + public function get(): array { + return ['field' => 'user']; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9951.php b/tests/PHPStan/Rules/Methods/data/bug-9951.php new file mode 100644 index 0000000000..ab7096f27e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9951.php @@ -0,0 +1,33 @@ +|string|Expressionable $field + * @param ($field is string|Expressionable ? ($value is null ? mixed : string) : never) $operator + * @param ($operator is string ? mixed : never) $value + */ + public function addCondition($field, $operator = null, $value = null): void + { + } + + public function testStr(string $field, bool $value): void + { + $this->addCondition($field, $value); + } + + public function testMixed(mixed $field, bool $value): void + { + $this->addCondition($field, $value); + } + + public function testMixedAsUnion(string|object|null $field, bool $value): void + { + $this->addCondition($field, $value); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-SplObjectStorage-remove.php b/tests/PHPStan/Rules/Methods/data/bug-SplObjectStorage-remove.php new file mode 100644 index 0000000000..3470979dd6 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-SplObjectStorage-remove.php @@ -0,0 +1,80 @@ + */ +class HelloWorld +{ + /** @var ObjectStorage */ + private \SplObjectStorage $foo; + + /** + * @param ObjectStorage $other + * @return ObjectStorage + */ + public function removeSame(\SplObjectStorage $other): \SplObjectStorage + { + $this->foo->removeAll($other); + $this->foo->removeAllExcept($other); + + return $this->foo; + } + + /** + * @param \SplObjectStorage $other + * @return ObjectStorage + */ + public function removeNarrower(\SplObjectStorage $other): \SplObjectStorage + { + $this->foo->removeAll($other); + $this->foo->removeAllExcept($other); + + return $this->foo; + } + + /** + * @param \SplObjectStorage $other + * @return ObjectStorage + */ + public function removeWider(\SplObjectStorage $other): \SplObjectStorage + { + $this->foo->removeAll($other); + $this->foo->removeAllExcept($other); + + return $this->foo; + } + + /** + * @param \SplObjectStorage $other + * @return ObjectStorage + */ + public function removePossibleIntersect(\SplObjectStorage $other): \SplObjectStorage + { + $this->foo->removeAll($other); + $this->foo->removeAllExcept($other); + + return $this->foo; + } + + /** + * @param \SplObjectStorage $other + * @return ObjectStorage + */ + public function removeNoIntersect(\SplObjectStorage $other): \SplObjectStorage + { + $this->foo->removeAll($other); + $this->foo->removeAllExcept($other); + + return $this->foo; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-template-mixed-union-intersect.php b/tests/PHPStan/Rules/Methods/data/bug-template-mixed-union-intersect.php new file mode 100644 index 0000000000..ce0c88fbe2 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-template-mixed-union-intersect.php @@ -0,0 +1,26 @@ += 8.0 + +namespace BugTemplateMixedUnionIntersect; + +interface FooInterface +{ + public function foo(): int; +} + +/** + * @template T of mixed + * @param T $a + */ +function foo(mixed $a, FooInterface $b, mixed $c): void +{ + if ($a instanceof FooInterface) { + var_dump($a->bar()); + } + if ($c instanceof FooInterface) { + var_dump($c->bar()); + } + $d = rand() > 1 ? $a : $b; + var_dump($d->foo()); + $d = rand() > 1 ? $c : $b; + var_dump($d->foo()); +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-wrong-method-name-with-template-mixed.php b/tests/PHPStan/Rules/Methods/data/bug-wrong-method-name-with-template-mixed.php new file mode 100644 index 0000000000..e4148acbb2 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-wrong-method-name-with-template-mixed.php @@ -0,0 +1,46 @@ += 8.1 + +namespace BugWrongMethodNameWithTemplateMixed; + +class HelloWorld +{ + /** + * @template T + * @param T $val + */ + public function foo(mixed $val): void + { + if ($val instanceof \UnitEnum) { + $val::from('a'); + } + } + + /** + * @template T of mixed + * @param T $val + */ + public function foo2(mixed $val): void + { + if ($val instanceof \UnitEnum) { + $val::from('a'); + } + } + + /** + * @template T of object + * @param T $val + */ + public function foo3(mixed $val): void + { + if ($val instanceof \UnitEnum) { + $val::from('a'); + } + } + + public function foo4(mixed $val): void + { + if ($val instanceof \UnitEnum) { + $val::from('a'); + } + } +} diff --git a/tests/PHPStan/Rules/Methods/data/call-methods-is-callable.php b/tests/PHPStan/Rules/Methods/data/call-methods-is-callable.php new file mode 100644 index 0000000000..66127c810b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/call-methods-is-callable.php @@ -0,0 +1,21 @@ +test('Test\CheckIsCallable::test'); + } + + public function testClosure(\Closure $closure) + { + $this->testClosure(function () { + + }); + } + +} + diff --git a/tests/PHPStan/Rules/Methods/data/call-methods-named-params-multivariant.php b/tests/PHPStan/Rules/Methods/data/call-methods-named-params-multivariant.php new file mode 100644 index 0000000000..e9377cb61a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/call-methods-named-params-multivariant.php @@ -0,0 +1,22 @@ += 8.0 + +namespace CallMethodsNamedParamsMultivariant; + + +$xslt = new \XSLTProcessor(); +$xslt->setParameter(namespace: 'ns', name:'aaa', value: 'bbb'); +$xslt->setParameter(namespace: 'ns', name: ['aaa' => 'bbb']); +// wrong +$xslt->setParameter(namespace: 'ns', options: ['aaa' => 'bbb']); + +$pdo = new \PDO('123'); +$pdo->query(query: 'SELECT 1', fetchMode: \PDO::FETCH_ASSOC); +// wrong +$pdo->query(query: 'SELECT 1', fetchMode: \PDO::FETCH_ASSOC, colno: 1); +// wrong +$pdo->query(query: 'SELECT 1', fetchMode: \PDO::FETCH_ASSOC, className: 'Foo', constructorArgs: []); + +$stmt = new \PDOStatement(); +$stmt->setFetchMode(mode: 5); +// wrong +$stmt->setFetchMode(mode: 5, className: 'aa'); diff --git a/tests/PHPStan/Rules/Methods/data/call-methods.php b/tests/PHPStan/Rules/Methods/data/call-methods.php index 6862b778e5..12c26595e3 100644 --- a/tests/PHPStan/Rules/Methods/data/call-methods.php +++ b/tests/PHPStan/Rules/Methods/data/call-methods.php @@ -656,7 +656,7 @@ public function test(callable $str) { $this->test('date'); $this->test('nonexistentFunction'); - $this->test('Test\CheckIsCallable::test'); + // $this->test('Test\CheckIsCallable::test'); differs between php7/8; tested separately $this->test('Test\CheckIsCallable::test2'); } diff --git a/tests/PHPStan/Rules/Methods/data/call-static-method-mixed.php b/tests/PHPStan/Rules/Methods/data/call-static-method-mixed.php new file mode 100644 index 0000000000..4dcb333ed1 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/call-static-method-mixed.php @@ -0,0 +1,171 @@ += 8.0 + +namespace CallStaticMethodMixed; + +class Foo +{ + + /** + * @param mixed $explicit + */ + public function doFoo( + $implicit, + $explicit + ): void + { + $implicit::foo(); + $explicit::foo(); + } + + /** + * @template T + * @param T $t + */ + public function doBar($t): void + { + $t::foo(); + } + +} + +class Bar +{ + + /** + * @param mixed $explicit + */ + public function doFoo( + $implicit, + $explicit + ): void + { + self::doBar($implicit); + self::doBar($explicit); + + self::acceptImplicitMixed($implicit); + self::acceptImplicitMixed($explicit); + + self::acceptExplicitMixed($implicit); + self::acceptExplicitMixed($explicit); + + self::acceptVariadicArguments(...$implicit); + self::acceptVariadicArguments(...$explicit); + } + + public static function doBar(int $i): void + { + + } + + public static function acceptImplicitMixed($mixed): void + { + + } + + public static function acceptExplicitMixed(mixed $mixed): void + { + + } + + public static function acceptVariadicArguments(mixed... $args): void + { + + } + + /** + * @template T + * @param T $t + */ + public function doLorem($t): void + { + self::doBar($t); + self::acceptImplicitMixed($t); + self::acceptExplicitMixed($t); + self::acceptVariadicArguments(...$t); + } + +} + +class CallableMixed +{ + + /** + * @param callable(mixed): void $cb + */ + public static function callAcceptsExplicitMixed(callable $cb): void + { + + } + + /** + * @param callable(int): void $cb + */ + public static function callAcceptsInt(callable $cb): void + { + + } + + /** + * @param callable(): mixed $cb + */ + public static function callReturnsExplicitMixed(callable $cb): void + { + + } + + public static function callReturnsImplicitMixed(callable $cb): void + { + + } + + /** + * @param callable(): int $cb + */ + public static function callReturnsInt(callable $cb): void + { + + } + + public static function doLorem(int $i, mixed $explicitMixed, $implicitMixed): void + { + $acceptsInt = function (int $i): void { + + }; + self::callAcceptsExplicitMixed($acceptsInt); + self::callAcceptsInt($acceptsInt); + + $acceptsExplicitMixed = function (mixed $m): void { + + }; + self::callAcceptsExplicitMixed($acceptsExplicitMixed); + self::callAcceptsInt($acceptsExplicitMixed); + + $acceptsImplicitMixed = function ($m): void { + + }; + self::callAcceptsExplicitMixed($acceptsImplicitMixed); + self::callAcceptsInt($acceptsImplicitMixed); + + $returnsInt = function () use ($i): int { + return $i; + }; + self::callReturnsExplicitMixed($returnsInt); + self::callReturnsImplicitMixed($returnsInt); + self::callReturnsInt($returnsInt); + + $returnsExplicitMixed = function () use ($explicitMixed): mixed { + return $explicitMixed; + }; + self::callReturnsExplicitMixed($returnsExplicitMixed); + self::callReturnsImplicitMixed($returnsExplicitMixed); + self::callReturnsInt($returnsExplicitMixed); + + $returnsImplicitMixed = function () use ($implicitMixed): mixed { + return $implicitMixed; + }; + self::callReturnsExplicitMixed($returnsImplicitMixed); + self::callReturnsImplicitMixed($returnsImplicitMixed); + self::callReturnsInt($returnsImplicitMixed); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/call-static-methods.php b/tests/PHPStan/Rules/Methods/data/call-static-methods.php index ddcf7e2c2d..0fadae62c3 100644 --- a/tests/PHPStan/Rules/Methods/data/call-static-methods.php +++ b/tests/PHPStan/Rules/Methods/data/call-static-methods.php @@ -345,3 +345,11 @@ public function doBar() } } + +class Bug2759 { + public function sayHello(string $html): void + { + $dom = \DOMDocument::loadHTML($html, LIBXML_NOWARNING | LIBXML_NONET | LIBXML_NOERROR); + } +} + diff --git a/tests/PHPStan/Rules/Methods/data/callables-without-check-nullables.php b/tests/PHPStan/Rules/Methods/data/callables-without-check-nullables.php new file mode 100644 index 0000000000..e4d1cc923c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/callables-without-check-nullables.php @@ -0,0 +1,89 @@ +doBar(function (?float $f): ?float { + return $f; + }); + $this->doBaz(function (?float $f): ?float { + return $f; + }); + $this->doBar(function (?float $f): float { + return $f; + }); + $this->doBaz(function (?float $f): float { + return $f; + }); + + $this->doBar(function (float $f): float { + return $f; + }); + $this->doBaz(function (float $f): float { + return $f; + }); + + $this->doBar2(function (?float $f): ?float { + return $f; + }); + $this->doBaz2(function (?float $f): ?float { + return $f; + }); + $this->doBar2(function (?float $f): float { + return $f; + }); + $this->doBaz2(function (?float $f): float { + return $f; + }); + + $this->doBar2(function (float $f): float { + return $f; + }); + $this->doBaz2(function (float $f): float { + return $f; + }); + } + + /** + * @param callable(float|null): (float|null) $cb + * @return void + */ + public function doBar(callable $cb): void + { + + } + + /** + * @param Closure(float|null): (float|null) $cb + * @return void + */ + public function doBaz(Closure $cb): void + { + + } + + /** + * @param callable(float|null): float $cb + * @return void + */ + public function doBar2(callable $cb): void + { + + } + + /** + * @param Closure(float|null): float $cb + * @return void + */ + public function doBaz2(Closure $cb): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/check-missing-override-attr.php b/tests/PHPStan/Rules/Methods/data/check-missing-override-attr.php new file mode 100644 index 0000000000..0206b87068 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/check-missing-override-attr.php @@ -0,0 +1,51 @@ +bindTo(new self()); // not checked + + // overwritten + $b = function (): void { + + }; + $b->bindTo(new self()); // not checked + + $c->bindTo(new \stdClass()); // ok + $c->bindTo(new self()); // error + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/closure-bind.php b/tests/PHPStan/Rules/Methods/data/closure-bind.php index f6aa9e05b7..57b9ccf5ff 100644 --- a/tests/PHPStan/Rules/Methods/data/closure-bind.php +++ b/tests/PHPStan/Rules/Methods/data/closure-bind.php @@ -49,4 +49,33 @@ public function fooMethod(): Foo })->call(new Foo()); } + public function x(): bool + { + return 1.0; + } + + public function testClassString(): bool + { + $fx = function () { + return $this->x(); + }; + + $res = 0.0; + $res += \Closure::bind($fx, $this)(); + $res += \Closure::bind($fx, $this, 'static')(); + $res += \Closure::bind($fx, $this, Foo2::class)(); + $res += \Closure::bind($fx, $this, 'CallClosureBind\Bar2')(); + $res += \Closure::bind($fx, $this, 'CallClosureBind\Bar3')(); + + $res += $fx->bindTo($this)(); + $res += $fx->bindTo($this, 'static')(); + $res += $fx->bindTo($this, Foo2::class)(); + $res += $fx->bindTo($this, 'CallClosureBind\Bar2')(); + $res += $fx->bindTo($this, 'CallClosureBind\Bar3')(); + + return $res; + } + } + +class Bar2 extends Bar {} diff --git a/tests/PHPStan/Rules/Methods/data/conditional-param.php b/tests/PHPStan/Rules/Methods/data/conditional-param.php new file mode 100644 index 0000000000..06adf7b93e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/conditional-param.php @@ -0,0 +1,23 @@ + $flags + */ + public static function replaceCallback($demoArg, int $flags = 0): void + {} +} + +function (): void { + HelloWorld::replaceCallback(true); // correct, error expected + HelloWorld::replaceCallback("string"); // correct + + HelloWorld::replaceCallback(true, PREG_OFFSET_CAPTURE); // correct + HelloWorld::replaceCallback("string", PREG_OFFSET_CAPTURE); // correct, error expected + + HelloWorld::replaceCallback(true, PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL); // should not report error +}; diff --git a/tests/PHPStan/Rules/Methods/data/constructor-return-type.php b/tests/PHPStan/Rules/Methods/data/constructor-return-type.php new file mode 100644 index 0000000000..5f6f456ed9 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/constructor-return-type.php @@ -0,0 +1,51 @@ + $param + */ + public function invariant(Invariant $param): void + { + } + + /** + * @param Covariant $param + */ + public function covariant(Covariant $param): void + { + } + + /** + * @param Contravariant $param + */ + public function contravariant(Contravariant $param): void + { + } + + public function testInvariant(): void + { + /** @var Invariant $invariantA */ + $invariantA = new Invariant(); + $this->invariant($invariantA); + + /** @var Invariant $invariantB */ + $invariantB = new Invariant(); + $this->invariant($invariantB); + + /** @var Invariant $invariantC */ + $invariantC = new Invariant(); + $this->invariant($invariantC); + } + + public function testCovariant(): void + { + /** @var Covariant $covariantA */ + $covariantA = new Covariant(); + $this->covariant($covariantA); + + /** @var Covariant $covariantB */ + $covariantB = new Covariant(); + $this->covariant($covariantB); + + /** @var Covariant $covariantC */ + $covariantC = new Covariant(); + $this->covariant($covariantC); + } + + public function testContravariant(): void + { + /** @var Contravariant $contravariantA */ + $contravariantA = new Contravariant(); + $this->contravariant($contravariantA); + + /** @var Contravariant $contravariantB */ + $contravariantB = new Contravariant(); + $this->contravariant($contravariantB); + + /** @var Contravariant $contravariantC */ + $contravariantC = new Contravariant(); + $this->contravariant($contravariantC); + } + + /** + * @param array{Invariant} $param + */ + public function invariantArray(array $param): void + { + } + + public function testInvariantArray(): void + { + /** @var Invariant $invariantC */ + $invariantC = new Invariant(); + $this->invariantArray([$invariantC]); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/imagick-pixel.php b/tests/PHPStan/Rules/Methods/data/imagick-pixel.php new file mode 100644 index 0000000000..1f6504f1c8 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/imagick-pixel.php @@ -0,0 +1,16 @@ +getColor(); + $pixel->getColor(0); + $pixel->getColor(1); + $pixel->getColor(2); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/intersection-types.php b/tests/PHPStan/Rules/Methods/data/intersection-types.php index 6a3987d9b6..a7b11a5e28 100644 --- a/tests/PHPStan/Rules/Methods/data/intersection-types.php +++ b/tests/PHPStan/Rules/Methods/data/intersection-types.php @@ -22,7 +22,7 @@ class Ipsum } -class Foo +class FooClass { public function doFoo(Foo&Bar $a): Foo&Bar diff --git a/tests/PHPStan/Rules/Methods/data/list-return-type-covariance.php b/tests/PHPStan/Rules/Methods/data/list-return-type-covariance.php new file mode 100644 index 0000000000..7ff8636ef3 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/list-return-type-covariance.php @@ -0,0 +1,21 @@ + */ + public function returnsList(); + + /** @return array */ + public function returnsArray(); +} + +interface ListChild extends ListParent +{ + /** @return array */ + public function returnsList(); + + /** @return list */ + public function returnsArray(); +} diff --git a/tests/PHPStan/Rules/Methods/data/magic-serialization.php b/tests/PHPStan/Rules/Methods/data/magic-serialization.php new file mode 100644 index 0000000000..cadddfb876 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/magic-serialization.php @@ -0,0 +1,30 @@ + */ + public function __serialize(): array + { + return []; + } + + /** @param array $data */ + public function __unserialize(array $data): void + { + } +} + +class WrongSignature { + + public function __serialize() + { + return ''; + } + + public function __unserialize($data) + { + return ''; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/magic-signatures.php b/tests/PHPStan/Rules/Methods/data/magic-signatures.php new file mode 100644 index 0000000000..0d71caa470 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/magic-signatures.php @@ -0,0 +1,70 @@ +pure1('test'); (new Bzz())->pure2('test'); (new Bzz())->pure3('test'); + (new Bzz())->pure4('test'); + (new Bzz())->pure5('test'); }; diff --git a/tests/PHPStan/Rules/Methods/data/method-in-enum-without-body.php b/tests/PHPStan/Rules/Methods/data/method-in-enum-without-body.php new file mode 100644 index 0000000000..fb9985a585 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/method-in-enum-without-body.php @@ -0,0 +1,12 @@ += 8.1 + +namespace MethodInEnumWithoutBody; + +enum Foo +{ + + public function doFoo(): void; + + abstract public function doBar(): void; + +} diff --git a/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php b/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php index 8346689258..d5a333491b 100644 --- a/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php +++ b/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php @@ -197,3 +197,47 @@ public function unserialize($data): void } } + +class MissingParamOutType { + + /** + * @param array $a + * @param-out array $a + */ + function oneArray(&$a): void { + + } + + /** + * @param mixed $a + * @param-out \ReflectionClass $a + */ + function generics(&$a): void { + + } +} + +class MissingParamClosureThisType { + + /** + * @param-closure-this \ReflectionClass $cb + * @param callable(): void $cb + */ + function generics(callable $cb): void + { + + } + +} + +class MissingPureClosureSignatureType { + + /** + * @param pure-Closure $cb + */ + function doFoo(\Closure $cb): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/missing-serialization.php b/tests/PHPStan/Rules/Methods/data/missing-serialization.php new file mode 100644 index 0000000000..e4c53064d5 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/missing-serialization.php @@ -0,0 +1,40 @@ += 8.1 + +namespace MissingMagicSerializationMethods; + +use Serializable; + +abstract class abstractObj implements Serializable { + public function serialize() { + } + public function unserialize($data) { + } +} + +class myObj implements Serializable { + public function serialize() { + } + public function unserialize($data) { + } +} + +enum myEnum implements Serializable { + case X; + case Y; + + public function serialize() { + } + public function unserialize($data) { + } +} + +abstract class allGood implements Serializable { + public function serialize() { + } + public function unserialize($data) { + } + public function __serialize() { + } + public function __unserialize($data) { + } +} diff --git a/tests/PHPStan/Rules/Methods/data/non-empty-array.php b/tests/PHPStan/Rules/Methods/data/non-empty-array.php new file mode 100644 index 0000000000..a2e1045a91 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/non-empty-array.php @@ -0,0 +1,29 @@ + $mightBeEmpty + * @param non-empty-array $nonEmpty + * @return void + */ + public function doFoo(array $mightBeEmpty, array $nonEmpty) + { + $this->requireNonEmpty($mightBeEmpty); + $this->requireNonEmpty($nonEmpty); + $this->requireNonEmpty([]); + $this->requireNonEmpty([123]); + } + + /** + * @param non-empty-array $nonEmpty + */ + public function requireNonEmpty(array $nonEmpty) + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/nullsafe-method-call.php b/tests/PHPStan/Rules/Methods/data/nullsafe-method-call.php index dbe6ef395d..0eb89b8ae8 100644 --- a/tests/PHPStan/Rules/Methods/data/nullsafe-method-call.php +++ b/tests/PHPStan/Rules/Methods/data/nullsafe-method-call.php @@ -27,4 +27,11 @@ public function doLorem(?self $selfOrNull): void $this->doBaz($selfOrNull?->test->test); } + public function doNull(): void + { + $null = null; + $null->foo(); + $null?->foo(); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/object-shapes.php b/tests/PHPStan/Rules/Methods/data/object-shapes.php new file mode 100644 index 0000000000..248eeecc30 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/object-shapes.php @@ -0,0 +1,214 @@ +doBar(new stdClass()); + $this->doBar(new Exception()); + } + + /** + * @param object{foo: int, bar: string} $o + */ + public function doBar($o): void + { + + } + + /** + * @param object{foo: string, bar: int} $o + * @param object{foo?: int, bar: string} $p + * @param object{foo: int, bar: string} $q + */ + public function doBaz( + $o, + $p, + $q + ): void + { + $this->doBar($o); + $this->doBar($p); + $this->doBar($q); + + $this->requireStdClass($o); + $this->requireStdClass((object) []); + $this->doBar((object) ['foo' => 1, 'bar' => 'bar']); // OK + $this->doBar((object) ['foo' => 'foo', 'bar' => 1]); // Error + $this->acceptsObject($o); + } + + public function requireStdClass(stdClass $std): void + { + + } + + public function acceptsObject(object $o): void + { + $this->doBar($o); + $this->doBar(new \stdClass()); + } + +} + +class Bar +{ + + /** @var int */ + public $a; + + /** + * @param object{a: int} $o + */ + public function doFoo(object $o): void + { + $this->requireBar($o); + } + + public function requireBar(self $bar): void + { + $this->doFoo($bar); + $this->doBar($bar); + } + + /** + * @param object{a: string} $o + */ + public function doBar(object $o): void + { + + } + +} + +/** + * @property-write int $c + */ +#[\AllowDynamicProperties] +class Baz +{ + + /** @var int */ + protected $a; + + /** @var array{foo: int} */ + public $d; + + public function doFoo(): void + { + $this->doBar($this); + $this->doBaz($this); + $this->doLorem($this); + $this->doIpsum($this); + } + + /** + * @param object{a: int} $o + */ + public function doBar(object $o): void + { + + } + + /** @var int */ + public static $b; + + /** + * @param object{b: int} $o + */ + public function doBaz(object $o): void + { + + } + + /** + * @param object{c: int} $o + */ + public function doLorem(object $o): void + { + + } + + /** + * @param object{d: array{foo: string}} $o + */ + public function doIpsum(object $o): void + { + + } + +} + +class OptionalProperty +{ + + /** + * @param object{foo?: string} $o + */ + public function doFoo(object $o): void + { + $this->doBar($o); + $this->doBaz($o); + } + + /** + * @param object{foo?: int} $o + */ + public function doBar(object $o): void + { + + } + + /** + * @param object{foo: int} $o + */ + public function doBaz(object $o): void + { + + } + +} + +final class FinalClass +{ + +} + +class ClassWithFooIntProperty +{ + + /** @var int */ + public $foo; + +} + +class TestAcceptance +{ + + /** + * @param object{foo: int} $o + * @return void + */ + public function doFoo(object $o): void + { + + } + + public function doBar( + \Traversable $traversable, + FinalClass $finalClass, + ClassWithFooIntProperty $classWithFooIntProperty + ) + { + $this->doFoo($traversable); + $this->doFoo($finalClass); + $this->doFoo($classWithFooIntProperty); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/override-attribute.php b/tests/PHPStan/Rules/Methods/data/override-attribute.php new file mode 100644 index 0000000000..ca16bdba5b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/override-attribute.php @@ -0,0 +1,60 @@ += 8.0 + +namespace OverridingIndirectPrototype; + +class Foo +{ + + public function doFoo(): mixed + { + + } + +} + +class Bar extends Foo +{ + + public function doFoo(): string + { + + } + +} + +class Baz extends Bar +{ + + public function doFoo(): mixed + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/overriding-trait-methods-phpdoc.php b/tests/PHPStan/Rules/Methods/data/overriding-trait-methods-phpdoc.php new file mode 100644 index 0000000000..8a84a36c65 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/overriding-trait-methods-phpdoc.php @@ -0,0 +1,38 @@ += 8.0 + +namespace OverridingTraitMethodsPhpDoc; + +trait Foo +{ + + public function doFoo(int $i): int + { + + } + + abstract public function doBar(string $i): int; + +} + +class Bar +{ + + use Foo; + + /** + * @param positive-int $i + */ + public function doFoo(int $i): string + { + // ok, trait method not abstract + } + + /** + * @param non-empty-string $i + */ + public function doBar(string $i): int + { + // error + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/overriding-trait-methods.php b/tests/PHPStan/Rules/Methods/data/overriding-trait-methods.php new file mode 100644 index 0000000000..6e66526370 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/overriding-trait-methods.php @@ -0,0 +1,84 @@ += 8.0 + +namespace OverridingTraitMethods; + +trait Foo +{ + + public function doFoo(string $i): int + { + + } + + abstract public function doBar(string $i): int; + +} + +class Bar +{ + + use Foo; + + public function doFoo(int $i): string + { + // ok, trait method not abstract + } + + public function doBar(int $i): int + { + // error + } + +} + +trait FooPrivate +{ + + abstract private function doBar(string $i): int; + +} + +class Baz +{ + use FooPrivate; + + private function doBar(int $i): int + { + + } +} + +class Lorem +{ + use Foo; + + protected function doBar(string $i): int + { + + } +} + +class Ipsum +{ + use Foo; + + public static function doBar(string $i): int + { + + } +} + +trait FooStatic +{ + abstract public static function doBar(string $i): int; +} + +class Dolor +{ + use FooStatic; + + public function doBar(string $i): int + { + + } +} diff --git a/tests/PHPStan/Rules/Methods/data/param-out.php b/tests/PHPStan/Rules/Methods/data/param-out.php new file mode 100644 index 0000000000..51df4504bf --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/param-out.php @@ -0,0 +1,25 @@ +acceptsCallable($cb); + $this->acceptsCallable($pureCb); + $this->acceptsPureCallable($cb); + $this->acceptsPureCallable($pureCb); + $this->acceptsInt($cb); + $this->acceptsInt($pureCb); + + $this->acceptsPureCallable(function (): int { + return 1; + }); + $this->acceptsPureCallable(function (): int { + sleep(1); + + return 1; + }); + } + + /** + * @param pure-Closure $cb + */ + public function acceptsPureClosure(\Closure $cb): void + { + + } + + public function doFoo2(): void + { + $this->acceptsPureClosure(function (): int { + return 1; + }); + $this->acceptsPureClosure(function (): int { + sleep(1); + + return 1; + }); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/reflection-class-issue-8679.php b/tests/PHPStan/Rules/Methods/data/reflection-class-issue-8679.php new file mode 100644 index 0000000000..32c3937b52 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/reflection-class-issue-8679.php @@ -0,0 +1,45 @@ +test1.$this->test2.$this->test3."\n"; + } +} + +class FooClassSimpleFactory +{ + /** + * @param array $options Options for MyClass + */ + public static function getClassA(array $options = []): FooClass + { + return (new \ReflectionClass('FooClass'))->newInstanceArgs($options); + } + + /** + * @param array $options Options for MyClass + */ + public static function getClassB(array $options = []): FooClass + { + return (new \ReflectionClass('FooClass'))->newInstanceArgs($options); + } + + /** + * @param array $options Options for MyClass + */ + public static function getClassC(array $options = []): FooClass + { + return (new \ReflectionClass('FooClass'))->newInstanceArgs($options); + } +} \ No newline at end of file diff --git a/tests/PHPStan/Rules/Methods/data/required-parameter-after-optional.php b/tests/PHPStan/Rules/Methods/data/required-parameter-after-optional.php index 647a05f3af..d93cdd1bd8 100644 --- a/tests/PHPStan/Rules/Methods/data/required-parameter-after-optional.php +++ b/tests/PHPStan/Rules/Methods/data/required-parameter-after-optional.php @@ -1,4 +1,4 @@ - 8.0 namespace RequiredAfterOptional; @@ -22,4 +22,31 @@ public function doLorem(bool $foo = true, $bar): void // not OK { } + public function doDolor(?int $foo = 1, $bar): void // not OK + { + } + + public function doSit(?int $foo = null, $bar): void // not OK + { + } + + public function doAmet(int|null $foo = 1, $bar): void // not OK + { + } + + public function doConsectetur(int|null $foo = null, $bar): void // not OK + { + } + + public function doAdipiscing(mixed $foo = 1, $bar): void // not OK + { + } + + public function doElit(mixed $foo = null, $bar): void // not OK + { + } + + public function doSed(int|null $foo = null, $bar, ?int $baz = null, $qux, int $quux = 1, $quuz): void // not OK + { + } } diff --git a/tests/PHPStan/Rules/Methods/data/return-list-rule.php b/tests/PHPStan/Rules/Methods/data/return-list-rule.php new file mode 100644 index 0000000000..4827058bdf --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/return-list-rule.php @@ -0,0 +1,87 @@ + + */ +class BinaryOpEnumValueRule implements Rule +{ + + /** @var class-string */ + private string $className; + + /** + * @param class-string $operator + */ + public function __construct(string $operator, ?string $okMessage = null) + { + $this->className = $operator; + } + + public function getNodeType(): string + { + return $this->className; + } + + /** + * @param BinaryOp $node + * @return list + */ + public function processNode(Node $node, Scope $scope): array + { + $leftType = $scope->getType($node->left); + $rightType = $scope->getType($node->right); + $isDirectCompareType = true; + + if (!$this->isEnumWithValue($leftType) || !$this->isEnumWithValue($rightType)) { + $isDirectCompareType = false; + } + + $errors = []; + $leftError = $this->processOpExpression($node->left, $leftType, $node->getOperatorSigil()); + $rightError = $this->processOpExpression($node->right, $rightType, $node->getOperatorSigil()); + + if ($leftError !== null) { + $errors[] = $leftError; + } + + if ($rightError !== null && $rightError !== $leftError) { + $errors[] = $rightError; + } + + if (!$isDirectCompareType && $errors === []) { + return []; + } + + if ($isDirectCompareType && $errors === []) { + $errors[] = sprintf( + 'Cannot compare %s to %s', + $leftType->describe(VerbosityLevel::typeOnly()), + $rightType->describe(VerbosityLevel::typeOnly()), + ); + } + + return array_map(static fn (string $message) => RuleErrorBuilder::message($message)->build(), $errors); + } + + private function processOpExpression(Expr $expression, Type $expressionType, string $sigil): ?string + { + return null; + } + + private function isEnumWithValue(Type $type): bool + { + return false; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/return-list.php b/tests/PHPStan/Rules/Methods/data/return-list.php new file mode 100644 index 0000000000..1e13f0b7bf --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/return-list.php @@ -0,0 +1,21 @@ + */ + public function getList1(): array + { + return array_filter(['foo', 'bar'], 'file_exists'); + } + + /** + * @param array $array + * @return list + */ + public function getList2(array $array): array + { + return array_intersect_key(['foo', 'bar'], $array); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/return-rule-error.php b/tests/PHPStan/Rules/Methods/data/return-rule-error.php new file mode 100644 index 0000000000..799bf37279 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/return-rule-error.php @@ -0,0 +1,95 @@ + + */ +class Foo implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // ok + return [ + RuleErrorBuilder::message('foo') + ->identifier('abc') + ->build(), + ]; + } + +} + +/** + * @implements Rule + */ +class Bar implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // not ok - returns plain string + return ['foo']; + } + +} + +/** + * @implements Rule + */ +class Baz implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // not ok - missing identifier + return [ + RuleErrorBuilder::message('foo') + ->build(), + ]; + } + +} + +/** + * @implements Rule + */ +class Lorem implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // not ok - not a list + return [ + 1 => RuleErrorBuilder::message('foo') + ->identifier('abc') + ->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/return-type-class-constant.php b/tests/PHPStan/Rules/Methods/data/return-type-class-constant.php new file mode 100644 index 0000000000..59a5d51884 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/return-type-class-constant.php @@ -0,0 +1,31 @@ += 8.3 + +namespace ReturnTypeClassConstant; + +use function PHPStan\Testing\assertType; + +enum Foo +{ + + const static FOO = Foo::A; + + case A; + + public function returnStatic(): static + { + assertType('ReturnTypeClassConstant\Foo::A', self::FOO); + return self::FOO; + } + + public function returnStatic2(self $self): static + { + assertType('ReturnTypeClassConstant\Foo::A', $self::FOO); + return $self::FOO; + } + +} + +function (Foo $foo): void { + assertType('ReturnTypeClassConstant\Foo::A', Foo::FOO); + assertType('ReturnTypeClassConstant\Foo::A', $foo::FOO); +}; diff --git a/tests/PHPStan/Rules/Methods/data/returnTypes.php b/tests/PHPStan/Rules/Methods/data/returnTypes.php index 26b59ee6ec..1f94e976ab 100644 --- a/tests/PHPStan/Rules/Methods/data/returnTypes.php +++ b/tests/PHPStan/Rules/Methods/data/returnTypes.php @@ -1256,3 +1256,28 @@ public function doBaz3(): string } } + +interface MySQLiAffectedRowsReturnTypeInterface +{ + /** + * @return int|numeric-string + */ + function exec(\mysqli $connection, string $sql); +} + +final class MySQLiAffectedRowsReturnType implements MySQLiAffectedRowsReturnTypeInterface +{ + /** + * @return int<0, max>|numeric-string + */ + function exec(\mysqli $mysqli, string $sql) + { + $result = $mysqli->query($sql); + + if ($result === false || 0 > $mysqli->affected_rows) { + throw new \RuntimeException(); + } + + return $mysqli->affected_rows; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/rule-error-signature.php b/tests/PHPStan/Rules/Methods/data/rule-error-signature.php new file mode 100644 index 0000000000..1fae783da6 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/rule-error-signature.php @@ -0,0 +1,132 @@ + + */ +class Foo implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // ok + } + +} + +/** + * @implements Rule + */ +class Bar implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + /** + * @return list + */ + public function processNode(Node $node, Scope $scope): array + { + // also ok + } + +} + +/** + * @implements Rule + */ +class Baz implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + /** + * @return (string|RuleError)[] errors + */ + public function processNode(Node $node, Scope $scope): array + { + // old return type - not ok + } + +} + +/** + * @implements Rule + */ +class Lorem implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + /** + * @return string[] + */ + public function processNode(Node $node, Scope $scope): array + { + // just strings - not ok + } + +} + +/** + * @implements Rule + */ +class Ipsum implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + /** + * @return RuleError[] + */ + public function processNode(Node $node, Scope $scope): array + { + // no identifiers - not ok + } + +} + +/** + * @implements Rule + */ +class Dolor implements Rule +{ + + public function getNodeType(): string + { + return Node::class; + } + + /** + * @return IdentifierRuleError[] + */ + public function processNode(Node $node, Scope $scope): array + { + // not a list - not ok + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/static-has-method.php b/tests/PHPStan/Rules/Methods/data/static-has-method.php new file mode 100644 index 0000000000..177bde0ab4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/static-has-method.php @@ -0,0 +1,51 @@ + + */ + #[\ReturnTypeWillChange] + public function getInnerIterator() + { + + } +} diff --git a/tests/PHPStan/Rules/Methods/data/tricky-callables.php b/tests/PHPStan/Rules/Methods/data/tricky-callables.php new file mode 100644 index 0000000000..189c65c71c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/tricky-callables.php @@ -0,0 +1,84 @@ +doBar($cb); + } + + /** + * @param callable(string|null): void $cb + */ + public function doBar(callable $cb) + { + + } + +} + +class Bar +{ + + /** + * @param callable(string): void $cb + */ + public function doFoo(callable $cb) + { + $this->doBar($cb); + } + + /** + * @param callable(string=): void $cb + */ + public function doBar(callable $cb) + { + + } + +} + +class Baz +{ + + /** + * @param callable(string): void $cb + */ + public function doFoo(callable $cb) + { + $this->doBar($cb); + } + + /** + * @param callable(): void $cb + */ + public function doBar(callable $cb) + { + + } + +} + +final class TwoErrorsAtOnce +{ + /** + * @param callable(string|int $key=): bool $filter + */ + public function run(callable $filter): void + { + } +} + +function (TwoErrorsAtOnce $t): void { + $filter = static fn (): bool => true; + $t->run($filter); + + $filter = static fn (int $key): bool => true; + $t->run($filter); +}; diff --git a/tests/PHPStan/Rules/Methods/data/virtual-nullsafe-method-call.php b/tests/PHPStan/Rules/Methods/data/virtual-nullsafe-method-call.php new file mode 100644 index 0000000000..e5cd99d7a4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/virtual-nullsafe-method-call.php @@ -0,0 +1,4 @@ += 8.0 + +$foo->regularCall(); +$foo?->nullsafeCall(); diff --git a/tests/PHPStan/Rules/Methods/data/visibility-in-interace.php b/tests/PHPStan/Rules/Methods/data/visibility-in-interace.php new file mode 100644 index 0000000000..73ea93d662 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/visibility-in-interace.php @@ -0,0 +1,30 @@ + + */ + public function doFoo(): array + { + return $this->listOfBars(); + } + + /** + * @return list + */ + public function listOfBars(): array + { + return []; + } + +} + +class Test2 +{ + + /** + * @return non-empty-array + */ + public function doFoo(): array + { + return $this->nonEmptyArrayOfBars(); + } + + /** + * @return non-empty-array + */ + public function nonEmptyArrayOfBars(): array + { + /** @var Bar $b */ + $b = doFoo(); + return [$b]; + } + +} + +class Test3 +{ + + /** + * @return non-empty-list + */ + public function doFoo(): array + { + return $this->nonEmptyArrayOfBars(); + } + + /** + * @return array + */ + public function nonEmptyArrayOfBars(): array + { + return []; + } + +} diff --git a/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php b/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php index 8261059c53..377a296017 100644 --- a/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php +++ b/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php @@ -137,6 +137,16 @@ public function testMissingMixedReturnInEmptyBody(): void ]); } + public function testBug3488(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkExplicitMixedMissingReturn = true; + $this->analyse([__DIR__ . '/data/bug-3488.php'], []); + } + public function testBug3669(): void { $this->checkExplicitMixedMissingReturn = true; @@ -235,7 +245,7 @@ public function dataCheckPhpDocMissingReturn(): array /** * @dataProvider dataCheckPhpDocMissingReturn - * @param mixed[] $errors + * @param list $errors */ public function testCheckPhpDocMissingReturn(bool $checkPhpDocMissingReturn, array $errors): void { @@ -293,4 +303,10 @@ public function testBug7384(): void $this->analyse([__DIR__ . '/data/bug-7384.php'], []); } + public function testBug9309(): void + { + $this->checkExplicitMixedMissingReturn = true; + $this->analyse([__DIR__ . '/data/bug-9309.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Missing/data/bug-3488.php b/tests/PHPStan/Rules/Missing/data/bug-3488.php new file mode 100644 index 0000000000..3f187f7687 --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/bug-3488.php @@ -0,0 +1,24 @@ += 8.1 + +namespace Bug3488; + +enum EnumWithThreeCases { + case ValueA; + case ValueB; + case ValueC; +} + +function testFunction(EnumWithThreeCases $var) : int +{ + switch ($var) { + case EnumWithThreeCases::ValueA: + // some other code + return 1; + case EnumWithThreeCases::ValueB: + // some other code + return 2; + case EnumWithThreeCases::ValueC: + // some other code + return 3; + } +} diff --git a/tests/PHPStan/Rules/Missing/data/bug-9309.php b/tests/PHPStan/Rules/Missing/data/bug-9309.php new file mode 100644 index 0000000000..5a32193d9f --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/bug-9309.php @@ -0,0 +1,9 @@ + + */ +final class UsedNamesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new UsedNamesRule(); + } + + public function testSimpleUses(): void + { + $this->analyse([__DIR__ . '/data/simple-uses.php'], [ + [ + 'Cannot declare class SomeNamespace\SimpleUses because the name is already in use.', + 7, + ], + ]); + } + + public function testGroupedUses(): void + { + $this->analyse([__DIR__ . '/data/grouped-uses.php'], [ + [ + 'Cannot declare interface SomeNamespace\GroupedUses because the name is already in use.', + 10, + ], + ]); + } + + public function testSimpleUsesUnderClass(): void + { + $this->analyse([__DIR__ . '/data/simple-uses-under-class.php'], [ + [ + 'Cannot use SomeOtherNamespace\UsesUnderClass as SimpleUsesUnderClass because the name is already in use.', + 9, + ], + ]); + } + + public function testGroupedUsesUnderClass(): void + { + $this->analyse([__DIR__ . '/data/grouped-uses-under-class.php'], [ + [ + 'Cannot use SomeOtherNamespace\FooBar as FooBar because the name is already in use.', + 14, + ], + [ + 'Cannot use SomeOtherNamespace\UsesUnderClass as GroupedUsesUnderClass because the name is already in use.', + 15, + ], + ]); + } + + public function testNoNamespace(): void + { + $this->analyse([__DIR__ . '/data/no-namespace.php'], [ + [ + 'Cannot declare class NoNamespace because the name is already in use.', + 5, + ], + [ + 'Cannot declare class NoNamespace because the name is already in use.', + 9, + ], + ]); + } + + public function testMultipleNamespaces(): void + { + $this->analyse([__DIR__ . '/data/multiple-namespaces.php'], [ + [ + 'Cannot declare trait FirstNamespace\MultipleNamespaces because the name is already in use.', + 24, + ], + ]); + } + + public function testIgnoreUseFunctionAndConstant(): void + { + $this->analyse([__DIR__ . '/data/ignore-use-function-and-constant.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Names/data/grouped-uses-under-class.php b/tests/PHPStan/Rules/Names/data/grouped-uses-under-class.php new file mode 100644 index 0000000000..0c96a7b5ac --- /dev/null +++ b/tests/PHPStan/Rules/Names/data/grouped-uses-under-class.php @@ -0,0 +1,16 @@ +createReflectionProvider(); - return new ExistingNamesInGroupUseRule($broker, new ClassCaseSensitivityCheck($broker, true), true); + $reflectionProvider = $this->createReflectionProvider(); + return new ExistingNamesInGroupUseRule( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, + ); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php b/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php index 7389c4b58e..372a5b311a 100644 --- a/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php +++ b/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Namespaces; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -14,8 +16,15 @@ class ExistingNamesInUseRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new ExistingNamesInUseRule($broker, new ClassCaseSensitivityCheck($broker, true), true); + $reflectionProvider = $this->createReflectionProvider(); + return new ExistingNamesInUseRule( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, + ); } public function testRule(): void @@ -47,4 +56,17 @@ public function testRule(): void ]); } + public function testPhpstanInternalClass(): void + { + $tip = 'This is most likely unintentional. Did you mean to type \PrefixedRuntimeException?'; + + $this->analyse([__DIR__ . '/../Classes/data/phpstan-internal-class.php'], [ + [ + 'Referencing prefixed PHPStan class: _PHPStan_156ee64ba\PrefixedRuntimeException.', + 14, + $tip, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php index 06c7503109..7729265a85 100644 --- a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php @@ -19,7 +19,7 @@ protected function getRule(): Rule { return new InvalidBinaryOperationRule( new ExprPrinter(new Printer()), - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false), + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false), ); } @@ -103,11 +103,11 @@ public function testRule(): void 127, ], [ - 'Binary operation "." between array|string and \'xyz\' results in an error.', + 'Binary operation "." between list|string and \'xyz\' results in an error.', 134, ], [ - 'Binary operation "+" between (array|string) and 1 results in an error.', + 'Binary operation "+" between (list|string) and 1 results in an error.', 136, ], [ @@ -259,6 +259,11 @@ public function testBug3515(): void $this->analyse([__DIR__ . '/data/bug-3515.php'], []); } + public function testBug8827(): void + { + $this->analyse([__DIR__ . '/../../Analyser/data/bug-8827.php'], []); + } + public function testRuleWithNullsafeVariant(): void { if (PHP_VERSION_ID < 80000) { diff --git a/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php index d42c08d4ff..3221658bc4 100644 --- a/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php @@ -16,7 +16,7 @@ class InvalidComparisonOperationRuleTest extends RuleTestCase protected function getRule(): Rule { return new InvalidComparisonOperationRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false), + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false), ); } @@ -143,6 +143,14 @@ public function testRule(): void 'Comparison operation "<" between array and array|int results in an error.', 98, ], + [ + 'Comparison operation ">" between array{1} and 2147483647|9223372036854775807 results in an error.', + 115, + ], + [ + 'Comparison operation "<" between numeric-string and DateTimeImmutable results in an error.', + 119, + ], ]); } diff --git a/tests/PHPStan/Rules/Operators/data/invalid-comparison.php b/tests/PHPStan/Rules/Operators/data/invalid-comparison.php index 5c2ac5b346..57f42e6560 100644 --- a/tests/PHPStan/Rules/Operators/data/invalid-comparison.php +++ b/tests/PHPStan/Rules/Operators/data/invalid-comparison.php @@ -106,3 +106,15 @@ function (array $a, array $b) { $a == $b; $a < $b; }; + +$xml = new SimpleXMLElement('1'); +$xml->a->b == 1; +$xml->a->b > 1; + +function (): void { + [1] > PHP_INT_MAX; +}; + +function (\DateTimeImmutable $d, \DateTimeImmutable $e): void { + $d->format('U') < $e; +}; diff --git a/tests/PHPStan/Rules/PhpDoc/FunctionAssertRuleTest.php b/tests/PHPStan/Rules/PhpDoc/FunctionAssertRuleTest.php new file mode 100644 index 0000000000..3bfd8ae7a6 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/FunctionAssertRuleTest.php @@ -0,0 +1,60 @@ + + */ +class FunctionAssertRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $initializerExprTypeResolver = self::getContainer()->getByType(InitializerExprTypeResolver::class); + return new FunctionAssertRule(new AssertRuleHelper($initializerExprTypeResolver)); + } + + public function testRule(): void + { + require_once __DIR__ . '/data/function-assert.php'; + $this->analyse([__DIR__ . '/data/function-assert.php'], [ + [ + 'Asserted type int for $i with type int does not narrow down the type.', + 8, + ], + [ + 'Asserted type int for $i with type int does not narrow down the type.', + 17, + ], + [ + 'Asserted type int for $i with type int does not narrow down the type.', + 26, + ], + [ + 'Asserted type int|string for $i with type int does not narrow down the type.', + 42, + ], + [ + 'Asserted type string for $i with type int can never happen.', + 49, + ], + [ + 'Assert references unknown parameter $j.', + 56, + ], + [ + 'Asserted negated type int for $i with type int can never happen.', + 63, + ], + [ + 'Asserted negated type string for $i with type int does not narrow down the type.', + 70, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/FunctionConditionalReturnTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/FunctionConditionalReturnTypeRuleTest.php index 488386bd53..0d4b491158 100644 --- a/tests/PHPStan/Rules/PhpDoc/FunctionConditionalReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/FunctionConditionalReturnTypeRuleTest.php @@ -63,4 +63,9 @@ public function testRule(): void ]); } + public function testBug8609(): void + { + $this->analyse([__DIR__ . '/data/bug-8609-function.php'], []); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php index e033737170..a66be60a2d 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php @@ -2,10 +2,10 @@ namespace PHPStan\Rules\PhpDoc; -use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -15,7 +15,7 @@ class IncompatibleClassConstantPhpDocTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new IncompatibleClassConstantPhpDocTypeRule(new GenericObjectTypeCheck(), new UnresolvableTypeHelper(), self::getContainer()->getByType(InitializerExprTypeResolver::class)); + return new IncompatibleClassConstantPhpDocTypeRule(new GenericObjectTypeCheck(), new UnresolvableTypeHelper()); } public function testRule(): void @@ -25,33 +25,29 @@ public function testRule(): void 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Foo::FOO contains unresolvable type.', 9, ], - [ - 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Foo::BAZ with type string is incompatible with value 1.', - 17, - ], - [ - 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Foo::DOLOR with type IncompatibleClassConstantPhpDoc\Foo is incompatible with value 1.', - 26, - ], [ 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Foo::DOLOR contains generic type IncompatibleClassConstantPhpDoc\Foo but class IncompatibleClassConstantPhpDoc\Foo is not generic.', - 26, - ], - [ - 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Bar::BAZ with type string is incompatible with value 2.', - 35, + 12, ], ]); } - public function testBug7352(): void + public function testNativeType(): void { - $this->analyse([__DIR__ . '/data/bug-7352.php'], []); - } + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } - public function testBug7352WithSubNamespace(): void - { - $this->analyse([__DIR__ . '/data/bug-7352-with-sub-namespace.php'], []); + $this->analyse([__DIR__ . '/data/incompatible-class-constant-phpdoc-native-type.php'], [ + [ + 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDocNativeType\Foo::BAZ with type string is incompatible with native type int.', + 14, + ], + [ + 'PHPDoc tag @var for constant IncompatibleClassConstantPhpDocNativeType\Foo::LOREM with type int|string is not subtype of native type int.', + 17, + ], + ]); } } diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php index c866a23d34..223fd616b0 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php @@ -2,7 +2,11 @@ namespace PHPStan\Rules\PhpDoc; +use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Generics\GenericObjectTypeCheck; +use PHPStan\Rules\Generics\TemplateTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; @@ -16,10 +20,25 @@ class IncompatiblePhpDocTypeRuleTest extends RuleTestCase protected function getRule(): Rule { + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); + return new IncompatiblePhpDocTypeRule( self::getContainer()->getByType(FileTypeMapper::class), new GenericObjectTypeCheck(), new UnresolvableTypeHelper(), + new GenericCallableRuleHelper( + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), + ), ); } @@ -154,6 +173,32 @@ public function testRule(): void 283, 'Write @template TFoo of int to fix this.', ], + [ + 'Call-site variance of covariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @param for parameter $foo is redundant, template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric has the same variance.', + 301, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of covariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @return is redundant, template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric has the same variance.', + 301, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @param for parameter $foo is in conflict with covariant template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric.', + 319, + ], + [ + 'Call-site variance of contravariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @return is in conflict with covariant template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric.', + 319, + ], + [ + 'PHPDoc tag @param for parameter $cb contains unresolvable type.', + 328, + ], + [ + 'PHPDoc tag @param for parameter $cl contains unresolvable type.', + 328, + ], ]); } @@ -233,4 +278,168 @@ public function testConditionalReturnType(): void ]); } + public function testParamOut(): void + { + $this->analyse([__DIR__ . '/data/param-out.php'], [ + [ + 'PHPDoc tag @param-out references unknown parameter: $z', + 23, + ], + [ + 'Parameter $i for PHPDoc tag @param-out is not passed by reference.', + 37, + ], + [ + 'PHPDoc tag @param-out for parameter $i contains unresolvable type.', + 44, + ], + [ + 'PHPDoc tag @param-out for parameter $i contains generic type Exception but class Exception is not generic.', + 51, + ], + [ + 'Generic type ParamOutPhpDocRule\FooBar in PHPDoc tag @param-out for parameter $i does not specify all template types of class ParamOutPhpDocRule\FooBar: T, TT', + 58, + ], + [ + 'Type mixed in generic type ParamOutPhpDocRule\FooBar in PHPDoc tag @param-out for parameter $i is not subtype of template type T of int of class ParamOutPhpDocRule\FooBar.', + 58, + ], + [ + 'Generic type ParamOutPhpDocRule\FooBar in PHPDoc tag @param-out for parameter $i does not specify all template types of class ParamOutPhpDocRule\FooBar: T, TT', + 65, + ], + + ]); + } + + public function testBug10097(): void + { + $this->analyse([__DIR__ . '/data/bug-10097.php'], []); + } + + public function testGenericCallables(): void + { + $this->analyse([__DIR__ . '/data/generic-callables-incompatible.php'], [ + [ + 'PHPDoc tag @param for parameter $existingClass template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 11, + ], + [ + 'PHPDoc tag @param for parameter $existingTypeAlias template of Closure(TypeAlias): TypeAlias cannot have existing type alias TypeAlias as its name.', + 18, + ], + [ + 'PHPDoc tag @param for parameter $invalidBoundType template T of Closure(T): T has invalid bound type GenericCallablesIncompatible\Invalid.', + 25, + ], + [ + 'PHPDoc tag @param for parameter $notSupported template T of Closure(T): T with bound type null is not supported.', + 32, + ], + [ + 'PHPDoc tag @param for parameter $shadows template T of Closure(T): T shadows @template T for function GenericCallablesIncompatible\testShadowFunction.', + 40, + ], + [ + 'PHPDoc tag @param-out for parameter $existingClass template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 47, + ], + [ + 'PHPDoc tag @param for parameter $shadows template T of Closure(T): T shadows @template T for method GenericCallablesIncompatible\Test::testShadowMethod.', + 60, + ], + [ + 'PHPDoc tag @param for parameter $shadows template U of Closure(T): T shadows @template U for class GenericCallablesIncompatible\Test.', + 60, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T shadows @template T for method GenericCallablesIncompatible\Test::testShadowMethodReturn.', + 68, + ], + [ + 'PHPDoc tag @return template U of Closure(T): T shadows @template U for class GenericCallablesIncompatible\Test.', + 68, + ], + [ + 'PHPDoc tag @return template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 76, + ], + [ + 'PHPDoc tag @return template of Closure(TypeAlias): TypeAlias cannot have existing type alias TypeAlias as its name.', + 83, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T has invalid bound type GenericCallablesIncompatible\Invalid.', + 90, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T with bound type null is not supported.', + 97, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T shadows @template T for function GenericCallablesIncompatible\testShadowFunctionReturn.', + 105, + ], + [ + 'PHPDoc tag @param for parameter $existingClass template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 117, + ], + [ + 'PHPDoc tag @param for parameter $existingTypeAlias template of Closure(TypeAlias): TypeAlias cannot have existing type alias TypeAlias as its name.', + 124, + ], + [ + 'PHPDoc tag @param for parameter $invalidBoundType template T of Closure(T): T has invalid bound type GenericCallablesIncompatible\Invalid.', + 131, + ], + [ + 'PHPDoc tag @param for parameter $notSupported template T of Closure(T): T with bound type null is not supported.', + 138, + ], + [ + 'PHPDoc tag @return template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 145, + ], + [ + 'PHPDoc tag @return template of Closure(TypeAlias): TypeAlias cannot have existing type alias TypeAlias as its name.', + 152, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T has invalid bound type GenericCallablesIncompatible\Invalid.', + 159, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T with bound type null is not supported.', + 166, + ], + [ + 'PHPDoc tag @param-out for parameter $existingClass template T of Closure(T): T shadows @template T for function GenericCallablesIncompatible\shadowsParamOut.', + 175, + ], + [ + 'PHPDoc tag @param-out for parameter $existingClasses template T of Closure(T): T shadows @template T for function GenericCallablesIncompatible\shadowsParamOutArray.', + 183, + ], + [ + 'PHPDoc tag @return template T of Closure(T): T shadows @template T for function GenericCallablesIncompatible\shadowsReturnArray.', + 191, + ], + [ + 'PHPDoc tag @param for parameter $shadows template T of Closure(T): T shadows @template T for class GenericCallablesIncompatible\Test3.', + 203, + ], + ]); + } + + public function testBug10622(): void + { + $this->analyse([__DIR__ . '/data/bug-10622.php'], []); + } + + public function testBug10622B(): void + { + $this->analyse([__DIR__ . '/data/bug-10622b.php'], []); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php index 5db3212403..d8ec3604b3 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php @@ -2,7 +2,11 @@ namespace PHPStan\Rules\PhpDoc; +use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Generics\GenericObjectTypeCheck; +use PHPStan\Rules\Generics\TemplateTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -14,9 +18,24 @@ class IncompatiblePropertyPhpDocTypeRuleTest extends RuleTestCase protected function getRule(): Rule { + $reflectionProvider = $this->createReflectionProvider(); + $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $reflectionProvider); + return new IncompatiblePropertyPhpDocTypeRule( new GenericObjectTypeCheck(), new UnresolvableTypeHelper(), + new GenericCallableRuleHelper( + new TemplateTypeCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new GenericObjectTypeCheck(), + $typeAliasResolver, + true, + ), + ), ); } @@ -59,6 +78,15 @@ public function testRule(): void 'PHPDoc tag @var for property InvalidPhpDoc\FooWithProperty::$unknownClassConstant2 contains unresolvable type.', 45, ], + [ + 'Call-site variance of covariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @var for property InvalidPhpDoc\FooWithProperty::$genericRedundantTypeProjection is redundant, template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric has the same variance.', + 51, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @var for property InvalidPhpDoc\FooWithProperty::$genericIncompatibleTypeProjection is in conflict with covariant template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric.', + 57, + ], ]); } @@ -136,4 +164,35 @@ public function testBug4227(): void $this->analyse([__DIR__ . '/data/bug-4227.php'], []); } + public function testBug7240(): void + { + $this->analyse([__DIR__ . '/data/bug-7240.php'], []); + } + + public function testGenericCallables(): void + { + $this->analyse([__DIR__ . '/data/generic-callable-properties.php'], [ + [ + 'PHPDoc tag @var template T of Closure(T): T shadows @template T for class GenericCallableProperties\Test.', + 16, + ], + [ + 'PHPDoc tag @var template of Closure(stdClass): stdClass cannot have existing class stdClass as its name.', + 21, + ], + [ + 'PHPDoc tag @var template of callable(TypeAlias): TypeAlias cannot have existing type alias TypeAlias as its name.', + 26, + ], + [ + 'PHPDoc tag @var template TNull of callable(TNull): TNull with bound type null is not supported.', + 31, + ], + [ + 'PHPDoc tag @var template TInvalid of callable(TInvalid): TInvalid has invalid bound type GenericCallableProperties\Invalid.', + 36, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatibleSelfOutTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatibleSelfOutTypeRuleTest.php new file mode 100644 index 0000000000..96ea886257 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/IncompatibleSelfOutTypeRuleTest.php @@ -0,0 +1,33 @@ + + */ +class IncompatibleSelfOutTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new IncompatibleSelfOutTypeRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/incompatible-self-out-type.php'], [ + [ + 'Self-out type int of method IncompatibleSelfOutType\A::three is not subtype of IncompatibleSelfOutType\A.', + 23, + ], + [ + 'Self-out type IncompatibleSelfOutType\A|null of method IncompatibleSelfOutType\A::four is not subtype of IncompatibleSelfOutType\A.', + 28, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php index fda7477219..7995c696f4 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function array_merge; /** * @extends RuleTestCase @@ -13,26 +14,60 @@ class InvalidPHPStanDocTagRuleTest extends RuleTestCase { + private bool $checkAllInvalidPhpDocs; + protected function getRule(): Rule { return new InvalidPHPStanDocTagRule( self::getContainer()->getByType(Lexer::class), self::getContainer()->getByType(PhpDocParser::class), + $this->checkAllInvalidPhpDocs, ); } - public function testRule(): void + public function dataRule(): iterable { - $this->analyse([__DIR__ . '/data/invalid-phpstan-doc.php'], [ + $errors = [ [ 'Unknown PHPDoc tag: @phpstan-extens', - 7, + 6, ], [ 'Unknown PHPDoc tag: @phpstan-pararm', - 14, + 11, + ], + [ + 'Unknown PHPDoc tag: @phpstan-varr', + 43, + ], + [ + 'Unknown PHPDoc tag: @phpstan-varr', + 46, + ], + ]; + yield [false, $errors]; + yield [true, array_merge($errors, [ + [ + 'Unknown PHPDoc tag: @phpstan-varr', + 56, ], - ]); + ])]; + } + + /** + * @dataProvider dataRule + * @param list $expectedErrors + */ + public function testRule(bool $checkAllInvalidPhpDocs, array $expectedErrors): void + { + $this->checkAllInvalidPhpDocs = $checkAllInvalidPhpDocs; + $this->analyse([__DIR__ . '/data/invalid-phpstan-doc.php'], $expectedErrors); + } + + public function testBug8697(): void + { + $this->checkAllInvalidPhpDocs = true; + $this->analyse([__DIR__ . '/data/bug-8697.php'], []); } } diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleNoBleedingEdgeTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleNoBleedingEdgeTest.php new file mode 100644 index 0000000000..b0bb271349 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleNoBleedingEdgeTest.php @@ -0,0 +1,146 @@ + + */ +class InvalidPhpDocTagValueRuleNoBleedingEdgeTest extends RuleTestCase +{ + + private bool $checkAllInvalidPhpDocs; + + protected function getRule(): Rule + { + return new InvalidPhpDocTagValueRule( + self::getContainer()->getByType(Lexer::class), + self::getContainer()->getByType(PhpDocParser::class), + $this->checkAllInvalidPhpDocs, + false, + ); + } + + public function dataRule(): iterable + { + $errors = [ + [ + 'PHPDoc tag @param has invalid value (): Unexpected token "\n * ", expected type at offset 13', + 25, + ], + [ + 'PHPDoc tag @param has invalid value (A & B | C $paramNameA): Unexpected token "|", expected variable at offset 72', + 25, + ], + [ + 'PHPDoc tag @param has invalid value ((A & B $paramNameB): Unexpected token "$paramNameB", expected \')\' at offset 105', + 25, + ], + [ + 'PHPDoc tag @param has invalid value (~A & B $paramNameC): Unexpected token "~A", expected type at offset 127', + 25, + ], + [ + 'PHPDoc tag @var has invalid value (): Unexpected token "\n * ", expected type at offset 156', + 25, + ], + [ + 'PHPDoc tag @var has invalid value ($invalid): Unexpected token "$invalid", expected type at offset 165', + 25, + ], + [ + 'PHPDoc tag @var has invalid value ($invalid Foo): Unexpected token "$invalid", expected type at offset 182', + 25, + ], + [ + 'PHPDoc tag @return has invalid value (): Unexpected token "\n * ", expected type at offset 208', + 25, + ], + [ + 'PHPDoc tag @return has invalid value ([int, string]): Unexpected token "[", expected type at offset 220', + 25, + ], + [ + 'PHPDoc tag @return has invalid value (A & B | C): Unexpected token "|", expected TOKEN_OTHER at offset 251', + 25, + ], + [ + 'PHPDoc tag @var has invalid value (\\\Foo|\Bar $test): Unexpected token "\\\\\\\Foo|\\\Bar", expected type at offset 9', + 29, + ], + [ + 'PHPDoc tag @var has invalid value ((Foo|Bar): Unexpected token "*/", expected \')\' at offset 18', + 62, + ], + [ + 'PHPDoc tag @throws has invalid value ((\Exception): Unexpected token "*/", expected \')\' at offset 24', + 72, + ], + [ + 'PHPDoc tag @var has invalid value ((Foo|Bar): Unexpected token "*/", expected \')\' at offset 18', + 81, + ], + [ + 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15', + 89, + ], + [ + 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15', + 92, + ], + ]; + + yield [false, $errors]; + yield [true, array_merge($errors, [ + [ + 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15', + 102, + ], + ])]; + } + + /** + * @dataProvider dataRule + * @param list $expectedErrors + */ + public function testRule(bool $checkAllInvalidPhpDocs, array $expectedErrors): void + { + $this->checkAllInvalidPhpDocs = $checkAllInvalidPhpDocs; + $this->analyse([__DIR__ . '/data/invalid-phpdoc.php'], $expectedErrors); + } + + public function testBug4731(): void + { + $this->checkAllInvalidPhpDocs = true; + $this->analyse([__DIR__ . '/data/bug-4731.php'], []); + } + + public function testBug4731WithoutFirstTag(): void + { + $this->checkAllInvalidPhpDocs = true; + $this->analyse([__DIR__ . '/data/bug-4731-no-first-tag.php'], []); + } + + public function testInvalidTypeInTypeAlias(): void + { + $this->checkAllInvalidPhpDocs = true; + $this->analyse([__DIR__ . '/data/invalid-type-type-alias.php'], [ + [ + 'PHPDoc tag @phpstan-type InvalidFoo has invalid value: Unexpected token "{", expected TOKEN_PHPDOC_EOL at offset 65', + 15, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + // reset bleedingEdge + return []; + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php index a704272a6c..c726559432 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function array_merge; /** * @extends RuleTestCase @@ -13,88 +14,137 @@ class InvalidPhpDocTagValueRuleTest extends RuleTestCase { + private bool $checkAllInvalidPhpDocs; + protected function getRule(): Rule { return new InvalidPhpDocTagValueRule( self::getContainer()->getByType(Lexer::class), self::getContainer()->getByType(PhpDocParser::class), + $this->checkAllInvalidPhpDocs, + true, ); } - public function testRule(): void + public function dataRule(): iterable { - $this->analyse([__DIR__ . '/data/invalid-phpdoc.php'], [ + $errors = [ [ - 'PHPDoc tag @param has invalid value (): Unexpected token "\n * ", expected type at offset 13', - 25, + 'PHPDoc tag @param has invalid value (): Unexpected token "\n * ", expected type at offset 13 on line 2', + 6, ], [ - 'PHPDoc tag @param has invalid value (A & B | C $paramNameA): Unexpected token "|", expected variable at offset 72', - 25, + 'PHPDoc tag @param has invalid value (A & B | C $paramNameA): Unexpected token "|", expected variable at offset 72 on line 5', + 9, ], [ - 'PHPDoc tag @param has invalid value ((A & B $paramNameB): Unexpected token "$paramNameB", expected \')\' at offset 105', - 25, + 'PHPDoc tag @param has invalid value ((A & B $paramNameB): Unexpected token "$paramNameB", expected \')\' at offset 105 on line 6', + 10, ], [ - 'PHPDoc tag @param has invalid value (~A & B $paramNameC): Unexpected token "~A", expected type at offset 127', - 25, + 'PHPDoc tag @param has invalid value (~A & B $paramNameC): Unexpected token "~A", expected type at offset 127 on line 7', + 11, ], [ - 'PHPDoc tag @var has invalid value (): Unexpected token "\n * ", expected type at offset 156', - 25, + 'PHPDoc tag @var has invalid value (): Unexpected token "\n * ", expected type at offset 156 on line 9', + 13, ], [ - 'PHPDoc tag @var has invalid value ($invalid): Unexpected token "$invalid", expected type at offset 165', - 25, + 'PHPDoc tag @var has invalid value ($invalid): Unexpected token "$invalid", expected type at offset 165 on line 10', + 14, ], [ - 'PHPDoc tag @var has invalid value ($invalid Foo): Unexpected token "$invalid", expected type at offset 182', - 25, + 'PHPDoc tag @var has invalid value ($invalid Foo): Unexpected token "$invalid", expected type at offset 182 on line 11', + 15, ], [ - 'PHPDoc tag @return has invalid value (): Unexpected token "\n * ", expected type at offset 208', - 25, + 'PHPDoc tag @return has invalid value (): Unexpected token "\n * ", expected type at offset 208 on line 13', + 17, ], [ - 'PHPDoc tag @return has invalid value ([int, string]): Unexpected token "[", expected type at offset 220', - 25, + 'PHPDoc tag @return has invalid value ([int, string]): Unexpected token "[", expected type at offset 220 on line 14', + 18, ], [ - 'PHPDoc tag @return has invalid value (A & B | C): Unexpected token "|", expected TOKEN_OTHER at offset 251', - 25, + 'PHPDoc tag @return has invalid value (A & B | C): Unexpected token "|", expected TOKEN_OTHER at offset 251 on line 15', + 19, ], [ - 'PHPDoc tag @var has invalid value (\\\Foo|\Bar $test): Unexpected token "\\\\\\\Foo|\\\Bar", expected type at offset 9', - 29, + 'PHPDoc tag @var has invalid value (\\\Foo|\Bar $test): Unexpected token "\\\\\\\Foo|\\\Bar", expected type at offset 9 on line 1', + 28, ], [ - 'PHPDoc tag @var has invalid value (callable(int)): Unexpected token "(", expected TOKEN_HORIZONTAL_WS at offset 17', - 59, + 'PHPDoc tag @var has invalid value (callable(int)): Unexpected token "(", expected TOKEN_HORIZONTAL_WS at offset 17 on line 1', + 58, ], [ - 'PHPDoc tag @var has invalid value ((Foo|Bar): Unexpected token "*/", expected \')\' at offset 18', - 62, + 'PHPDoc tag @var has invalid value ((Foo|Bar): Unexpected token "*/", expected \')\' at offset 18 on line 1', + 61, ], [ - 'PHPDoc tag @throws has invalid value ((\Exception): Unexpected token "*/", expected \')\' at offset 24', - 72, + 'PHPDoc tag @throws has invalid value ((\Exception): Unexpected token "*/", expected \')\' at offset 24 on line 1', + 71, ], [ - 'PHPDoc tag @var has invalid value ((Foo|Bar): Unexpected token "*/", expected \')\' at offset 18', - 81, + 'PHPDoc tag @var has invalid value ((Foo|Bar): Unexpected token "*/", expected \')\' at offset 18 on line 1', + 80, ], - ]); + [ + 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15 on line 1', + 88, + ], + [ + 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15 on line 1', + 91, + ], + ]; + + yield [false, $errors]; + yield [true, array_merge($errors, [ + [ + 'PHPDoc tag @var has invalid value ((Foo&): Unexpected token "*/", expected type at offset 15 on line 1', + 101, + ], + ])]; + } + + /** + * @dataProvider dataRule + * @param list $expectedErrors + */ + public function testRule(bool $checkAllInvalidPhpDocs, array $expectedErrors): void + { + $this->checkAllInvalidPhpDocs = $checkAllInvalidPhpDocs; + $this->analyse([__DIR__ . '/data/invalid-phpdoc.php'], $expectedErrors); } public function testBug4731(): void { + $this->checkAllInvalidPhpDocs = true; $this->analyse([__DIR__ . '/data/bug-4731.php'], []); } public function testBug4731WithoutFirstTag(): void { + $this->checkAllInvalidPhpDocs = true; $this->analyse([__DIR__ . '/data/bug-4731-no-first-tag.php'], []); } + public function testInvalidTypeInTypeAlias(): void + { + $this->checkAllInvalidPhpDocs = true; + $this->analyse([__DIR__ . '/data/invalid-type-type-alias.php'], [ + [ + 'PHPDoc tag @phpstan-type InvalidFoo has invalid value: Unexpected token "{", expected TOKEN_PHPDOC_EOL at offset 65 on line 3', + 7, + ], + ]); + } + + public function testIgnoreWithinPhpDoc(): void + { + $this->checkAllInvalidPhpDocs = true; + $this->analyse([__DIR__ . '/data/ignore-line-within-phpdoc.php'], []); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php index 8ee68cf319..2b82619cc9 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\PhpDoc; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\Rule; @@ -18,13 +20,16 @@ class InvalidPhpDocVarTagTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new InvalidPhpDocVarTagTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - $broker, - new ClassCaseSensitivityCheck($broker, true), + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), new GenericObjectTypeCheck(), - new MissingTypehintCheck($broker, true, true, true, true, []), + new MissingTypehintCheck(true, true, true, true, []), new UnresolvableTypeHelper(), true, true, @@ -94,8 +99,17 @@ public function testRule(): void 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ - 'PHPDoc tag @var for variable $foo contains unknown class InvalidVarTagType\Blabla.', + 'Call-site variance of covariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @var for variable $test is redundant, template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric has the same variance.', 67, + 'You can safely remove the call-site variance annotation.', + ], + [ + 'Call-site variance of contravariant int in generic type InvalidPhpDocDefinitions\FooCovariantGeneric in PHPDoc tag @var for variable $test is in conflict with covariant template type T of class InvalidPhpDocDefinitions\FooCovariantGeneric.', + 73, + ], + [ + 'PHPDoc tag @var for variable $foo contains unknown class InvalidVarTagType\Blabla.', + 79, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], ]); @@ -151,4 +165,15 @@ public function testBug6348(): void $this->analyse([__DIR__ . '/data/bug-6348.php'], []); } + public function testBug9055(): void + { + $this->analyse([__DIR__ . '/data/bug-9055.php'], [ + [ + 'PHPDoc tag @var for variable $x contains unknown class Bug9055\uncheckedNotExisting.', + 16, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php index 90120196da..2328aeb0d7 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php @@ -77,6 +77,28 @@ public function testInheritedPhpDocs(): void ]); } + public function testThrowsWithRequireExtends(): void + { + $this->analyse([__DIR__ . '/data/throws-with-require.php'], [ + [ + 'PHPDoc tag @throws with type ThrowsWithRequire\\RequiresExtendsStdClassInterface is not subtype of Throwable', + 25, + ], + [ + 'PHPDoc tag @throws with type DateTimeInterface|ThrowsWithRequire\\RequiresExtendsExceptionInterface is not subtype of Throwable', + 39, + ], + [ + 'PHPDoc tag @throws with type Exception|ThrowsWithRequire\\RequiresExtendsStdClassInterface is not subtype of Throwable', + 46, + ], + [ + 'PHPDoc tag @throws with type Iterator&ThrowsWithRequire\\RequiresExtendsStdClassInterface is not subtype of Throwable', + 74, + ], + ]); + } + public function dataMergeInheritedPhpDocs(): array { return [ diff --git a/tests/PHPStan/Rules/PhpDoc/MethodAssertRuleTest.php b/tests/PHPStan/Rules/PhpDoc/MethodAssertRuleTest.php new file mode 100644 index 0000000000..932a09c299 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/MethodAssertRuleTest.php @@ -0,0 +1,75 @@ + + */ +class MethodAssertRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $initializerExprTypeResolver = self::getContainer()->getByType(InitializerExprTypeResolver::class); + return new MethodAssertRule(new AssertRuleHelper($initializerExprTypeResolver)); + } + + public function testRule(): void + { + require_once __DIR__ . '/data/method-assert.php'; + $this->analyse([__DIR__ . '/data/method-assert.php'], [ + [ + 'Asserted type int for $i with type int does not narrow down the type.', + 10, + ], + [ + 'Asserted type int for $i with type int does not narrow down the type.', + 19, + ], + [ + 'Asserted type int for $i with type int does not narrow down the type.', + 28, + ], + [ + 'Asserted type int|string for $i with type int does not narrow down the type.', + 44, + ], + [ + 'Asserted type string for $i with type int can never happen.', + 51, + ], + [ + 'Assert references unknown parameter $j.', + 58, + ], + [ + 'Asserted negated type int for $i with type int can never happen.', + 65, + ], + [ + 'Asserted negated type string for $i with type int does not narrow down the type.', + 72, + ], + ]); + } + + public function testBug10573(): void + { + $this->analyse([__DIR__ . '/data/bug-10573.php'], []); + } + + public function testBug10214(): void + { + $this->analyse([__DIR__ . '/data/bug-10214.php'], []); + } + + public function testBug10594(): void + { + $this->analyse([__DIR__ . '/data/bug-10594.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php index ddd8bd8222..48258202dc 100644 --- a/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php @@ -23,10 +23,6 @@ public function testRule(): void 'Conditional return type uses subject type stdClass which is not part of PHPDoc @template tags.', 48, ], - [ - 'Conditional return type uses subject type TAboveClass which is not part of PHPDoc @template tags.', - 57, - ], [ 'Conditional return type references unknown parameter $j.', 65, @@ -67,7 +63,36 @@ public function testRule(): void 'Condition "array{foo: string} is array{foo: int}" in conditional return type is always false.', 156, ], + [ + 'Condition "int is int" in conditional return type is always true.', + 185, + ], + ]); + } + + public function testBug8284(): void + { + $this->analyse([__DIR__ . '/data/bug-8284.php'], [ + [ + 'Conditional return type references unknown parameter $callable.', + 14, + ], ]); } + public function testBug8609(): void + { + $this->analyse([__DIR__ . '/data/bug-8609.php'], []); + } + + public function testBug8408(): void + { + $this->analyse([__DIR__ . '/data/bug-8408.php'], []); + } + + public function testBug7310(): void + { + $this->analyse([__DIR__ . '/data/bug-7310.php'], []); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionClassRuleTest.php b/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionClassRuleTest.php new file mode 100644 index 0000000000..5a24e75502 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionClassRuleTest.php @@ -0,0 +1,88 @@ + + */ +class RequireExtendsDefinitionClassRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new RequireExtendsDefinitionClassRule( + new RequireExtendsCheck( + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, + ), + ); + } + + public function testRule(): void + { + $enumError = 'PHPDoc tag @phpstan-require-extends cannot contain non-class type IncompatibleRequireExtends\SomeEnum.'; + $enumTip = null; + if (PHP_VERSION_ID < 80100) { + $enumError = 'PHPDoc tag @phpstan-require-extends contains unknown class IncompatibleRequireExtends\SomeEnum.'; + $enumTip = 'Learn more at https://phpstan.org/user-guide/discovering-symbols'; + } + + $this->analyse([__DIR__ . '/data/incompatible-require-extends.php'], [ + [ + 'PHPDoc tag @phpstan-require-extends cannot contain non-class type IncompatibleRequireExtends\SomeTrait.', + 8, + ], + [ + 'PHPDoc tag @phpstan-require-extends cannot contain non-class type IncompatibleRequireExtends\SomeInterface.', + 13, + ], + [ + $enumError, + 18, + $enumTip, + ], + [ + 'PHPDoc tag @phpstan-require-extends contains unknown class IncompatibleRequireExtends\TypeDoesNotExist.', + 23, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'PHPDoc tag @phpstan-require-extends contains non-object type int.', + 34, + ], + [ + 'PHPDoc tag @phpstan-require-extends is only valid on trait or interface.', + 39, + ], + [ + 'PHPDoc tag @phpstan-require-extends is only valid on trait or interface.', + 44, + ], + [ + 'PHPDoc tag @phpstan-require-extends cannot contain final class IncompatibleRequireExtends\SomeFinalClass.', + 121, + ], + [ + 'PHPDoc tag @phpstan-require-extends contains non-object type IncompatibleRequireExtends\UnresolvableExtendsInterface&stdClass.', + 135, + ], + [ + 'PHPDoc tag @phpstan-require-extends can only be used once.', + 178, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionTraitRuleTest.php b/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionTraitRuleTest.php new file mode 100644 index 0000000000..746c3b9007 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/RequireExtendsDefinitionTraitRuleTest.php @@ -0,0 +1,51 @@ + + */ +class RequireExtendsDefinitionTraitRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new RequireExtendsDefinitionTraitRule( + $reflectionProvider, + new RequireExtendsCheck( + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, + ), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/incompatible-require-extends.php'], [ + [ + 'PHPDoc tag @phpstan-require-extends cannot contain final class IncompatibleRequireExtends\SomeFinalClass.', + 126, + ], + [ + 'PHPDoc tag @phpstan-require-extends contains non-object type *NEVER*.', + 140, + ], + [ + 'PHPDoc tag @phpstan-require-extends can only be used once.', + 171, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionClassRuleTest.php b/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionClassRuleTest.php new file mode 100644 index 0000000000..06cb9fdf88 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionClassRuleTest.php @@ -0,0 +1,35 @@ + + */ +class RequireImplementsDefinitionClassRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new RequireImplementsDefinitionClassRule(); + } + + public function testRule(): void + { + $expectedErrors = [ + [ + 'PHPDoc tag @phpstan-require-implements is only valid on trait.', + 40, + ], + [ + 'PHPDoc tag @phpstan-require-implements is only valid on trait.', + 45, + ], + ]; + + $this->analyse([__DIR__ . '/data/incompatible-require-implements.php'], $expectedErrors); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionTraitRuleTest.php b/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionTraitRuleTest.php new file mode 100644 index 0000000000..ad2f2f7cba --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/RequireImplementsDefinitionTraitRuleTest.php @@ -0,0 +1,73 @@ + + */ +class RequireImplementsDefinitionTraitRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new RequireImplementsDefinitionTraitRule( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, + ); + } + + public function testRule(): void + { + $enumError = 'PHPDoc tag @phpstan-require-implements cannot contain non-interface type IncompatibleRequireImplements\SomeEnum.'; + $enumTip = null; + if (PHP_VERSION_ID < 80100) { + $enumError = 'PHPDoc tag @phpstan-require-implements contains unknown class IncompatibleRequireImplements\SomeEnum.'; + $enumTip = 'Learn more at https://phpstan.org/user-guide/discovering-symbols'; + } + + $expectedErrors = [ + [ + 'PHPDoc tag @phpstan-require-implements cannot contain non-interface type IncompatibleRequireImplements\SomeTrait.', + 8, + ], + [ + $enumError, + 13, + $enumTip, + ], + [ + 'PHPDoc tag @phpstan-require-implements contains unknown class IncompatibleRequireImplements\TypeDoesNotExist.', + 18, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'PHPDoc tag @phpstan-require-implements cannot contain non-interface type IncompatibleRequireImplements\SomeClass.', + 24, + ], + [ + 'PHPDoc tag @phpstan-require-implements contains non-object type int.', + 29, + ], + [ + 'PHPDoc tag @phpstan-require-implements contains non-object type *NEVER*.', + 34, + ], + ]; + + $this->analyse([__DIR__ . '/data/incompatible-require-implements.php'], $expectedErrors); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php new file mode 100644 index 0000000000..f3ea56d12f --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php @@ -0,0 +1,73 @@ + + */ +class VarTagChangedExpressionTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new VarTagChangedExpressionTypeRule(new VarTagTypeRuleHelper(true, true)); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/var-tag-changed-expr-type.php'], [ + [ + 'PHPDoc tag @var with type string is not subtype of native type int.', + 17, + ], + [ + 'PHPDoc tag @var with type string is not subtype of type int.', + 37, + ], + [ + 'PHPDoc tag @var with type string is not subtype of native type int.', + 54, + ], + [ + 'PHPDoc tag @var with type string is not subtype of native type int.', + 73, + ], + ]); + } + + public function testAssignOfDifferentVariable(): void + { + $this->analyse([__DIR__ . '/data/wrong-var-native-type.php'], [ + [ + 'PHPDoc tag @var with type string is not subtype of type int.', + 95, + ], + ]); + } + + public function testBug10130(): void + { + $this->analyse([__DIR__ . '/data/bug-10130.php'], [ + [ + 'PHPDoc tag @var with type array is not subtype of type array.', + 14, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type list.', + 17, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type array{id: int}.', + 20, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type list.', + 23, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php index 63164b9bda..6083de943d 100644 --- a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php @@ -13,10 +13,18 @@ class WrongVariableNameInVarTagRuleTest extends RuleTestCase { + private bool $checkTypeAgainstNativeType = false; + + private bool $checkTypeAgainstPhpDocType = false; + + private bool $strictWideningCheck = false; + protected function getRule(): Rule { return new WrongVariableNameInVarTagRule( self::getContainer()->getByType(FileTypeMapper::class), + new VarTagTypeRuleHelper($this->checkTypeAgainstPhpDocType, $this->strictWideningCheck), + $this->checkTypeAgainstNativeType, ); } @@ -65,63 +73,63 @@ public function testRule(): void ], [ 'Multiple PHPDoc @var tags above single variable assignment are not supported.', - 125, + 126, ], [ 'Variable $b in PHPDoc tag @var does not exist.', - 134, + 135, ], [ 'PHPDoc tag @var does not specify variable name.', - 155, + 156, ], [ 'PHPDoc tag @var does not specify variable name.', - 176, + 177, ], [ 'Variable $foo in PHPDoc tag @var does not exist.', - 210, + 211, ], [ 'PHPDoc tag @var above foreach loop does not specify variable name.', - 234, + 235, ], [ 'Variable $foo in PHPDoc tag @var does not exist.', - 248, + 249, ], [ 'Variable $bar in PHPDoc tag @var does not exist.', - 248, + 249, ], [ 'Variable $slots in PHPDoc tag @var does not exist.', - 262, + 263, ], [ 'Variable $slots in PHPDoc tag @var does not exist.', - 268, + 269, ], [ 'PHPDoc tag @var above assignment does not specify variable name.', - 274, + 275, ], [ 'Variable $slots in PHPDoc tag @var does not match assigned variable $itemSlots.', - 280, + 281, ], [ 'PHPDoc tag @var above a class has no effect.', - 300, + 301, ], [ 'PHPDoc tag @var above a method has no effect.', - 304, + 305, ], [ 'PHPDoc tag @var above a function has no effect.', - 312, + 313, ], ]); } @@ -188,4 +196,319 @@ public function testEnums(): void ]); } + public function dataReportWrongType(): iterable + { + $nativeCheckOnly = [ + [ + 'PHPDoc tag @var with type string|null is not subtype of native type string.', + 14, + ], + [ + 'PHPDoc tag @var with type stdClass is not subtype of native type SplObjectStorage.', + 23, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type \'foo\'.', + 26, + ], + [ + 'PHPDoc tag @var with type Iterator is not subtype of native type array.', + 38, + ], + [ + 'PHPDoc tag @var with type string is not subtype of native type 1.', + 99, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type string.', + 109, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type \'foo\'.', + 148, + ], + [ + 'PHPDoc tag @var with type stdClass is not subtype of native type PHPStan\Type\Type|null.', + 186, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\Type|null is always PHPStan\Type\ObjectType|null but it\'s error-prone and dangerous.', + 189, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\Type|null is always PHPStan\Type\ObjectType but it\'s error-prone and dangerous.', + 192, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\ObjectType|null is always PHPStan\Type\ObjectType but it\'s error-prone and dangerous.', + 195, + ], + [ + 'PHPDoc tag @var with type PHPStan\Type\Type|null is not subtype of native type PHPStan\Type\ObjectType|null.', + 201, + ], + [ + 'PHPDoc tag @var with type PHPStan\Type\ObjectType|null is not subtype of type PHPStan\Type\Generic\GenericObjectType|null.', + 204, + ], + ]; + + yield [false, false, false, []]; + yield [true, false, false, $nativeCheckOnly]; + yield [true, false, true, $nativeCheckOnly]; + yield [false, true, false, []]; + yield [false, true, true, []]; + yield [true, true, false, [ + [ + 'PHPDoc tag @var with type string|null is not subtype of native type string.', + 14, + ], + [ + 'PHPDoc tag @var with type stdClass is not subtype of native type SplObjectStorage.', + 23, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type \'foo\'.', + 26, + ], + [ + 'PHPDoc tag @var with type int is not subtype of type string.', + 29, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type list.', + 35, + ], + [ + 'PHPDoc tag @var with type Iterator is not subtype of native type array.', + 38, + ], + [ + 'PHPDoc tag @var with type Iterator is not subtype of type Iterator.', + 44, + ], + /*[ + // reported by VarTagChangedExpressionTypeRule + 'PHPDoc tag @var with type string is not subtype of type int.', + 95, + ],*/ + [ + 'PHPDoc tag @var with type string is not subtype of native type 1.', + 99, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type string.', + 109, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type array.', + 137, + ], + [ + 'PHPDoc tag @var with type string is not subtype of type int.', + 137, + ], + [ + 'PHPDoc tag @var with type int is not subtype of type string.', + 137, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type \'foo\'.', + 148, + ], + [ + 'PHPDoc tag @var with type array> is not subtype of type array>.', + 160, + ], + [ + 'PHPDoc tag @var with type array> is not subtype of type array>.', + 163, + ], + [ + 'PHPDoc tag @var with type stdClass is not subtype of native type PHPStan\Type\Type|null.', + 186, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\Type|null is always PHPStan\Type\ObjectType|null but it\'s error-prone and dangerous.', + 189, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\Type|null is always PHPStan\Type\ObjectType but it\'s error-prone and dangerous.', + 192, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\ObjectType|null is always PHPStan\Type\ObjectType but it\'s error-prone and dangerous.', + 195, + ], + [ + 'PHPDoc tag @var with type PHPStan\Type\Type|null is not subtype of native type PHPStan\Type\ObjectType|null.', + 201, + ], + [ + 'PHPDoc tag @var with type PHPStan\Type\ObjectType|null is not subtype of type PHPStan\Type\Generic\GenericObjectType|null.', + 204, + ], + ]]; + yield [true, true, true, [ + [ + 'PHPDoc tag @var with type string|null is not subtype of native type string.', + 14, + ], + [ + 'PHPDoc tag @var with type stdClass is not subtype of native type SplObjectStorage.', + 23, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type \'foo\'.', + 26, + ], + [ + 'PHPDoc tag @var with type int is not subtype of type string.', + 29, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type list.', + 32, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type list.', + 35, + ], + [ + 'PHPDoc tag @var with type Iterator is not subtype of native type array.', + 38, + ], + [ + 'PHPDoc tag @var with type Iterator is not subtype of type Iterator.', + 44, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type array.', + 47, + ], + /*[ + // reported by VarTagChangedExpressionTypeRule + 'PHPDoc tag @var with type string is not subtype of type int.', + 95, + ],*/ + [ + 'PHPDoc tag @var with type string is not subtype of native type 1.', + 99, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type string.', + 109, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type array.', + 122, + ], + [ + 'PHPDoc tag @var with type array is not subtype of type array.', + 137, + ], + [ + 'PHPDoc tag @var with type string is not subtype of type int.', + 137, + ], + [ + 'PHPDoc tag @var with type int is not subtype of type string.', + 137, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type \'foo\'.', + 148, + ], + [ + 'PHPDoc tag @var with type array> is not subtype of type array>.', + 154, + ], + [ + 'PHPDoc tag @var with type array> is not subtype of type array>.', + 157, + ], + [ + 'PHPDoc tag @var with type array> is not subtype of type array>.', + 160, + ], + [ + 'PHPDoc tag @var with type array> is not subtype of type array>.', + 163, + ], + [ + 'PHPDoc tag @var with type stdClass is not subtype of native type PHPStan\Type\Type|null.', + 186, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\Type|null is always PHPStan\Type\ObjectType|null but it\'s error-prone and dangerous.', + 189, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\Type|null is always PHPStan\Type\ObjectType but it\'s error-prone and dangerous.', + 192, + ], + [ + 'PHPDoc tag @var assumes the expression with type PHPStan\Type\ObjectType|null is always PHPStan\Type\ObjectType but it\'s error-prone and dangerous.', + 195, + ], + [ + 'PHPDoc tag @var with type PHPStan\Type\Type|null is not subtype of native type PHPStan\Type\ObjectType|null.', + 201, + ], + [ + 'PHPDoc tag @var with type PHPStan\Type\ObjectType|null is not subtype of type PHPStan\Type\Generic\GenericObjectType|null.', + 204, + ], + [ + 'PHPDoc tag @var with type array|null is not subtype of type array{id: int}|null.', + 235, + ], + ]]; + } + + /** + * @dataProvider dataPermutateCheckTypeAgainst + */ + public function testEmptyArrayInitWithWiderPhpDoc(bool $checkTypeAgainstNativeType, bool $checkTypeAgainstPhpDocType): void + { + $this->checkTypeAgainstNativeType = $checkTypeAgainstNativeType; + $this->checkTypeAgainstPhpDocType = $checkTypeAgainstPhpDocType; + + $errors = !$checkTypeAgainstNativeType + ? [] + : [ + [ + 'PHPDoc tag @var with type int is not subtype of native type array{}.', + 24, + ], + ]; + + $this->analyse([__DIR__ . '/data/var-above-empty-array-widening.php'], $errors); + } + + public function dataPermutateCheckTypeAgainst(): iterable + { + yield [true, true]; + yield [false, true]; + yield [true, false]; + yield [false, false]; + } + + /** + * @dataProvider dataReportWrongType + * @param list $expectedErrors + */ + public function testReportWrongType( + bool $checkTypeAgainstNativeType, + bool $checkTypeAgainstPhpDocType, + bool $strictWideningCheck, + array $expectedErrors, + ): void + { + $this->checkTypeAgainstNativeType = $checkTypeAgainstNativeType; + $this->checkTypeAgainstPhpDocType = $checkTypeAgainstPhpDocType; + $this->strictWideningCheck = $strictWideningCheck; + $this->analyse([__DIR__ . '/data/wrong-var-native-type.php'], $expectedErrors); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-10097.php b/tests/PHPStan/Rules/PhpDoc/data/bug-10097.php new file mode 100644 index 0000000000..db239d2cce --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-10097.php @@ -0,0 +1,37 @@ + + */ + public function getMessageType(): string; +} + + +/** + * @extends Consumer + */ +interface SomeMessageConsumer extends Consumer +{ +} + +/** + * @return list> + */ +function getConsumers(SomeMessageConsumer $consumerA): array +{ + return [$consumerA]; +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-10130.php b/tests/PHPStan/Rules/PhpDoc/data/bug-10130.php new file mode 100644 index 0000000000..7fbc69f30f --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-10130.php @@ -0,0 +1,26 @@ + $map + * @param list $list + * @param array{id: int} $shape + * @param list $listOfShapes + */ + public function doFoo($map, $list, $shape, $listOfShapes): void + { + /** @var mixed[] $map */ + if ($map) {} + + /** @var mixed[] $list */ + if ($list) {} + + /** @var mixed[] $shape */ + if ($shape) {} + + /** @var mixed[] $listOfShapes */ + if ($listOfShapes) {} + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-10214.php b/tests/PHPStan/Rules/PhpDoc/data/bug-10214.php new file mode 100644 index 0000000000..b1292d2ba5 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-10214.php @@ -0,0 +1,42 @@ + + */ +class PostVoter extends Voter { + function supports($attribute, $subject): bool + { + return $attribute === 'POST_READ' && $subject instanceof Post; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-10594.php b/tests/PHPStan/Rules/PhpDoc/data/bug-10594.php new file mode 100644 index 0000000000..dfc27264bb --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-10594.php @@ -0,0 +1,34 @@ +getParameters(); + if (count($parameters) >= 2) { + return $parameters[1]->getType() !== null && ($parameters[1]->getType() instanceof ReflectionNamedType && $parameters[1]->getType()->getName() !== 'string'); + } + return true; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-10622.php b/tests/PHPStan/Rules/PhpDoc/data/bug-10622.php new file mode 100644 index 0000000000..22caf7f0a2 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-10622.php @@ -0,0 +1,30 @@ + : SupportCollection) + */ + public function map(callable $callback) {} +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-10622b.php b/tests/PHPStan/Rules/PhpDoc/data/bug-10622b.php new file mode 100644 index 0000000000..23110ad14c --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-10622b.php @@ -0,0 +1,71 @@ + + */ +class FooBoxedArray +{ + /** @var T */ + private $value; + + /** + * @param T $value + */ + public function __construct(array $value) + { + $this->value = $value; + } + + /** + * @return T + */ + public function get(): array + { + return $this->value; + } +} + +/** + * @template TKey of object|array + * @template TValue of object|array + */ +class FooMap +{ + /** + * @var array)>, + * \WeakReference<(TValue is object ? TValue : FooBoxedArray)> + * }> + */ + protected $weakKvByIndex = []; + + /** + * @template T of TKey|TValue + * + * @param T $value + * + * @return (T is object ? T : FooBoxedArray) + */ + protected function boxValue($value): object + { + return is_array($value) + ? new FooBoxedArray($value) + : $value; + } + + /** + * @template T of TKey|TValue + * + * @param (T is object ? T : FooBoxedArray) $value + * + * @return T + */ + protected function unboxValue(object $value) + { + return $value instanceof FooBoxedArray + ? $value->get() + : $value; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-7240.php b/tests/PHPStan/Rules/PhpDoc/data/bug-7240.php new file mode 100644 index 0000000000..da13e6f11b --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-7240.php @@ -0,0 +1,36 @@ += 7.4 + +declare(strict_types=1); + +namespace Bug7240; + +/** + * @phpstan-type TypeArrayMinMax array{min:int,max:int} + * @phpstan-type TypeArrayMinMaxSet array + */ +class A +{ + /** @var TypeArrayMinMaxSet */ + protected array $var; +} + +class B extends A +{ + protected array $var = ["year" => ["min" => 1990, "max" => 2200]]; +} + +class AbstractC extends A +{ +} + +class CBroken extends AbstractC +{ + protected array $var = ["year" => ["min" => 1990, "max" => 2200]]; +} + +/** @phpstan-import-type TypeArrayMinMaxSet from A */ +class CWorks extends AbstractC +{ + /** @var TypeArrayMinMaxSet */ + protected array $var = ["year" => ["min" => 1990, "max" => 2200]]; +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-7310.php b/tests/PHPStan/Rules/PhpDoc/data/bug-7310.php new file mode 100644 index 0000000000..cab561b15c --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-7310.php @@ -0,0 +1,33 @@ + + */ +class ObjectWithMetadata { + + /** @var M */ + private $metadata; + + /** + * @param M $metadata + */ + public function __construct( + array $metadata + ) { + $this->metadata = $metadata; + } + + /** + * @template K of string + * @template D + * @param K $key + * @param D $default + * @return (M[K] is not null ? M[K] : D) + */ + public function getMeta(string $key, mixed $default = null): mixed + { + return $this->metadata[ $key ] ?? $default; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-8284.php b/tests/PHPStan/Rules/PhpDoc/data/bug-8284.php new file mode 100644 index 0000000000..b372aa1f82 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-8284.php @@ -0,0 +1,39 @@ +}>):string) : (callable(array):string)) $replacement + * @param string $subject + * @param int $count Set by method + * @param int-mask $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set + */ + public static function replaceCallback($pattern, callable $replacement, $subject, int $limit = -1, int &$count = null, int $flags = 0): string + { + if (!is_scalar($subject)) { + throw new \TypeError(''); + } + + $result = preg_replace_callback($pattern, $replacement, $subject, $limit, $count, $flags | PREG_UNMATCHED_AS_NULL); + if ($result === null) { + throw new \RuntimeException; + } + + return $result; + } +} + +function () { + HelloWorld::replaceCallback('{a+}', function ($match): string { + \PHPStan\dumpType($match); + return (string)$match[0][0]; + }, 'abcaddsa', -1, $count, PREG_OFFSET_CAPTURE); + + HelloWorld::replaceCallback('{a+}', function ($match): string { + \PHPStan\dumpType($match); + return (string)$match[0]; + }, 'abcaddsa', -1, $count); +}; diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-8408.php b/tests/PHPStan/Rules/PhpDoc/data/bug-8408.php new file mode 100644 index 0000000000..c70a3cd146 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-8408.php @@ -0,0 +1,17 @@ +|list> + * @param T $bar + * + * @return (T[0] is string ? array{T} : T) + */ +function foo(array $bar) : array{ return is_string($bar[0]) ? [$bar] : $bar; } + +function(): void { + assertType("array{array{'foo', 'bar'}}", foo(['foo', 'bar'])); + assertType("array{array{'foo', 'bar'}, array{'xyz', 'asd'}}", foo([['foo','bar'],['xyz','asd']])); +}; diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-8609.php b/tests/PHPStan/Rules/PhpDoc/data/bug-8609.php new file mode 100644 index 0000000000..ac6e4eb891 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-8609.php @@ -0,0 +1,34 @@ +value = $value; + } + + /** + * @template C of int + * + * @param C $coefficient + * + * @return ( + * T is positive-int + * ? (C is positive-int ? positive-int : negative-int) + * : T is negative-int + * ? (C is positive-int ? negative-int : positive-int) + * : (T is 0 ? 0 : int) + * ) + * ) + */ + public function multiply(int $coefficient): int { + return $this->value * $coefficient; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-8697.php b/tests/PHPStan/Rules/PhpDoc/data/bug-8697.php new file mode 100644 index 0000000000..3478c66524 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-8697.php @@ -0,0 +1,34 @@ += 8.0 + +namespace GenericCallableProperties; + +use Closure; +use stdClass; + +/** + * @template T + */ +class Test +{ + /** + * @var Closure(T): T + */ + private Closure $shadows; + + /** + * @var Closure(stdClass): stdClass + */ + private Closure $existingClass; + + /** + * @var callable(TypeAlias): TypeAlias + */ + private $typeAlias; + + /** + * @var callable(TNull): TNull + */ + private $unsupported; + + /** + * @var callable(TInvalid): TInvalid + */ + private $invalid; + + /** + * @param Closure(T): T $notReported + */ + public function __construct(private Closure $notReported) {} +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/generic-callables-incompatible.php b/tests/PHPStan/Rules/PhpDoc/data/generic-callables-incompatible.php new file mode 100644 index 0000000000..238d822894 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/generic-callables-incompatible.php @@ -0,0 +1,204 @@ + 8.0 + +namespace GenericCallablesIncompatible; + +use Closure; +use stdClass; + +/** + * @param Closure(stdClass $val): stdClass $existingClass + */ +function existingClass(Closure $existingClass): void +{ +} + +/** + * @param Closure(TypeAlias $val): TypeAlias $existingTypeAlias + */ +function existingTypeAlias(Closure $existingTypeAlias): void +{ +} + +/** + * @param Closure(T $val): T $invalidBoundType + */ +function invalidBoundType(Closure $invalidBoundType): void +{ +} + +/** + * @param Closure(T $val): T $notSupported + */ +function notSupported(Closure $notSupported): void +{ +} + +/** + * @template T + * @param Closure(T $val): T $shadows + */ +function testShadowFunction(Closure $shadows): void +{ +} + +/** + * @param-out Closure(stdClass $val): stdClass $existingClass + */ +function existingClassParamOut(Closure &$existingClass): void +{ +} + +/** + * @template U + */ +class Test +{ + /** + * @template T + * @param Closure(T $val): T $shadows + */ + function testShadowMethod(Closure $shadows): void + { + } + + /** + * @template T + * @return Closure(T $val): T + */ + function testShadowMethodReturn(): Closure + { + } +} + +/** + * @return Closure(stdClass $val): stdClass + */ +function existingClassReturn(): Closure +{ +} + +/** + * @return Closure(TypeAlias $val): TypeAlias + */ +function existingTypeAliasReturn(): Closure +{ +} + +/** + * @return Closure(T $val): T + */ +function invalidBoundTypeReturn(): Closure +{ +} + +/** + * @return Closure(T $val): T + */ +function notSupportedReturn(): Closure +{ +} + +/** + * @template T + * @return Closure(T $val): T + */ +function testShadowFunctionReturn(): Closure +{ +} + +/** + * @template U + */ +class Test2 +{ + /** + * @param Closure(stdClass $val): stdClass $existingClass + */ + public function existingClass(Closure $existingClass): void + { + } + + /** + * @param Closure(TypeAlias $val): TypeAlias $existingTypeAlias + */ + public function existingTypeAlias(Closure $existingTypeAlias): void + { + } + + /** + * @param Closure(T $val): T $invalidBoundType + */ + public function invalidBoundType(Closure $invalidBoundType): void + { + } + + /** + * @param Closure(T $val): T $notSupported + */ + public function notSupported(Closure $notSupported): void + { + } + + /** + * @return Closure(stdClass $val): stdClass + */ + public function existingClassReturn(): Closure + { + } + + /** + * @return Closure(TypeAlias $val): TypeAlias + */ + public function existingTypeAliasReturn(): Closure + { + } + + /** + * @return Closure(T $val): T + */ + public function invalidBoundTypeReturn(): Closure + { + } + + /** + * @return Closure(T $val): T + */ + public function notSupportedReturn(): Closure + { + } +} + +/** + * @template T + * @param-out Closure(T $val): T $existingClass + */ +function shadowsParamOut(Closure &$existingClass): void +{ +} + +/** + * @template T + * @param-out list(T $val): T> $existingClasses + */ +function shadowsParamOutArray(array &$existingClasses): void +{ +} + +/** + * @template T + * @return list(T $val): T> + */ +function shadowsReturnArray(): array +{ +} + +/** + * @template T + */ +class Test3 +{ + /** + * @param Closure(T): T $shadows + */ + public function __construct(private Closure $shadows) {} +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/ignore-line-within-phpdoc.php b/tests/PHPStan/Rules/PhpDoc/data/ignore-line-within-phpdoc.php new file mode 100644 index 0000000000..13b81b9f00 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/ignore-line-within-phpdoc.php @@ -0,0 +1,28 @@ += 8.3 + +namespace IncompatibleClassConstantPhpDocNativeType; + +class Foo +{ + + public const int FOO = 1; + + /** @var positive-int */ + public const int BAR = 1; + + /** @var non-empty-string */ + public const int BAZ = 1; + + /** @var int|string */ + public const int LOREM = 1; + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc.php index c4f163e773..d2b9714425 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc.php @@ -8,45 +8,7 @@ class Foo /** @var self&\stdClass */ const FOO = 1; - /** @var int */ - const BAR = 1; - - const NO_TYPE = 'string'; - - /** @var string */ - const BAZ = 1; - - /** @var string|int */ - const LOREM = 1; - - /** @var int */ - const IPSUM = self::LOREM; // resolved to 1, I'd prefer string|int - /** @var self */ const DOLOR = 1; } - -class Bar extends Foo -{ - - const BAR = 2; - - const BAZ = 2; - -} - -class Baz -{ - - /** @var string */ - private const BAZ = 'foo'; - -} - -class Lorem extends Baz -{ - - private const BAZ = 1; - -} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-phpdoc.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-phpdoc.php index 2aa3ffc894..93f04e4a13 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-phpdoc.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-phpdoc.php @@ -44,4 +44,16 @@ class FooWithProperty /** @var self::BLABLA */ private $unknownClassConstant2; + /** @var \InvalidPhpDocDefinitions\FooCovariantGeneric */ + private $genericCompatibleInvariantType; + + /** @var \InvalidPhpDocDefinitions\FooCovariantGeneric */ + private $genericRedundantTypeProjection; + + /** @var \InvalidPhpDocDefinitions\FooCovariantGeneric<*> */ + private $genericCompatibleStarProjection; + + /** @var \InvalidPhpDocDefinitions\FooCovariantGeneric */ + private $genericIncompatibleTypeProjection; + } diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-extends.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-extends.php new file mode 100644 index 0000000000..eb362e5b61 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-extends.php @@ -0,0 +1,178 @@ += 8.1 + +namespace IncompatibleRequireExtends; + +/** + * @phpstan-require-extends SomeTrait + */ +interface InvalidInterface1 {} + +/** + * @phpstan-require-extends SomeInterface + */ +interface InvalidInterface2 {} + +/** + * @phpstan-require-extends SomeEnum + */ +interface InvalidInterface3 {} + +/** + * @phpstan-require-extends TypeDoesNotExist + */ +interface InvalidInterface4 {} + +/** + * @template T + * @phpstan-require-extends SomeClass + */ +interface InvalidInterface5 {} + +/** + * @phpstan-require-extends int + */ +interface InvalidInterface6 {} + +/** + * @phpstan-require-extends SomeClass + */ +class InvalidClass {} + +/** + * @phpstan-require-extends SomeClass + */ +enum InvalidEnum {} + +class InValidTraitUse2 +{ + use ValidTrait; +} + +class InValidTraitUse extends SomeOtherClass +{ + use ValidTrait; +} + +class InvalidInterfaceUse2 implements ValidInterface {} + +class InvalidInterfaceUse extends SomeOtherClass implements ValidInterface {} + +class ValidInterfaceUse extends SomeClass implements ValidInterface {} + +class ValidTraitUse extends SomeClass +{ + use ValidTrait; +} + +class ValidTraitUse2 extends SomeSubClass +{ + use ValidTrait; +} +/** + * @phpstan-require-extends SomeClass + */ +interface ValidInterface {} + +/** + * @phpstan-require-extends SomeClass + */ +trait ValidTrait {} + + + +interface SomeInterface +{ + +} + +trait SomeTrait +{ + +} + +class SomeClass +{ + +} + +final class SomeFinalClass +{ + +} + +class SomeSubClass extends SomeClass +{ + +} + +class SomeOtherClass +{ + +} + +enum SomeEnum +{ + +} + +/** + * @phpstan-require-extends SomeFinalClass + */ +interface InvalidInterface7 {} + +/** + * @phpstan-require-extends SomeFinalClass + */ +trait InvalidTrait {} + +class InvalidClass2 { + use InvalidTrait; +} + +/** + * @phpstan-require-extends self&\stdClass + */ +interface UnresolvableExtendsInterface {} + +/** + * @phpstan-require-extends self&\stdClass + */ +trait UnresolvableExtendsTrait {} + +class InvalidClass3 { + use UnresolvableExtendsTrait; +} + +new class { + use ValidTrait; +}; + +new class extends SomeClass { + use ValidTrait; +}; + +/** + * @psalm-require-extends SomeClass + */ +trait ValidPsalmTrait {} + +new class extends SomeClass { + use ValidPsalmTrait; +}; + +new class { + use ValidPsalmTrait; +}; + +/** + * @phpstan-require-extends SomeClass + * @phpstan-require-extends SomeOtherClass + */ +trait TooMuchExtends {} + +/** + * @phpstan-require-extends SomeClass + * @phpstan-require-extends SomeOtherClass + * @phpstan-require-extends SomeOtherClass + */ +interface TooMuchExtendsIface {} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-implements.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-implements.php new file mode 100644 index 0000000000..246d01a6e8 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-require-implements.php @@ -0,0 +1,170 @@ += 8.1 + +namespace IncompatibleRequireImplements; + +/** + * @phpstan-require-implements SomeTrait + */ +trait InvalidTrait1 {} + +/** + * @phpstan-require-implements SomeEnum + */ +trait InvalidTrait2 {} + +/** + * @phpstan-require-implements TypeDoesNotExist + */ +trait InvalidTrait3 {} + +/** + * @template T + * @phpstan-require-implements SomeClass + */ +trait InvalidTrait4 {} + +/** + * @phpstan-require-implements int + */ +trait InvalidTrait5 {} + +/** + * @phpstan-require-implements self&\stdClass + */ +trait InvalidTrait6 {} + + +/** + * @phpstan-require-implements SomeClass + */ +class InvalidClass {} + +/** + * @phpstan-require-implements SomeClass + */ +enum InvalidEnum {} + +class InValidTraitUse2 +{ + use ValidTrait; +} + +enum InvalidEnumTraitUse { + use ValidTrait; +} + +class InValidTraitUse extends SomeOtherClass implements WrongInterface +{ + use ValidTrait; +} + +class ValidTraitUse extends SomeClass implements RequiredInterface +{ + use ValidTrait; +} + +class ValidTraitUse2 extends ValidTraitUse +{ +} + +class ValidTraitUse3 extends ValidTraitUse +{ + use ValidTrait; +} + +/** + * @phpstan-require-implements RequiredInterface + */ +trait ValidTrait {} + +interface WrongInterface +{ + +} + +interface RequiredInterface +{ + +} + +interface SomeInterface +{ + +} + +trait SomeTrait +{ + +} + +class SomeClass {} + +class SomeSubClass extends SomeClass +{ + +} + +class SomeOtherClass +{ + +} + +enum SomeEnum +{ + +} + +new class { + use ValidTrait; +}; + +new class implements RequiredInterface { + use ValidTrait; +}; + +class InvalidTraitUse1 { + use InvalidTrait1; +} + +class InvalidTraitUse2 { + use InvalidTrait2; +} + +class InvalidTraitUse3 { + use InvalidTrait3; +} + +class InvalidTraitUse4 { + use InvalidTrait4; +} + +class InvalidTraitUse5 { + use InvalidTrait5; +} + +class InvalidTraitUse6 { + use InvalidTrait6; +} + +interface RequiredInterface2 +{ + +} + +/** + * @psalm-require-implements RequiredInterface + * @psalm-require-implements RequiredInterface2 + */ +trait ValidPsalmTrait {} + +new class implements RequiredInterface, RequiredInterface2 { + use ValidPsalmTrait; +}; + +new class implements RequiredInterface { + use ValidPsalmTrait; +}; + +new class { + use ValidPsalmTrait; +}; diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php new file mode 100644 index 0000000000..a0c4e977a0 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-self-out-type.php @@ -0,0 +1,29 @@ + + */ + public function two($param); + + /** + * @phpstan-self-out int + */ + public function three(); + + /** + * @phpstan-self-out self|null + */ + public function four(); +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php index 5f7fd45b73..078e004d26 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php @@ -284,3 +284,48 @@ function genericWrongBound(int $i) { } + +/** + * @param \InvalidPhpDocDefinitions\FooCovariantGeneric $foo + * @return \InvalidPhpDocDefinitions\FooCovariantGeneric + */ +function genericCompatibleInvariantType($foo) +{ + +} + +/** + * @param \InvalidPhpDocDefinitions\FooCovariantGeneric $foo + * @return \InvalidPhpDocDefinitions\FooCovariantGeneric + */ +function genericRedundantTypeProjection($foo) +{ + +} + +/** + * @param \InvalidPhpDocDefinitions\FooCovariantGeneric<*> $foo + * @return \InvalidPhpDocDefinitions\FooCovariantGeneric<*> + */ +function genericCompatibleStarProjection($foo) +{ + +} + +/** + * @param \InvalidPhpDocDefinitions\FooCovariantGeneric $foo + * @return \InvalidPhpDocDefinitions\FooCovariantGeneric + */ +function genericIncompatibleTypeProjection($foo) +{ + +} + +/** + * @param pure-callable(): void $cb + * @param pure-Closure(): void $cl + */ +function pureCallableCannotReturnVoid(callable $cb, \Closure $cl): void +{ + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-definitions.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-definitions.php index 7eff223ae0..f52cae0d04 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-definitions.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-definitions.php @@ -15,3 +15,11 @@ class FooGeneric { } + +/** + * @template-covariant T + */ +class FooCovariantGeneric +{ + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc.php index 15a61e603e..8b0cf8bd81 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc.php @@ -81,3 +81,25 @@ class ClassConstant const FOO = 1; } + +class AboveProperty +{ + + /** @var (Foo& */ + private $foo; + + /** @var (Foo& */ + private const TEST = 1; + +} + +class AboveReturn +{ + + public function doFoo(): string + { + /** @var (Foo& */ + return doFoo(); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php index fa2e510986..60a35c8178 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php @@ -36,3 +36,25 @@ function any() } } + +class AboveProperty +{ + + /** @phpstan-varr 1 */ + private $foo; + + /** @phpstan-varr 1 */ + private const TEST = 1; + +} + +class AboveReturn +{ + + public function doFoo(): string + { + /** @phpstan-varr string */ + return doFoo(); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-type-type-alias.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-type-type-alias.php new file mode 100644 index 0000000000..b99b3e5577 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-type-type-alias.php @@ -0,0 +1,20 @@ + $test */ + $test = doFoo(); + + /** @var \InvalidPhpDocDefinitions\FooCovariantGeneric $test */ + $test = doFoo(); + + /** @var \InvalidPhpDocDefinitions\FooCovariantGeneric<*> $test */ + $test = doFoo(); + + /** @var \InvalidPhpDocDefinitions\FooCovariantGeneric $test */ + $test = doFoo(); } public function doBar($foo) diff --git a/tests/PHPStan/Rules/PhpDoc/data/method-assert.php b/tests/PHPStan/Rules/PhpDoc/data/method-assert.php new file mode 100644 index 0000000000..fa603eb85f --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/method-assert.php @@ -0,0 +1,82 @@ + ? true : false) + */ + public function foo(): bool + { + + } + +} + +class ParamOut +{ + + /** + * @param-out ($i is int ? 1 : 2) $out + */ + public function doFoo(int $i, &$out) { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/param-out.php b/tests/PHPStan/Rules/PhpDoc/data/param-out.php new file mode 100644 index 0000000000..5c4da69694 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/param-out.php @@ -0,0 +1,115 @@ + $i + */ +function unresolvableParamOutType(int &$i) { + +} + +/** + * @param-out \Exception $i + */ +function invalidParamOutGeneric(int &$i) { + +} + +/** + * @param-out FooBar $i + */ +function invalidParamOutWrongGenericParams(int &$i) { + +} + +/** + * @param-out FooBar $i + */ +function invalidParamOutNotAllGenericParams(int &$i) { + +} + +/** + * @template T of int + * @template TT of string + */ +class FooBar { + /** + * @param-out T $s + */ + function genericClassFoo(mixed &$s): void + { + } + + /** + * @template S of self + * @param-out S $s + */ + function genericSelf(mixed &$s): void + { + } + + /** + * @template S of static + * @param-out S $s + */ + function genericStatic(mixed &$s): void + { + } +} + +class C { + /** + * @var \Closure|null + */ + private $onCancel; + + public function __construct() { + $this->foo($this->onCancel); + } + + /** + * @param mixed $onCancel + * @param-out \Closure $onCancel + */ + public function foo(&$onCancel) : void { + $onCancel = function (): void {}; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/throws-with-require.php b/tests/PHPStan/Rules/PhpDoc/data/throws-with-require.php new file mode 100644 index 0000000000..9e816d8a66 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/throws-with-require.php @@ -0,0 +1,76 @@ + $a */ +$a = []; + +/** @var array{string, int} $a */ +$a = []; + +/** @var int $a */ +$a = []; + +$translationsTree = []; + +/** @var array $byRef */ +$byRef = &$translationsTree; diff --git a/tests/PHPStan/Rules/PhpDoc/data/var-tag-changed-expr-type.php b/tests/PHPStan/Rules/PhpDoc/data/var-tag-changed-expr-type.php new file mode 100644 index 0000000000..d8d72956fc --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/var-tag-changed-expr-type.php @@ -0,0 +1,78 @@ +foo; + } + + public function doBar() + { + /** @var string */ + return $this->foo; + } + +} + +class Baz +{ + + public function doFoo(int $foo) + { + /** @var int $foo */ + return $this->doBar($foo); + } + + public function doBar(int $foo) + { + /** @var string $foo */ + return $this->doFoo($foo); + } + +} + +class Lorem +{ + + public function doFoo(int $foo) + { + /** @var int $foo */ + if ($foo) { + + } + } + + public function doBar(int $foo) + { + /** @var string $foo */ + if ($foo) { + + } + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/wrong-var-native-type.php b/tests/PHPStan/Rules/PhpDoc/data/wrong-var-native-type.php new file mode 100644 index 0000000000..2edb6ba496 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/wrong-var-native-type.php @@ -0,0 +1,238 @@ +doBar(); + + /** @var string|null $stringOrNull */ + $stringOrNull = $this->doBar(); + + /** @var string|null $null */ + $null = null; + + /** @var \SplObjectStorage<\stdClass, array{int, string}> $running */ + $running = new \SplObjectStorage(); + + /** @var \stdClass $running2 */ + $running2 = new \SplObjectStorage(); + + /** @var int $int */ + $int = 'foo'; + + /** @var int $test */ + $test = $this->doBaz(); + + /** @var array $ints */ + $ints = $this->returnsListOfIntegers(); + + /** @var array $strings */ + $strings = $this->returnsListOfIntegers(); + + /** @var \Iterator $intIterator */ + $intIterator = $this->returnsListOfIntegers(); + + /** @var \Iterator $intIterator */ + $intIterator2 = $this->returnsIteratorOfIntegers(); + + /** @var \Iterator $stringIterator */ + $stringIterator = $this->returnsIteratorOfIntegers(); + + /** @var int[] $ints2 */ + $ints2 = $this->returnsArrayOfIntegers(); + } + + public function doBar(): string + { + + } + + /** + * @return string + */ + public function doBaz() + { + + } + + /** + * @return list + */ + public function returnsListOfIntegers(): array + { + + } + + /** + * @return \Iterator + */ + public function returnsIteratorOfIntegers(): \Iterator + { + + } + + /** @return array */ + public function returnsArrayOfIntegers(): array + { + + } + + /** @param int[] $integers */ + public function trickyForeachCase(array $integers): void + { + foreach ($integers as $int) { + /** @var int $int */ + $a = new \stdClass(); + } + + foreach ($integers as $int) { + /** @var string $int */ + $a = new \stdClass(); + } + + /** @var string */ + $nameless = 1; + } + + public function testArrayDestructuring(int $i, string $s): void + { + /** + * @var int $a + * @var string $b + * @var int $c + */ + [$a, $b, $c] = [$i, $s, $s]; + } + + /** + * @param array $a + */ + public function testForeach(array $a): void + { + /** + * @var string[] $a + * @var int $k + * @var string $v + */ + foreach ($a as $k => $v) { + + } + } + + /** + * @param array $a + */ + public function testForeach2(array $a): void + { + /** + * @var int[] $a + * @var string $k + * @var int $v + */ + foreach ($a as $k => $v) { + + } + } + + public function testStatic(): void + { + /** @var int $a */ + static $a = 1; + + /** @var int $b */ + static $b = 'foo'; + } + + public function iterablesRecursively(): void + { + /** @var array> $a */ + $a = $this->arrayOfLists(); + + /** @var array> $b */ + $b = $this->arrayOfLists(); + + /** @var array> $c */ + $c = $this->arrayOfLists(); + + /** @var array<\Traversable> $d */ + $d = $this->arrayOfLists(); + } + + /** @return array> */ + private function arrayOfLists(): array + { + + } + +} + +class PHPStanType +{ + + public function doFoo(): void + { + /** @var \PHPStan\Type\Type $a */ + $a = $this->doBar(); // not narrowing - ok + + /** @var \PHPStan\Type\Type|null $b */ + $b = $this->doBar(); // not narrowing - ok + + /** @var \stdClass $c */ + $c = $this->doBar(); // not subtype - error + + /** @var \PHPStan\Type\ObjectType|null $d */ + $d = $this->doBar(); // narrowing Type - error + + /** @var \PHPStan\Type\ObjectType $e */ + $e = $this->doBar(); // narrowing Type - error + + /** @var \PHPStan\Type\ObjectType $f */ + $f = $this->doBaz(); // not narrowing - does not have to error but currently does + + /** @var \PHPStan\Type\ObjectType|null $g */ + $g = $this->doBaz(); // not narrowing - ok + + /** @var \PHPStan\Type\Type|null $g */ + $g = $this->doBaz(); // generalizing - not ok + + /** @var \PHPStan\Type\ObjectType|null $h */ + $h = $this->doBazPhpDoc(); // generalizing - not ok + } + + public function doBar(): ?\PHPStan\Type\Type + { + + } + + public function doBaz(): ?\PHPStan\Type\ObjectType + { + + } + + /** + * @return \PHPStan\Type\Generic\GenericObjectType|null + */ + public function doBazPhpDoc() + { + + } + +} + +class Ipsum +{ + /** + * @param array{id: int}|null $b + */ + public function doFoo($b): void + { + /** @var mixed[]|null $a */ + $a = $b; + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/wrong-variable-name-var.php b/tests/PHPStan/Rules/PhpDoc/data/wrong-variable-name-var.php index ceffc3b89c..a769c5f2dd 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/wrong-variable-name-var.php +++ b/tests/PHPStan/Rules/PhpDoc/data/wrong-variable-name-var.php @@ -79,16 +79,16 @@ public function doBaz() static $var; /** @var int */ - static $var; + static $var2; /** @var int */ - static $var, $bar; + static $var3, $bar; /** * @var int * @var string */ - static $var, $bar; + static $var4, $bar2; /** @var int $foo */ static $test; @@ -115,6 +115,7 @@ public function multiplePrefixedTagsAreFine() * @var int * @phpstan-var int * @psalm-var int + * @phan-var int */ $test = doFoo(); // OK diff --git a/tests/PHPStan/Rules/Playground/FunctionNeverRuleTest.php b/tests/PHPStan/Rules/Playground/FunctionNeverRuleTest.php new file mode 100644 index 0000000000..a75b82f714 --- /dev/null +++ b/tests/PHPStan/Rules/Playground/FunctionNeverRuleTest.php @@ -0,0 +1,37 @@ + + */ +class FunctionNeverRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new FunctionNeverRule(new NeverRuleHelper()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/function-never.php'], [ + [ + 'Function FunctionNever\doBar() always throws an exception, it should have return type "never".', + 18, + ], + [ + 'Function FunctionNever\callsNever() always terminates script execution, it should have return type "never".', + 23, + ], + [ + 'Function FunctionNever\doBaz() always terminates script execution, it should have return type "never".', + 28, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Playground/MethodNeverRuleTest.php b/tests/PHPStan/Rules/Playground/MethodNeverRuleTest.php new file mode 100644 index 0000000000..583c6a5a5f --- /dev/null +++ b/tests/PHPStan/Rules/Playground/MethodNeverRuleTest.php @@ -0,0 +1,37 @@ + + */ +class MethodNeverRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MethodNeverRule(new NeverRuleHelper()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/method-never.php'], [ + [ + 'Method MethodNever\Foo::doBar() always throws an exception, it should have return type "never".', + 21, + ], + [ + 'Method MethodNever\Foo::callsNever() always terminates script execution, it should have return type "never".', + 26, + ], + [ + 'Method MethodNever\Foo::doBaz() always terminates script execution, it should have return type "never".', + 31, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Playground/NoPhpCodeRuleTest.php b/tests/PHPStan/Rules/Playground/NoPhpCodeRuleTest.php new file mode 100644 index 0000000000..146c26325e --- /dev/null +++ b/tests/PHPStan/Rules/Playground/NoPhpCodeRuleTest.php @@ -0,0 +1,34 @@ + + */ +class NoPhpCodeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new NoPhpCodeRule(); + } + + public function testEmptyFile(): void + { + $this->analyse([__DIR__ . '/data/empty.php'], []); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/no-php-code.php'], [ + [ + 'The example does not contain any PHP code. Did you forget the opening */ +function yields(): \Generator +{ + while (true) { + yield rand(); + } +} diff --git a/tests/PHPStan/Rules/Playground/data/method-never.php b/tests/PHPStan/Rules/Playground/data/method-never.php new file mode 100644 index 0000000000..8353da4d4a --- /dev/null +++ b/tests/PHPStan/Rules/Playground/data/method-never.php @@ -0,0 +1,57 @@ +doFoo(); + } + + public function doBaz() + { + while (true) { + + } + } + + public function onlySometimes() + { + if (rand(0, 1)) { + return; + } + + throw new \Exception(); + } + + /** + * @return \Generator + */ + public function yields(): \Generator + { + while(true) { + yield 1; + } + } + +} diff --git a/tests/PHPStan/Rules/Playground/data/no-php-code.php b/tests/PHPStan/Rules/Playground/data/no-php-code.php new file mode 100644 index 0000000000..1211cfbd4b --- /dev/null +++ b/tests/PHPStan/Rules/Playground/data/no-php-code.php @@ -0,0 +1,4 @@ +class Foo +{ + private int $foo; +} diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php index 69c2b3f7a5..9b70004995 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php @@ -17,20 +17,23 @@ protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); return new AccessPropertiesInAssignRule( - new AccessPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false, false), true, true), + new AccessPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), true, true), ); } public function testRule(): void { + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/access-properties-assign.php'], [ [ 'Access to an undefined property TestAccessPropertiesAssign\AccessPropertyWithDimFetch::$foo.', 10, + $tipText, ], [ 'Access to an undefined property TestAccessPropertiesAssign\AccessPropertyWithDimFetch::$foo.', 15, + $tipText, ], ]); } @@ -40,42 +43,52 @@ public function testRuleAssignOp(): void if (PHP_VERSION_ID < 70400) { self::markTestSkipped('Test requires PHP 7.4.'); } + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/access-properties-assign-op.php'], [ [ 'Access to an undefined property TestAccessProperties\AssignOpNonexistentProperty::$flags.', 15, + $tipText, ], ]); } public function testRuleExpressionNames(): void { + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/properties-from-variable-into-object.php'], [ [ 'Access to an undefined property PropertiesFromVariableIntoObject\Foo::$noop.', 26, + $tipText, ], ]); } public function testRuleExpressionNames2(): void { + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/properties-from-array-into-object.php'], [ [ 'Access to an undefined property PropertiesFromArrayIntoObject\Foo::$noop.', 42, + $tipText, ], [ 'Access to an undefined property PropertiesFromArrayIntoObject\Foo::$noop.', 54, + $tipText, ], [ 'Access to an undefined property PropertiesFromArrayIntoObject\Foo::$noop.', 69, + $tipText, ], [ 'Access to an undefined property PropertiesFromArrayIntoObject\Foo::$noop.', 110, + $tipText, ], ]); } @@ -85,4 +98,26 @@ public function testBug4492(): void $this->analyse([__DIR__ . '/data/bug-4492.php'], []); } + public function testObjectShapes(): void + { + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; + $this->analyse([__DIR__ . '/data/properties-object-shapes.php'], [ + [ + 'Access to an undefined property object{foo: int, bar?: string}::$bar.', + 19, + $tipText, + ], + [ + 'Access to an undefined property object{foo: int, bar?: string}::$baz.', + 20, + $tipText, + ], + ]); + } + + public function testConflictingAnnotationProperty(): void + { + $this->analyse([__DIR__ . '/data/conflicting-annotation-property.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index 78b6b79cb3..4673409880 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use function array_merge; use const PHP_VERSION_ID; /** @@ -22,7 +23,7 @@ class AccessPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); - return new AccessPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, $this->checkUnionTypes, false, false), true, $this->checkDynamicProperties); + return new AccessPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, $this->checkUnionTypes, false, false, true, false), true, $this->checkDynamicProperties); } public function testAccessProperties(): void @@ -30,12 +31,15 @@ public function testAccessProperties(): void $this->checkThisOnly = false; $this->checkUnionTypes = true; $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse( [__DIR__ . '/data/access-properties.php'], [ [ 'Access to an undefined property TestAccessProperties\BarAccessProperties::$loremipsum.', 24, + $tipText, ], [ 'Access to private property $foo of parent class TestAccessProperties\FooAccessProperties.', @@ -56,10 +60,12 @@ public function testAccessProperties(): void [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$baz.', 50, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$nonexistent.', 53, + $tipText, ], [ 'Access to private property TestAccessProperties\FooAccessProperties::$foo.', @@ -77,26 +83,32 @@ public function testAccessProperties(): void [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$emptyBaz.', 69, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$emptyNonexistent.', 71, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherNonexistent.', 77, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherNonexistent.', 78, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherEmptyNonexistent.', 81, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherEmptyNonexistent.', 84, + $tipText, ], [ 'Access to property $test on an unknown class TestAccessProperties\FirstUnknownClass.', @@ -111,10 +123,12 @@ public function testAccessProperties(): void [ 'Access to an undefined property TestAccessProperties\WithFooAndBarProperty|TestAccessProperties\WithFooProperty::$bar.', 177, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\SomeInterface&TestAccessProperties\WithFooProperty::$bar.', 194, + $tipText, ], [ 'Cannot access property $ipsum on TestAccessProperties\FooAccessProperties|null.', @@ -127,10 +141,12 @@ public function testAccessProperties(): void [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$lorem.', 248, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$dolor.', 251, + $tipText, ], [ 'Cannot access property $bar on TestAccessProperties\NullCoalesce|null.', @@ -147,6 +163,7 @@ public function testAccessProperties(): void [ 'Access to an undefined property class@anonymous/tests/PHPStan/Rules/Properties/data/access-properties.php:297::$barProperty.', 302, + $tipText, ], [ 'Cannot access property $selfOrNull on TestAccessProperties\RevertNonNullabilityForIsset|null.', @@ -161,12 +178,15 @@ public function testAccessPropertiesWithoutUnionTypes(): void $this->checkThisOnly = false; $this->checkUnionTypes = false; $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse( [__DIR__ . '/data/access-properties.php'], [ [ 'Access to an undefined property TestAccessProperties\BarAccessProperties::$loremipsum.', 24, + $tipText, ], [ 'Access to private property $foo of parent class TestAccessProperties\FooAccessProperties.', @@ -187,10 +207,12 @@ public function testAccessPropertiesWithoutUnionTypes(): void [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$baz.', 50, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$nonexistent.', 53, + $tipText, ], [ 'Access to private property TestAccessProperties\FooAccessProperties::$foo.', @@ -208,26 +230,32 @@ public function testAccessPropertiesWithoutUnionTypes(): void [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$emptyBaz.', 69, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$emptyNonexistent.', 71, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherNonexistent.', 77, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherNonexistent.', 78, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherEmptyNonexistent.', 81, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherEmptyNonexistent.', 84, + $tipText, ], [ 'Access to property $test on an unknown class TestAccessProperties\FirstUnknownClass.', @@ -242,6 +270,7 @@ public function testAccessPropertiesWithoutUnionTypes(): void [ 'Access to an undefined property TestAccessProperties\SomeInterface&TestAccessProperties\WithFooProperty::$bar.', 194, + $tipText, ], [ 'Cannot access property $foo on null.', @@ -250,10 +279,12 @@ public function testAccessPropertiesWithoutUnionTypes(): void [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$lorem.', 248, + $tipText, ], [ 'Access to an undefined property TestAccessProperties\FooAccessProperties::$dolor.', 251, + $tipText, ], [ 'Cannot access property $bar on TestAccessProperties\NullCoalesce|null.', @@ -262,6 +293,7 @@ public function testAccessPropertiesWithoutUnionTypes(): void [ 'Access to an undefined property class@anonymous/tests/PHPStan/Rules/Properties/data/access-properties.php:297::$barProperty.', 302, + $tipText, ], ], ); @@ -275,10 +307,13 @@ public function testRuleAssignOp(): void $this->checkThisOnly = false; $this->checkUnionTypes = true; $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/access-properties-assign-op.php'], [ [ 'Access to an undefined property TestAccessProperties\AssignOpNonexistentProperty::$flags.', 10, + $tipText, ], ]); } @@ -288,12 +323,15 @@ public function testAccessPropertiesOnThisOnly(): void $this->checkThisOnly = true; $this->checkUnionTypes = true; $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse( [__DIR__ . '/data/access-properties.php'], [ [ 'Access to an undefined property TestAccessProperties\BarAccessProperties::$loremipsum.', 24, + $tipText, ], [ 'Access to private property $foo of parent class TestAccessProperties\FooAccessProperties.', @@ -308,6 +346,8 @@ public function testAccessPropertiesAfterIsNullInBooleanOr(): void $this->checkThisOnly = false; $this->checkUnionTypes = true; $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/access-properties-after-isnull.php'], [ [ 'Cannot access property $fooProperty on null.', @@ -320,10 +360,12 @@ public function testAccessPropertiesAfterIsNullInBooleanOr(): void [ 'Access to an undefined property AccessPropertiesAfterIsNull\Foo::$barProperty.', 28, + $tipText, ], [ 'Access to an undefined property AccessPropertiesAfterIsNull\Foo::$barProperty.', 31, + $tipText, ], [ 'Cannot access property $fooProperty on null.', @@ -336,10 +378,12 @@ public function testAccessPropertiesAfterIsNullInBooleanOr(): void [ 'Access to an undefined property AccessPropertiesAfterIsNull\Foo::$barProperty.', 47, + $tipText, ], [ 'Access to an undefined property AccessPropertiesAfterIsNull\Foo::$barProperty.', 50, + $tipText, ], ]); } @@ -349,10 +393,13 @@ public function testDateIntervalChildProperties(): void $this->checkThisOnly = false; $this->checkUnionTypes = true; $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/date-interval-child-properties.php'], [ [ 'Access to an undefined property AccessPropertiesDateIntervalChild\DateIntervalChild::$nonexistent.', 14, + $tipText, ], ]); } @@ -392,10 +439,13 @@ public function testMixin(): void $this->checkThisOnly = false; $this->checkUnionTypes = true; $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/mixin.php'], [ [ 'Access to an undefined property MixinProperties\GenericFoo::$namee.', 55, + $tipText, ], ]); } @@ -414,10 +464,12 @@ public function testNullSafe(): void $this->checkUnionTypes = true; $this->checkDynamicProperties = false; + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/nullsafe-property-fetch.php'], [ [ 'Access to an undefined property NullsafePropertyFetch\Foo::$baz.', 13, + $tipText, ], [ 'Cannot access property $bar on string.', @@ -435,6 +487,14 @@ public function testNullSafe(): void 'Cannot access property $bar on string.', 22, ], + [ + 'Cannot access property $foo on null.', + 28, + ], + [ + 'Cannot access property $foo on null.', + 29, + ], ]); } @@ -499,14 +559,18 @@ public function testBug6385(): void $this->checkThisOnly = false; $this->checkUnionTypes = true; $this->checkDynamicProperties = false; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $this->analyse([__DIR__ . '/data/bug-6385.php'], [ [ 'Access to an undefined property UnitEnum::$value.', 43, + $tipText, ], [ 'Access to an undefined property Bug6385\ActualUnitEnum::$value.', 47, + $tipText, ], ]); } @@ -563,49 +627,97 @@ public function testBug3659(): void public function dataDynamicProperties(): array { + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; $errors = [ + [ + 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', + 23, + $tipText, + ], + ]; + + $errorsWithMore = array_merge([ [ 'Access to an undefined property DynamicProperties\Foo::$dynamicProperty.', 9, + $tipText, ], [ 'Access to an undefined property DynamicProperties\Foo::$dynamicProperty.', 10, + $tipText, ], [ 'Access to an undefined property DynamicProperties\Foo::$dynamicProperty.', 11, + $tipText, ], [ 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', 14, + $tipText, ], [ 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', 15, + $tipText, ], [ 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', 16, + $tipText, ], + ], $errors); + + $errorsWithMore = array_merge($errorsWithMore, [ [ 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', - 23, + 26, + $tipText, ], - ]; + [ + 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', + 27, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', + 28, + $tipText, + ], + ]); - $errorsWithMore = $errors; - $errorsWithMore[] = [ - 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', - 26, - ]; - $errorsWithMore[] = [ - 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', - 27, - ]; - $errorsWithMore[] = [ - 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', - 28, + $otherErrors = [ + [ + 'Access to an undefined property DynamicProperties\FinalFoo::$dynamicProperty.', + 36, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\FinalFoo::$dynamicProperty.', + 37, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\FinalFoo::$dynamicProperty.', + 38, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\FinalBar::$dynamicProperty.', + 41, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\FinalBar::$dynamicProperty.', + 42, + $tipText, + ], + [ + 'Access to an undefined property DynamicProperties\FinalBar::$dynamicProperty.', + 43, + $tipText, + ], ]; return [ @@ -613,15 +725,16 @@ public function dataDynamicProperties(): array [ 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', 23, + $tipText, ], - ] : $errors], - [true, $errorsWithMore], + ] : array_merge($errors, $otherErrors)], + [true, array_merge($errorsWithMore, $otherErrors)], ]; } /** * @dataProvider dataDynamicProperties - * @param mixed[] $errors + * @param list $errors */ public function testDynamicProperties(bool $checkDynamicProperties, array $errors): void { @@ -670,19 +783,35 @@ public function dataTrueAndFalse(): array public function testPhp82AndDynamicProperties(bool $b): void { $errors = []; + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; if (PHP_VERSION_ID >= 80200) { $errors[] = [ 'Access to an undefined property Php82DynamicProperties\ClassA::$properties.', 34, + $tipText, ]; + if ($b) { + $errors[] = [ + 'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.', + 71, + $tipText, + ]; + } $errors[] = [ - 'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.', - 71, + 'Access to an undefined property Php82DynamicProperties\FinalHelloWorld::$world.', + 105, + $tipText, ]; } elseif ($b) { $errors[] = [ 'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.', 71, + $tipText, + ]; + $errors[] = [ + 'Access to an undefined property Php82DynamicProperties\FinalHelloWorld::$world.', + 105, + $tipText, ]; } $this->checkThisOnly = false; @@ -697,10 +826,12 @@ public function testPhp82AndDynamicProperties(bool $b): void public function testPhp82AndDynamicPropertiesAllow(bool $b): void { $errors = []; + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; if ($b) { $errors[] = [ 'Access to an undefined property Php82DynamicPropertiesAllow\HelloWorld::$world.', 75, + $tipText, ]; } $this->checkThisOnly = false; @@ -709,4 +840,101 @@ public function testPhp82AndDynamicPropertiesAllow(bool $b): void $this->analyse([__DIR__ . '/data/php-82-dynamic-properties-allow.php'], $errors); } + public function testBug2435(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-2435.php'], []); + } + + public function testBug7640(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-7640.php'], []); + } + + public function testBug3572(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-3572.php'], []); + } + + public function testBug393(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-393.php'], []); + } + + public function testObjectShapes(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; + $this->analyse([__DIR__ . '/data/properties-object-shapes.php'], [ + [ + 'Access to an undefined property object{foo: int, bar?: string}::$bar.', + 15, + $tipText, + ], + [ + 'Access to an undefined property object{foo: int, bar?: string}::$baz.', + 16, + $tipText, + ], + ]); + } + + public function testConflictingAnnotationProperty(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/conflicting-annotation-property.php'], []); + } + + public function testBug8536(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/../Comparison/data/bug-8536.php'], []); + } + + public function testRequireExtends(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + + $tipText = 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'; + $this->analyse([__DIR__ . '/data/require-extends.php'], [ + [ + 'Access to an undefined property RequireExtends\MyInterface::$bar.', + 36, + $tipText, + ], + ]); + } + + public function testBug8629(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/bug-8629.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php index 720ecfe015..e8cd8306de 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Properties; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -18,7 +20,14 @@ protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); return new AccessStaticPropertiesInAssignRule( - new AccessStaticPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false, false), new ClassCaseSensitivityCheck($reflectionProvider, true)), + new AccessStaticPropertiesRule( + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + ), ); } diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index 45fca7890b..14e4779f22 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Properties; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -19,8 +21,11 @@ protected function getRule(): Rule $reflectionProvider = $this->createReflectionProvider(); return new AccessStaticPropertiesRule( $reflectionProvider, - new RuleLevelHelper($reflectionProvider, true, false, true, false, false), - new ClassCaseSensitivityCheck($reflectionProvider, true), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), ); } @@ -373,6 +378,10 @@ public function testAccessStaticPropertiesPhp82(): void 'Cannot access static property $anotherProperty on ClassOrString|false.', 150, ], + [ + 'Static access to instance property ClassOrString::$instanceProperty.', + 152, + ], [ 'Access to an undefined static property AccessInIsset::$foo.', 178, @@ -440,4 +449,18 @@ public function testBug6809(): void $this->analyse([__DIR__ . '/data/bug-6809.php'], $errors); } + public function testBug8333(): void + { + $this->analyse([__DIR__ . '/data/bug-8333.php'], [ + [ + 'Access to an undefined static property static(Bug8333\BarAccessProperties)::$loremipsum.', + 68, + ], + [ + 'Access to private static property $foo of parent class Bug8333\FooAccessProperties.', + 69, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/Bug7074Test.php b/tests/PHPStan/Rules/Properties/Bug7074Test.php index e6e4679321..3748675490 100644 --- a/tests/PHPStan/Rules/Properties/Bug7074Test.php +++ b/tests/PHPStan/Rules/Properties/Bug7074Test.php @@ -15,7 +15,7 @@ class Bug7074Test extends RuleTestCase protected function getRule(): Rule { - return new DefaultValueTypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false)); + return new DefaultValueTypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false)); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php index 2e6a85544f..50b4abe32b 100644 --- a/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php @@ -15,7 +15,7 @@ class DefaultValueTypesAssignedToPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new DefaultValueTypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false)); + return new DefaultValueTypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false)); } public function testDefaultValueTypesAssignedToProperties(): void diff --git a/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php index 518780cd6d..97a9f1b1f6 100644 --- a/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php @@ -4,6 +4,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -19,10 +21,13 @@ class ExistingClassesInPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new ExistingClassesInPropertiesRule( - $broker, - new ClassCaseSensitivityCheck($broker, true), + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersion), true, @@ -160,7 +165,7 @@ public function dataIntersectionTypes(): array /** * @dataProvider dataIntersectionTypes - * @param mixed[] $errors + * @param list $errors */ public function testIntersectionTypes(int $phpVersion, array $errors): void { diff --git a/tests/PHPStan/Rules/Properties/InvalidCallablePropertyTypeRuleTest.php b/tests/PHPStan/Rules/Properties/InvalidCallablePropertyTypeRuleTest.php new file mode 100644 index 0000000000..581a70e179 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/InvalidCallablePropertyTypeRuleTest.php @@ -0,0 +1,41 @@ + + */ +class InvalidCallablePropertyTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new InvalidCallablePropertyTypeRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/invalid-callable-property-type.php'], [ + [ + 'Property InvalidCallablePropertyType\HelloWorld::$a cannot have callable in its type declaration.', + 9, + ], + [ + 'Property InvalidCallablePropertyType\HelloWorld::$b cannot have callable in its type declaration.', + 12, + ], + [ + 'Property InvalidCallablePropertyType\HelloWorld::$c cannot have callable in its type declaration.', + 15, + ], + [ + 'Property InvalidCallablePropertyType\HelloWorld::$callback cannot have callable in its type declaration.', + 23, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php b/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php index c25f58afae..4a10bebc8c 100644 --- a/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php @@ -14,8 +14,7 @@ class MissingPropertyTypehintRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new MissingPropertyTypehintRule(new MissingTypehintCheck($broker, true, true, true, true, [])); + return new MissingPropertyTypehintRule(new MissingTypehintCheck(true, true, true, true, [])); } public function testRule(): void @@ -40,17 +39,22 @@ public function testRule(): void ], [ 'Property MissingPropertyTypehint\Bar::$foo with generic interface MissingPropertyTypehint\GenericInterface does not specify its types: T, U', - 74, + 77, 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Property MissingPropertyTypehint\Bar::$baz with generic class MissingPropertyTypehint\GenericClass does not specify its types: A, B', - 80, + 83, 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', ], [ 'Property MissingPropertyTypehint\CallableSignature::$cb type has no signature specified for callable.', - 93, + 96, + ], + [ + 'Property MissingPropertyTypehint\NestedArrayInProperty::$args type has no value type specified in iterable type array.', + 106, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], ]); } diff --git a/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php index 30f3872c84..c7a90b03ab 100644 --- a/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -18,9 +18,12 @@ class MissingReadOnlyByPhpDocPropertyAssignRuleTest extends RuleTestCase protected function getRule(): Rule { return new MissingReadOnlyByPhpDocPropertyAssignRule( - new ConstructorsHelper([ - 'MissingReadOnlyPropertyAssignPhpDoc\\TestCase::setUp', - ]), + new ConstructorsHelper( + self::getContainer(), + [ + 'MissingReadOnlyPropertyAssignPhpDoc\\TestCase::setUp', + ], + ), ); } @@ -81,6 +84,10 @@ public function testRule(): void 'Class MissingReadOnlyPropertyAssignPhpDoc\BarDoubleAssignInSetter has an uninitialized @readonly property $foo. Assign it in the constructor.', 57, ], + [ + 'Class MissingReadOnlyPropertyAssignPhpDoc\AssignOp has an uninitialized @readonly property $foo. Assign it in the constructor.', + 85, + ], [ 'Access to an uninitialized @readonly property MissingReadOnlyPropertyAssignPhpDoc\AssignOp::$foo.', 92, diff --git a/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php index 638e9d74b4..bd4b77daf2 100644 --- a/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php @@ -7,6 +7,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use function in_array; +use function strpos; use const PHP_VERSION_ID; /** @@ -18,9 +19,15 @@ class MissingReadOnlyPropertyAssignRuleTest extends RuleTestCase protected function getRule(): Rule { return new MissingReadOnlyPropertyAssignRule( - new ConstructorsHelper([ - 'MissingReadOnlyPropertyAssign\\TestCase::setUp', - ]), + new ConstructorsHelper( + self::getContainer(), + [ + 'MissingReadOnlyPropertyAssign\\TestCase::setUp', + 'Bug10523\\Controller::init', + 'Bug10523\\MultipleWrites::init', + 'Bug10523\\SingleWriteInConstructorCalledMethod::init', + ], + ), ); } @@ -51,6 +58,25 @@ private function isEntityId(PropertyReflection $property, string $propertyName): } }, + new class() implements ReadWritePropertiesExtension { + + public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool + { + return false; + } + + public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool + { + return $this->isInitialized($property, $propertyName); + } + + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return $property->isPublic() && + strpos($property->getDocComment() ?? '', '@init') !== false; + } + + }, ]; } @@ -81,6 +107,10 @@ public function testRule(): void 'Class MissingReadOnlyPropertyAssign\BarDoubleAssignInSetter has an uninitialized readonly property $foo. Assign it in the constructor.', 53, ], + [ + 'Class MissingReadOnlyPropertyAssign\AssignOp has an uninitialized readonly property $foo. Assign it in the constructor.', + 79, + ], [ 'Access to an uninitialized readonly property MissingReadOnlyPropertyAssign\AssignOp::$foo.', 85, @@ -105,6 +135,22 @@ public function testRule(): void 'Readonly property MissingReadOnlyPropertyAssign\FooTraitClass::$doubleAssigned is already assigned.', 149, ], + [ + 'Readonly property MissingReadOnlyPropertyAssign\AdditionalAssignOfReadonlyPromotedProperty::$x is already assigned.', + 188, + ], + [ + 'Access to an uninitialized readonly property MissingReadOnlyPropertyAssign\MethodCalledFromConstructorBeforeAssign::$foo.', + 226, + ], + [ + 'Access to an uninitialized readonly property MissingReadOnlyPropertyAssign\MethodCalledTwice::$foo.', + 244, + ], + [ + 'Class MissingReadOnlyPropertyAssign\PropertyAssignedOnDifferentObjectUninitialized has an uninitialized readonly property $foo. Assign it in the constructor.', + 264, + ], ]); } @@ -126,4 +172,119 @@ public function testBug7314(): void $this->analyse([__DIR__ . '/data/bug-7314.php'], []); } + public function testBug8412(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-8412.php'], []); + } + + public function testBug8958(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-8958.php'], []); + } + + public function testBug8563(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-8563.php'], []); + } + + public function testBug6402(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-6402.php'], [ + [ + 'Access to an uninitialized readonly property Bug6402\SomeModel2::$views.', + 28, + ], + ]); + } + + public function testBug7198(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-7198.php'], []); + } + + public function testBug7649(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-7649.php'], [ + [ + 'Class Bug7649\Foo has an uninitialized readonly property $bar. Assign it in the constructor.', + 7, + ], + ]); + } + + public function testBug9577(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/../Classes/data/bug-9577.php'], [ + [ + 'Class Bug9577\SpecializedException2 has an uninitialized readonly property $message. Assign it in the constructor.', + 8, + ], + ]); + } + + public function testAnonymousReadonlyClass(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->analyse([__DIR__ . '/data/missing-readonly-anonymous-class-property-assign.php'], [ + [ + 'Class class@anonymous/tests/PHPStan/Rules/Properties/data/missing-readonly-anonymous-class-property-assign.php:10 has an uninitialized readonly property $foo. Assign it in the constructor.', + 11, + ], + ]); + } + + public function testBug10523(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-10523.php'], [ + [ + 'Readonly property Bug10523\MultipleWrites::$userAccount is already assigned.', + 55, + ], + ]); + } + + public function testBug10822(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-10822.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php b/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php index 14fc1b5d33..b442a9dd2f 100644 --- a/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php +++ b/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php @@ -41,4 +41,41 @@ public function testBug7109(): void $this->analyse([__DIR__ . '/data/bug-7109.php'], []); } + public function testBug5172(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/../../Analyser/data/bug-5172.php'], []); + } + + public function testBug7980(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/../../Analyser/data/bug-7980.php'], []); + } + + public function testBug8517(): void + { + $this->analyse([__DIR__ . '/../../Analyser/data/bug-8517.php'], []); + } + + public function testBug9105(): void + { + $this->analyse([__DIR__ . '/../../Analyser/data/bug-9105.php'], []); + } + + public function testBug6922(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-6922.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php index 83106d26aa..dafac4e5f0 100644 --- a/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php @@ -151,7 +151,7 @@ public function dataRulePHPDocTypes(): array /** * @dataProvider dataRulePHPDocTypes - * @param mixed[] $errors + * @param list $errors */ public function testRulePHPDocTypes(bool $reportMaybes, array $errors): void { @@ -165,4 +165,10 @@ public function testBug7839(): void $this->analyse([__DIR__ . '/data/bug-7839.php'], []); } + public function testBug7692(): void + { + $this->reportMaybes = true; + $this->analyse([__DIR__ . '/data/bug-7692.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php b/tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php new file mode 100644 index 0000000000..ecf4597d97 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php @@ -0,0 +1,33 @@ + + */ +class PropertiesInInterfaceRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PropertiesInInterfaceRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/properties-in-interface.php'], [ + [ + 'Interfaces may not include properties.', + 7, + ], + [ + 'Interfaces may not include properties.', + 9, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php index 94b145358c..b6d394d30b 100644 --- a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php @@ -5,6 +5,8 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\ClassForbiddenNameCheck; +use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -25,7 +27,7 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true, false, false), + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), @@ -36,7 +38,11 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, ), ); } @@ -51,4 +57,18 @@ public function testRule(): void ]); } + public function testDeprecatedAttribute(): void + { + $this->analyse([__DIR__ . '/data/property-attributes-deprecated.php'], [ + [ + 'Attribute class DeprecatedPropertyAttribute\DoSomethingTheOldWay is deprecated.', + 16, + ], + [ + 'Attribute class DeprecatedPropertyAttribute\DoSomethingTheOldWayWithDescription is deprecated: Use something else please', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php index bac6bace61..e5496fc646 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -18,6 +18,7 @@ protected function getRule(): Rule return new ReadOnlyByPhpDocPropertyAssignRule( new PropertyReflectionFinder(), new ConstructorsHelper( + self::getContainer(), [ 'ReadonlyPropertyAssignPhpDoc\\TestCase::setUp', ], @@ -30,87 +31,99 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/readonly-assign-phpdoc.php'], [ [ '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$foo is assigned outside of the constructor.', - 40, + 47, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$phan is assigned outside of the constructor.', + 49, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$bar is assigned outside of its declaring class.', - 53, + 61, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$baz is assigned outside of its declaring class.', - 54, + 62, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$psalm is assigned outside of its declaring class.', - 55, + 63, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$phan is assigned outside of its declaring class.', + 64, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$bar is assigned outside of its declaring class.', - 60, + 69, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$psalm is assigned outside of its declaring class.', - 61, + 70, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$phan is assigned outside of its declaring class.', + 71, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$baz is assigned outside of its declaring class.', - 68, + 78, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\FooArrays::$details is assigned outside of the constructor.', - 87, + 97, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\FooArrays::$details is assigned outside of the constructor.', - 88, + 98, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\NotThis::$foo is not assigned on $this.', - 118, + 128, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\PostInc::$foo is assigned outside of the constructor.', - 134, + 144, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\PostInc::$foo is assigned outside of the constructor.', - 135, + 145, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\PostInc::$foo is assigned outside of the constructor.', - 137, + 147, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\ListAssign::$foo is assigned outside of the constructor.', - 158, + 168, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\ListAssign::$foo is assigned outside of the constructor.', - 163, + 173, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$baz is assigned outside of its declaring class.', - 173, + 183, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\Foo::$baz is assigned outside of its declaring class.', - 174, + 184, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\Immutable::$foo is assigned outside of the constructor.', - 237, + 247, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\B::$b is assigned outside of the constructor.', - 269, + 279, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\A::$a is assigned outside of its declaring class.', - 270, + 280, ], [ '@readonly property ReadonlyPropertyAssignPhpDoc\C::$c is assigned outside of the constructor.', - 283, + 293, ], ]); } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php index 3259cad332..c2dce590b6 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php @@ -52,4 +52,18 @@ public function testRuleIgnoresNativeReadonly(): void $this->analyse([__DIR__ . '/data/read-only-property-phpdoc-and-native.php'], []); } + public function testRuleAllowedPrivateMutation(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/read-only-property-phpdoc-allowed-private-mutation.php'], [ + [ + '@readonly property cannot have a default value.', + 9, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php index 8c013acd93..90da9c44ec 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php @@ -18,6 +18,7 @@ protected function getRule(): Rule return new ReadOnlyPropertyAssignRule( new PropertyReflectionFinder(), new ConstructorsHelper( + self::getContainer(), [ 'ReadonlyPropertyAssign\\TestCase::setUp', ], @@ -84,7 +85,7 @@ public function testRule(): void 'Readonly property ReadonlyPropertyAssign\ListAssign::$foo is assigned outside of the constructor.', 127, ], - [ + /*[ 'Readonly property ReadonlyPropertyAssign\FooEnum::$name is assigned outside of the constructor.', 140, ], @@ -99,7 +100,7 @@ public function testRule(): void [ 'Readonly property ReadonlyPropertyAssign\FooEnum::$value is assigned outside of its declaring class.', 152, - ], + ],*/ [ 'Readonly property ReadonlyPropertyAssign\Foo::$baz is assigned outside of its declaring class.', 162, @@ -139,4 +140,18 @@ public function testReadOnlyClasses(): void ]); } + public function testBug6773(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-6773.php'], [ + [ + 'Readonly property Bug6773\Repository::$data is assigned outside of the constructor.', + 16, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php index 51776a3dda..30f9606744 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php @@ -81,7 +81,7 @@ public function dataRule(): array /** * @dataProvider dataRule - * @param mixed[] $errors + * @param list $errors */ public function testRule(int $phpVersionId, array $errors): void { @@ -91,7 +91,7 @@ public function testRule(int $phpVersionId, array $errors): void /** * @dataProvider dataRule - * @param mixed[] $errors + * @param list $errors */ public function testRuleReadonlyClass(int $phpVersionId, array $errors): void { diff --git a/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php index 5044bf10dc..d5834f4003 100644 --- a/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php @@ -16,7 +16,7 @@ class ReadingWriteOnlyPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ReadingWriteOnlyPropertiesRule(new PropertyDescriptor(), new PropertyReflectionFinder(), new RuleLevelHelper($this->createReflectionProvider(), true, $this->checkThisOnly, true, false, false), $this->checkThisOnly); + return new ReadingWriteOnlyPropertiesRule(new PropertyDescriptor(), new PropertyReflectionFinder(), new RuleLevelHelper($this->createReflectionProvider(), true, $this->checkThisOnly, true, false, false, true, false), $this->checkThisOnly); } public function testPropertyMustBeReadableInAssignOp(): void @@ -25,11 +25,11 @@ public function testPropertyMustBeReadableInAssignOp(): void $this->analyse([__DIR__ . '/data/writing-to-read-only-properties.php'], [ [ 'Property WritingToReadOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 25, + 27, ], [ 'Property WritingToReadOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 35, + 40, ], ]); } @@ -40,7 +40,7 @@ public function testPropertyMustBeReadableInAssignOpCheckThisOnly(): void $this->analyse([__DIR__ . '/data/writing-to-read-only-properties.php'], [ [ 'Property WritingToReadOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 25, + 27, ], ]); } @@ -51,11 +51,11 @@ public function testReadingWriteOnlyProperties(): void $this->analyse([__DIR__ . '/data/reading-write-only-properties.php'], [ [ 'Property ReadingWriteOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 20, + 23, ], [ 'Property ReadingWriteOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 25, + 29, ], ]); } @@ -66,7 +66,7 @@ public function testReadingWriteOnlyPropertiesCheckThisOnly(): void $this->analyse([__DIR__ . '/data/reading-write-only-properties.php'], [ [ 'Property ReadingWriteOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 20, + 23, ], ]); } @@ -77,9 +77,15 @@ public function testNullsafe(): void $this->analyse([__DIR__ . '/data/reading-write-only-properties-nullsafe.php'], [ [ 'Property ReadingWriteOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 9, + 10, ], ]); } + public function testConflictingAnnotationProperty(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/conflicting-annotation-property.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleNoBleedingEdgeTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleNoBleedingEdgeTest.php index fe7d6628f6..9b0aaf913d 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleNoBleedingEdgeTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleNoBleedingEdgeTest.php @@ -16,7 +16,7 @@ class TypesAssignedToPropertiesRuleNoBleedingEdgeTest extends RuleTestCase protected function getRule(): Rule { - return new TypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, false), new PropertyDescriptor(), new PropertyReflectionFinder()); + return new TypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, false, true, false), new PropertyReflectionFinder()); } public function testGenericObjectWithUnspecifiedTemplateTypes(): void diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index cd0fa17e76..7a788aa3cc 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -17,7 +17,7 @@ class TypesAssignedToPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new TypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, false), new PropertyDescriptor(), new PropertyReflectionFinder()); + return new TypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, false, true, false), new PropertyReflectionFinder()); } public function testTypesAssignedToProperties(): void @@ -213,10 +213,6 @@ public function testBug3777(): void public function testAppendendArrayKey(): void { $this->analyse([__DIR__ . '/../Arrays/data/appended-array-key.php'], [ - [ - 'Property AppendedArrayKey\Foo::$intArray (array) does not accept array.', - 27, - ], [ 'Property AppendedArrayKey\Foo::$intArray (array) does not accept array.', 28, @@ -326,10 +322,12 @@ public function testBug6286(): void [ 'Property Bug6286\HelloWorld::$details (array{name: string, age: int}) does not accept array{name: string, age: \'Forty-two\'}.', 19, + "Offset 'age' (int) does not accept type string.", ], [ "Property Bug6286\HelloWorld::\$nestedDetails (array) does not accept non-empty-array.", 22, + "Offset 'age' (int) does not accept type int|string.", ], ]); } @@ -464,14 +462,17 @@ public function testBug6356b(): void [ 'Property Bug6356b\HelloWorld2::$details (array{name: string, age: int}) does not accept array{name: string, age: \'Forty-two\'}.', 19, + "Offset 'age' (int) does not accept type string.", ], [ 'Property Bug6356b\HelloWorld2::$nestedDetails (array) does not accept non-empty-array.', 21, + "Offset 'age' (int) does not accept type int|string.", ], [ 'Property Bug6356b\HelloWorld2::$nestedDetails (array) does not accept non-empty-array.', 26, + "Offset 'age' (int) does not accept type int|string.", ], ]); } @@ -503,4 +504,108 @@ public function testIntegerRangesAndConstants(): void ]); } + public function testBug3311b(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-3311b.php'], [ + [ + 'Property Bug3311b\Foo::$bar (list) does not accept non-empty-array, string>.', + 16, + 'non-empty-array, string> might not be a list.', + ], + ]); + } + + public function testBug7789(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7789.php'], []); + } + + public function testBug9131(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-9131.php'], []); + } + + public function testBug8222(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8222.php'], []); + } + + public function testWritingReadonlyProperty(): void + { + $this->analyse([__DIR__ . '/data/writing-to-read-only-properties.php'], [ + [ + 'Property WritingToReadOnlyProperties\Foo::$usualProperty (int) does not accept string.', + 24, + ], + [ + 'Property WritingToReadOnlyProperties\Foo::$writeOnlyProperty (int) does not accept string.', + 27, + ], + [ + 'Property WritingToReadOnlyProperties\Foo::$usualProperty (int) does not accept string.', + 34, + ], + [ + 'Property WritingToReadOnlyProperties\Foo::$writeOnlyProperty (int) does not accept string.', + 40, + ], + ]); + } + + public function testBug8190(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8190.php'], []); + } + + public function testBug8074(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8074.php'], []); + } + + public function testBug7087(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-7087.php'], []); + } + + public function testUnset(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/property-type-after-unset.php'], [ + [ + 'Property PropertyTypeAfterUnset\Foo::$nonEmpty (non-empty-array) does not accept array.', + 19, + 'array might be empty.', + ], + [ + 'Property PropertyTypeAfterUnset\Foo::$listProp (list) does not accept array, int>.', + 20, + 'array, int> might not be a list.', + ], + [ + 'Property PropertyTypeAfterUnset\Foo::$nestedListProp (array>) does not accept array, int>>.', + 21, + 'array, int> might not be a list.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php index 7273e07695..39fdf5acc1 100644 --- a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\Reflection\PropertyReflection; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function strpos; /** * @extends RuleTestCase @@ -17,8 +18,13 @@ protected function getRule(): Rule { return new UninitializedPropertyRule( new ConstructorsHelper( + self::getContainer(), [ 'UninitializedProperty\\TestCase::setUp', + 'Bug9619\\AdminPresenter::startup', + 'Bug9619\\AdminPresenter2::startup', + 'Bug9619\\AdminPresenter3::startup', + 'Bug9619\\AdminPresenter3::startup2', ], ), ); @@ -45,6 +51,34 @@ public function isInitialized(PropertyReflection $property, string $propertyName } }, + + // bug-9619 + new class() implements ReadWritePropertiesExtension { + + public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool + { + return false; + } + + public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool + { + return $this->isInitialized($property, $propertyName); + } + + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return $property->isPublic() && + strpos($property->getDocComment() ?? '', '@inject') !== false; + } + + }, + ]; + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/uninitialized-property-rule.neon', ]; } @@ -79,6 +113,22 @@ public function testRule(): void 'Class UninitializedProperty\FooTraitClass has an uninitialized property $baz. Give it default value or assign it in the constructor.', 159, ], + /*[ + 'Access to an uninitialized property UninitializedProperty\InitializedInPublicSetterNonFinalClass::$foo.', + 278, + ],*/ + [ + 'Class UninitializedProperty\SometimesInitializedInPrivateSetter has an uninitialized property $foo. Give it default value or assign it in the constructor.', + 286, + ], + [ + 'Access to an uninitialized property UninitializedProperty\SometimesInitializedInPrivateSetter::$foo.', + 303, + ], + [ + 'Class UninitializedProperty\EarlyReturn has an uninitialized property $foo. Give it default value or assign it in the constructor.', + 372, + ], ]); } @@ -113,4 +163,43 @@ public function testBug7219(): void ]); } + public function testAdditionalConstructorsExtension(): void + { + $this->analyse([__DIR__ . '/data/uninitialized-property-additional-constructors.php'], [ + [ + 'Class TestInitializedProperty\TestAdditionalConstructor has an uninitialized property $one. Give it default value or assign it in the constructor.', + 07, + ], + [ + 'Class TestInitializedProperty\TestAdditionalConstructor has an uninitialized property $three. Give it default value or assign it in the constructor.', + 11, + ], + ]); + } + + public function testEfabricaLatteBug(): void + { + $this->analyse([__DIR__ . '/data/efabrica-latte-bug.php'], []); + } + + public function testBug9619(): void + { + $this->analyse([__DIR__ . '/data/bug-9619.php'], [ + [ + 'Access to an uninitialized property Bug9619\AdminPresenter3::$user.', + 55, + ], + ]); + } + + public function testBug9831(): void + { + $this->analyse([__DIR__ . '/data/bug-9831.php'], [ + [ + 'Access to an uninitialized property Bug9831\Foo::$bar.', + 12, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/VirtualNullsafePropertyFetchTest.php b/tests/PHPStan/Rules/Properties/VirtualNullsafePropertyFetchTest.php new file mode 100644 index 0000000000..12c42b7aea --- /dev/null +++ b/tests/PHPStan/Rules/Properties/VirtualNullsafePropertyFetchTest.php @@ -0,0 +1,56 @@ + + */ +class VirtualNullsafePropertyFetchTest extends RuleTestCase +{ + + /** + * @return Rule + */ + protected function getRule(): Rule + { + return new /** @implements Rule */ class implements Rule { + + public function getNodeType(): string + { + return PropertyFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node->getAttribute('virtualNullsafePropertyFetch') === true) { + return [RuleErrorBuilder::message('Nullable property fetch detected')->identifier('ruleTest.VirtualNullsafeProperty')->build()]; + } + + return [RuleErrorBuilder::message('Regular property fetch detected')->identifier('ruleTest.VirtualNullsafeProperty')->build()]; + } + + }; + } + + public function testAttribute(): void + { + $this->analyse([ __DIR__ . '/data/virtual-nullsafe-property-fetch.php'], [ + [ + 'Regular property fetch detected', + 3, + ], + [ + 'Nullable property fetch detected', + 4, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php index 358fb56411..d3196aba89 100644 --- a/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php @@ -16,7 +16,7 @@ class WritingToReadOnlyPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new WritingToReadOnlyPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false), new PropertyDescriptor(), new PropertyReflectionFinder(), $this->checkThisOnly); + return new WritingToReadOnlyPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false), new PropertyDescriptor(), new PropertyReflectionFinder(), $this->checkThisOnly); } public function testCheckThisOnlyProperties(): void @@ -25,11 +25,11 @@ public function testCheckThisOnlyProperties(): void $this->analyse([__DIR__ . '/data/writing-to-read-only-properties.php'], [ [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 18, + 20, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 19, + 21, ], ]); } @@ -40,25 +40,51 @@ public function testCheckAllProperties(): void $this->analyse([__DIR__ . '/data/writing-to-read-only-properties.php'], [ [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 18, + 20, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 19, + 21, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 28, + 30, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 29, + 31, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 38, + 43, ], ]); } + public function testObjectShapes(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/properties-object-shapes.php'], [ + [ + 'Property object{foo: int, bar?: string}::$foo is not writable.', + 18, + ], + [ + 'Property object{foo: int}|stdClass::$foo is not writable.', + 42, + ], + ]); + } + + public function testConflictingAnnotationProperty(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/conflicting-annotation-property.php'], [ + /*[ + 'Property ConflictingAnnotationProperty\PropertyWithAnnotation::$test is not writable.', + 27, + ],*/ + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/UninitializedPropertyAdditionalConstructorsExtensions.php b/tests/PHPStan/Rules/Properties/data/UninitializedPropertyAdditionalConstructorsExtensions.php new file mode 100644 index 0000000000..316a4e136e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/UninitializedPropertyAdditionalConstructorsExtensions.php @@ -0,0 +1,20 @@ +getName() === 'TestInitializedProperty\\TestAdditionalConstructor') { + return ['setTwo']; + } + + return []; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-10523.php b/tests/PHPStan/Rules/Properties/data/bug-10523.php new file mode 100644 index 0000000000..a5e88ebcd2 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-10523.php @@ -0,0 +1,84 @@ += 8.1 + +namespace Bug10523; + +final class Controller +{ + private readonly B $userAccount; + + public function __construct() + { + $this->userAccount = new B(); + } + + public function init(): void + { + $this->redirectIfNkdeCheckoutNotAllowed(); + $this->redirectIfNoShoppingBasketPresent(); + } + + private function redirectIfNkdeCheckoutNotAllowed(): void + { + + } + + private function redirectIfNoShoppingBasketPresent(): void + { + $x = $this->userAccount; + } + +} + +class B {} + +final class MultipleWrites +{ + private readonly B $userAccount; + + public function __construct() + { + $this->userAccount = new B(); + } + + public function init(): void + { + $this->redirectIfNkdeCheckoutNotAllowed(); + $this->redirectIfNoShoppingBasketPresent(); + } + + private function redirectIfNkdeCheckoutNotAllowed(): void + { + } + + private function redirectIfNoShoppingBasketPresent(): void + { + $this->userAccount = new B(); + } + +} + + +final class SingleWriteInConstructorCalledMethod +{ + private readonly B $userAccount; + + public function __construct() + { + } + + public function init(): void + { + $this->redirectIfNkdeCheckoutNotAllowed(); + $this->redirectIfNoShoppingBasketPresent(); + } + + private function redirectIfNkdeCheckoutNotAllowed(): void + { + } + + private function redirectIfNoShoppingBasketPresent(): void + { + $this->userAccount = new B(); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-10822.php b/tests/PHPStan/Rules/Properties/data/bug-10822.php new file mode 100644 index 0000000000..35ed77467b --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-10822.php @@ -0,0 +1,294 @@ += 8.1 + +namespace Bug10822; + +enum Deprecation: string +{ + case callString = 'call-string'; + case userAuthored = 'user-authored'; +} + +interface FileLocation +{ + public function getOffset(): int; + + public function getLine(): int; + + public function getColumn(): int; +} + +interface FileSpan +{ + public function getSourceUrl(): ?string; + + public function getStart(): FileLocation; + + public function getEnd(): FileLocation; +} + +final class Frame +{ + public function __construct(private readonly string $url, private readonly ?int $line, private readonly ?int $column, private readonly ?string $member) + { + } + + public function getMember(): ?string + { + return $this->member; + } + + public function getLocation(): string + { + $library = $this->url; + + if ($this->line === null) { + return $library; + } + + if ($this->column === null) { + return $library . ' ' . $this->line; + } + + return $library . ' ' . $this->line . ':' . $this->column; + } +} + +final class Trace +{ + /** + * @param list $frames + */ + public function __construct(public readonly array $frames) + { + } +} + +interface DeprecationAwareLoggerInterface +{ + public function warn(string $message, bool $deprecation = false, ?FileSpan $span = null, ?Trace $trace = null): void; + + public function warnForDeprecation(Deprecation $deprecation, string $message, ?FileSpan $span = null, ?Trace $trace = null): void; +} + +interface AstNode +{ + public function getSpan(): FileSpan; +} + +final class SassScriptException extends \Exception +{ +} + +final class SassRuntimeException extends \Exception +{ + public readonly FileSpan $span; + public readonly Trace $sassTrace; + + public function __construct(string $message, FileSpan $span, ?Trace $sassTrace = null, ?\Throwable $previous = null) + { + $this->span = $span; + $this->sassTrace = $sassTrace ?? new Trace([]); + + parent::__construct($message, 0, $previous); + } +} + +interface SassCallable +{ + public function getName(): string; +} + +class BuiltInCallable implements SassCallable +{ + /** + * @param callable(list): Value $callback + */ + public static function function (string $name, string $arguments, callable $callback): BuiltInCallable + { + return new BuiltInCallable($name, [[$arguments, $callback]]); + } + + /** + * @param list): Value}> $overloads + */ + private function __construct(private readonly string $name, public readonly array $overloads) + { + } + + public function getName(): string + { + return $this->name; + } +} + +abstract class Value +{ + public function assertString(string $name): SassString + { + throw new SassScriptException("\$$name: this is not a string."); + } +} + +final class SassString extends Value +{ + public function __construct( + private readonly string $text, + public readonly bool $hasQuotes, + ) + { + } + + public function getText(): string + { + return $this->text; + } + + public function assertString(string $name): SassString + { + return $this; + } +} + +final class SassMixin extends Value +{ + public function __construct(public readonly SassCallable $callable) + { + } +} + +interface ImportCache +{ + public function humanize(string $uri): string; +} + +final class Environment +{ + public function getMixin(string $name): SassCallable + { + throw new \BadMethodCallException('not implemented yet'); + } +} + +class EvaluateVisitor +{ + private readonly ImportCache $importCache; + + /** + * @var array + */ + public array $builtInFunctions = []; + + private readonly DeprecationAwareLoggerInterface $logger; + + /** + * @var array> + */ + private array $warningsEmitted = []; + + private Environment $environment; + + private string $member = "root stylesheet"; + + private ?AstNode $callableNode = null; + + /** + * @var list + */ + private array $stack = []; + + public function __construct(ImportCache $importCache, DeprecationAwareLoggerInterface $logger) + { + $this->importCache = $importCache; + $this->logger = $logger; + $this->environment = new Environment(); + + // These functions are defined in the context of the evaluator because + // they need access to the environment or other local state. + $metaFunctions = [ + BuiltInCallable::function('get-mixin', '$name', function ($arguments) { + $name = $arguments[0]->assertString('name'); + + \assert($this->callableNode !== null); + $callable = $this->addExceptionSpan($this->callableNode, function () use ($name) { + return $this->environment->getMixin(str_replace('_', '-', $name->getText())); + }); + + return new SassMixin($callable); + }), + ]; + + foreach ($metaFunctions as $function) { + $this->builtInFunctions[$function->getName()] = $function; + } + } + + private function stackFrame(string $member, FileSpan $span): Frame + { + $url = $span->getSourceUrl(); + + if ($url !== null) { + $url = $this->importCache->humanize($url); + } + + return new Frame( + $url ?? $span->getSourceUrl() ?? '-', + $span->getStart()->getLine() + 1, + $span->getStart()->getColumn() + 1, + $member + ); + } + + private function stackTrace(?FileSpan $span = null): Trace + { + $frames = []; + + foreach ($this->stack as [$member, $nodeWithSpan]) { + $frames[] = $this->stackFrame($member, $nodeWithSpan->getSpan()); + } + + if ($span !== null) { + $frames[] = $this->stackFrame($this->member, $span); + } + + return new Trace(array_reverse($frames)); + } + + public function warn(string $message, FileSpan $span, ?Deprecation $deprecation = null): void + { + $spanString = ($span->getSourceUrl() ?? '') . "\0" . $span->getStart()->getOffset() . "\0" . $span->getEnd()->getOffset(); + + if (isset($this->warningsEmitted[$message][$spanString])) { + return; + } + $this->warningsEmitted[$message][$spanString] = true; + + $trace = $this->stackTrace($span); + + if ($deprecation === null) { + $this->logger->warn($message, false, $span, $trace); + } else { + $this->logger->warnForDeprecation($deprecation, $message, $span, $trace); + } + } + + /** + * Runs $callback, and converts any {@see SassScriptException}s it throws to + * {@see SassRuntimeException}s with $nodeWithSpan's source span. + * + * @template T + * + * @param callable(): T $callback + * + * @return T + * + * @throws SassRuntimeException + */ + private function addExceptionSpan(AstNode $nodeWithSpan, callable $callback, bool $addStackFrame = true) + { + try { + return $callback(); + } catch (SassScriptException $e) { + throw new SassRuntimeException($e->getMessage(), $nodeWithSpan->getSpan(), $this->stackTrace($addStackFrame ? $nodeWithSpan->getSpan() : null), $e); + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-2435.php b/tests/PHPStan/Rules/Properties/data/bug-2435.php new file mode 100644 index 0000000000..0370be5b97 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-2435.php @@ -0,0 +1,15 @@ +root->root !== null; + } +} + +class Bar extends Foo { +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-3311b.php b/tests/PHPStan/Rules/Properties/data/bug-3311b.php new file mode 100644 index 0000000000..93464208a8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-3311b.php @@ -0,0 +1,17 @@ += 7.4 + +namespace Bug3311b; + +final class Foo +{ + /** + * @var array + * @psalm-var list + */ + public array $bar = []; +} + +function () { + $instance = new Foo; + $instance->bar[1] = 'baz'; +}; diff --git a/tests/PHPStan/Rules/Properties/data/bug-3572.php b/tests/PHPStan/Rules/Properties/data/bug-3572.php new file mode 100644 index 0000000000..e9bd88cd95 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-3572.php @@ -0,0 +1,24 @@ += 7.4 + +namespace Bug3572; + +class A +{ + private string $field; + + public function setField(string $value): void + { + $this->field = $value; + } + + public static function castToB(A $a): B + { + $self = new B(); + $self->field = $a->field; + return $self; + } +} + +class B extends A +{ +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-393.php b/tests/PHPStan/Rules/Properties/data/bug-393.php new file mode 100644 index 0000000000..530f7054b8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-393.php @@ -0,0 +1,34 @@ +privateProperty = 123; + }, + null, + Foo::class + ))(); + + (\Closure::bind( + static function () { + $bar = new Bar(); + $bar->privateProperty = 123; + }, + null, + Foo::class + ))(); +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-4559.php b/tests/PHPStan/Rules/Properties/data/bug-4559.php index 30136d761b..e3c0b6952f 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-4559.php +++ b/tests/PHPStan/Rules/Properties/data/bug-4559.php @@ -4,9 +4,9 @@ class HelloWorld { - public function doBar() + public function doBar(string $s) { - $response = json_decode(''); + $response = json_decode($s); if (isset($response->error->code)) { echo $response->error->message ?? ''; } diff --git a/tests/PHPStan/Rules/Properties/data/bug-6402.php b/tests/PHPStan/Rules/Properties/data/bug-6402.php new file mode 100644 index 0000000000..16f9c8c2aa --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6402.php @@ -0,0 +1,32 @@ += 8.1 + +namespace Bug6402; + +class SomeModel +{ + public readonly ?int $views; + + public function __construct(string $mode, int $views) + { + if ($mode === 'mode1') { + $this->views = $views; + } else { + $this->views = null; + } + } +} + +class SomeModel2 +{ + public readonly ?int $views; + + public function __construct(string $mode, int $views) + { + if ($mode === 'mode1') { + $this->views = $views; + } else { + echo $this->views; + $this->views = null; + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6773.php b/tests/PHPStan/Rules/Properties/data/bug-6773.php new file mode 100644 index 0000000000..e83ed3166a --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6773.php @@ -0,0 +1,18 @@ += 8.1 + +namespace Bug6773; + +final class Repository +{ + /** + * @param array $data + */ + public function __construct(private readonly array $data) + { + } + + public function remove(string $key): void + { + unset($this->data[$key]); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6922.php b/tests/PHPStan/Rules/Properties/data/bug-6922.php new file mode 100644 index 0000000000..5e0a49eec2 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6922.php @@ -0,0 +1,25 @@ += 8.1 + +namespace Bug6922; + +class Person { + + public function __construct( + public readonly string $name, + public readonly bool $isDeveloper, + public readonly bool $isAdmin + ) { + + } +} + +class Proof +{ + public function test(?Person $person): void + { + if ($person?->isDeveloper === FALSE || + $person?->isAdmin === FALSE) { + echo "Bug"; + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7087.php b/tests/PHPStan/Rules/Properties/data/bug-7087.php new file mode 100644 index 0000000000..209bfa62e0 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7087.php @@ -0,0 +1,40 @@ += 8.1 + +namespace Bug7087; + +class Foo { + /** + * @var array, mixed> $array1 + */ + public readonly array $array1; + /** + * @var array, mixed> $array2 + */ + public readonly array $array2; + + /** + * @param array, mixed> $param + */ + public function __construct(array $param) { + $this->array1 = $this->foo($param); + $this->array2 = $this->bar($param); + } + + /** + * @param array, mixed> $param + * @return array, mixed> + */ + private function foo(array $param): array { + return $param; + } + + /** + * @template IKey + * @template IValue + * @param array $param + * @return array + */ + private function bar(array $param): array { + return $param; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7198.php b/tests/PHPStan/Rules/Properties/data/bug-7198.php new file mode 100644 index 0000000000..75d9ab0af5 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7198.php @@ -0,0 +1,88 @@ += 8.1 + +namespace Bug7198; + +trait TestTrait { + public function foo(): void + { + $this->callee->foo(); + } +} + +class TestCallee { + public function foo(): void + { + echo "FOO\n"; + } +} + +class TestCaller { + use TestTrait; + + public function __construct(private readonly TestCallee $callee) + { + $this->foo(); + } +} + +class TestCaller2 { + public function foo(): void + { + $this->callee->foo(); + } + + public function __construct(private readonly TestCallee $callee) + { + $this->foo(); + } +} + +class TestCaller3 { + + public function __construct(private readonly TestCallee $callee) + { + $this->foo(); + } + + public function foo(): void + { + $this->callee->foo(); + } +} + +trait Identifiable +{ + public readonly int $id; + + public function __construct() + { + $this->id = rand(); + } +} + +trait CreateAware +{ + public readonly \DateTimeImmutable $createdAt; + + public function __construct() + { + $this->createdAt = new \DateTimeImmutable(); + } +} + +abstract class Entity +{ + use Identifiable { + Identifiable::__construct as private __identifiableConstruct; + } + + use CreateAware { + CreateAware::__construct as private __createAwareConstruct; + } + + public function __construct() + { + $this->__identifiableConstruct(); + $this->__createAwareConstruct(); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7640.php b/tests/PHPStan/Rules/Properties/data/bug-7640.php new file mode 100644 index 0000000000..6321a23416 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7640.php @@ -0,0 +1,57 @@ += 8.0 + +namespace Bug7640; + +class C +{ +} + +class P +{ + private ?C $_connection = null; + + public function getConnection(): C + { + $this->_connection = new C(); + + return $this->_connection; + } + + public static function connect(): P + { + return new P(); + } + + public static function assertInstanceOf(object $object): static + { + if (!$object instanceof static) { + throw new \TypeError('Object is not an instance of static class'); + } + + return $object; + } +} + +abstract class TestCase +{ + protected function createPWithLazyConnect(): void + { + new class() extends P + { + public function __construct() + { + } + + public function getConnection(): C + { + \Closure::bind(function () { + if ($this->_connection === null) { + $connection = P::assertInstanceOf(P::connect())->_connection; + } + }, null, P::class)(); + + return parent::getConnection(); + } + }; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7649.php b/tests/PHPStan/Rules/Properties/data/bug-7649.php new file mode 100644 index 0000000000..b8f4ec0bfd --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7649.php @@ -0,0 +1,16 @@ += 8.1 + +namespace Bug7649; + +class Foo +{ + public readonly string $bar; + + public function __construct(bool $flag) + { + if ($flag) { + $this->bar = 'baz'; + } else { + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7692.php b/tests/PHPStan/Rules/Properties/data/bug-7692.php new file mode 100644 index 0000000000..c8fbe7a5a7 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7692.php @@ -0,0 +1,54 @@ + + */ + protected static $entityClass; + } +} + +namespace BaseNamespace7692\Entity { + + interface EntityBaseInterface + { + + } +} + +namespace DeepInheritingNamespace7692 { + + use InheritingNamespace7692\TheIntermediateService; + use DeepInheritingNamespace7692\Entity\TheEntity; + + final class TheChildService extends TheIntermediateService + { + protected static $entityClass = TheEntity::class; + } +} + +namespace DeepInheritingNamespace7692\Entity { + + use BaseNamespace7692\Entity\EntityBaseInterface; + + final class TheEntity implements EntityBaseInterface + { + + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-7789.php b/tests/PHPStan/Rules/Properties/data/bug-7789.php new file mode 100644 index 0000000000..6fdf12b477 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-7789.php @@ -0,0 +1,14 @@ += 8.0 + +namespace Bug8074; + +use ReflectionClass; +use ReflectionClassConstant; +use TypeError; +use UnexpectedValueException; + +/** + * @template K + * @template T + * @template L + * @template U + * + * @param iterable $stream + * @param callable(T, K): iterable $fn + * + * @return \Generator + */ +function scollect(iterable $stream, callable $fn): \Generator +{ + foreach ($stream as $key => $value) { + yield from $fn($value, $key); + } +} + +/** + * @template K of array-key + * @template T + * @template L of array-key + * @template U + * + * @param array $array + * @param callable(T, K): iterable $fn + * + * @return array + */ +function collectWithKeys(array $array, callable $fn): array +{ + $values = []; + $counter = 0; + + foreach (scollect($array, $fn) as $key => $value) { + try { + $values[$key] = $value; + } catch (TypeError $e) { + throw new UnexpectedValueException('The key yielded in the callable is not compatible with the type "array-key".'); + } + + ++$counter; + } + + if ($counter !== count($values)) { + throw new UnexpectedValueException( + 'Data loss occurred because of duplicated keys. Use `collect()` if you do not care about ' . + 'the yielded keys, or use `scollect()` if you need to support duplicated keys (as arrays cannot).' + ); + } + + return $values; +} + +function __(string $message, bool $capitalize = true): string +{ + // some fake translation function + return $capitalize ? ucfirst($message) : $message; +} + +final class CsvExport +{ + public const COLUMN_A = 'something'; + public const COLUMN_B = 'else'; + public const COLUMN_C = 'entirely'; + + /** + * @var array The translated header as value + */ + private static array $headers; + + /** + * @return array + */ + public static function getHeaders(): array + { + if (!isset(self::$headers)) { + /** Using [at]var array $headers here would fix the inspection */ + $headers = collectWithKeys( + (new ReflectionClass(self::class))->getReflectionConstants(), + static function (ReflectionClassConstant $constant): iterable { + /** @var self::COLUMN_* $value */ + $value = $constant->getValue(); + + yield $value => __(sprintf('activities.export.urbanus.csv_header.%s', $value), capitalize: false); + }, + ); + + self::$headers = $headers; + } + + return self::$headers; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-8190.php b/tests/PHPStan/Rules/Properties/data/bug-8190.php new file mode 100644 index 0000000000..81556c611c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8190.php @@ -0,0 +1,57 @@ += 7.4 + +namespace Bug8190; + +/** + * @phpstan-type OwnerBackup array{name: string, isTest?: bool} + */ +class ClassA +{ + /** @var OwnerBackup */ + public array $ownerBackup; + + /** + * @param OwnerBackup|null $ownerBackup + */ + public function __construct(?array $ownerBackup) + { + $this->ownerBackup = $ownerBackup ?? [ + 'name' => 'Deleted', + ]; + } + + + /** + * @param OwnerBackup|null $ownerBackup + */ + public function setOwnerBackup(?array $ownerBackup): void + { + $this->ownerBackup = $ownerBackup ?: [ + 'name' => 'Deleted', + ]; + } + + /** + * @param OwnerBackup|null $ownerBackup + */ + public function setOwnerBackupWorksForSomeReason(?array $ownerBackup): void + { + $this->ownerBackup = $ownerBackup !== null ? $ownerBackup : [ + 'name' => 'Deleted', + ]; + } + + /** + * @param OwnerBackup|null $ownerBackup + */ + public function setOwnerBackupAlsoWorksForSomeReason(?array $ownerBackup): void + { + if ($ownerBackup) { + $this->ownerBackup = $ownerBackup; + } else { + $this->ownerBackup = [ + 'name' => 'Deleted', + ]; + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-8222.php b/tests/PHPStan/Rules/Properties/data/bug-8222.php new file mode 100644 index 0000000000..93af72ca29 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8222.php @@ -0,0 +1,14 @@ += 7.4 + +namespace Bug8222; + +class ValueCollection +{ + /** @var array */ + public array $values; + + public function addValue(string $value): void + { + $this->values[] = $value; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-8333.php b/tests/PHPStan/Rules/Properties/data/bug-8333.php new file mode 100644 index 0000000000..ee2395a389 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8333.php @@ -0,0 +1,75 @@ += 8.1 + +namespace Bug8412; + +use InvalidArgumentException; + +enum Zustand: string +{ + case Failed = 'failed'; + case Pending = 'pending'; +} + +final class HelloWorld +{ + public readonly ?int $value; + + public function __construct(Zustand $zustand) + { + $this->value = match ($zustand) { + Zustand::Failed => 1, + Zustand::Pending => 2, + default => throw new InvalidArgumentException('Unknown Zustand: ' . $zustand->value), + }; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-8563.php b/tests/PHPStan/Rules/Properties/data/bug-8563.php new file mode 100644 index 0000000000..bfd0f75b07 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8563.php @@ -0,0 +1,16 @@ += 8.1 + +namespace Bug8563; + +class BankAccount { + + readonly string $bic; + readonly string $iban; + readonly string $label; + + function __construct(object $data = new \stdClass) { + $this->bic = $data->bic ?? ""; + $this->iban = $data->iban ?? ""; + $this->label = $data->label ?? ""; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-8629.php b/tests/PHPStan/Rules/Properties/data/bug-8629.php new file mode 100644 index 0000000000..b8fc89ee21 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8629.php @@ -0,0 +1,10 @@ +nodeType); +}; diff --git a/tests/PHPStan/Rules/Properties/data/bug-8958.php b/tests/PHPStan/Rules/Properties/data/bug-8958.php new file mode 100644 index 0000000000..21b375bd53 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8958.php @@ -0,0 +1,69 @@ += 8.1 + +namespace Bug8958; + +interface TimeRangeInterface +{ + public function getStart(): \DateTimeInterface; + + public function getEnd(): \DateTimeInterface; +} + +trait TimeRangeTrait +{ + private readonly \DateTimeImmutable $start; + + private readonly \DateTimeImmutable $end; + + public function getStart(): \DateTimeImmutable + { + return $this->start; // @phpstan-ignore-line + } + + public function getEnd(): \DateTimeImmutable + { + return $this->end; // @phpstan-ignore-line + } + + private function initTimeRange( + \DateTimeInterface $start, + \DateTimeInterface $end + ): void { + $this->start = \DateTimeImmutable::createFromInterface($start); // @phpstan-ignore-line + $this->end = \DateTimeImmutable::createFromInterface($end); // @phpstan-ignore-line + } +} + +class Foo implements TimeRangeInterface { + use TimeRangeTrait; + + public function __construct(\DateTimeInterface $start, \DateTimeInterface $end) + { + $this->initTimeRange($start, $end); + } +} + +class Bar implements TimeRangeInterface { + use TimeRangeTrait; + + public function __construct( + private TimeRangeInterface $first, + private TimeRangeInterface $second, + ?\DateTimeInterface $start = null, + \DateTimeInterface $end = null + ) { + $this->initTimeRange( + $start ?? max($first->getStart(), $second->getStart()), + $end ?? min($first->getEnd(), $second->getEnd()), + ); + } + + public function getFirst(): TimeRangeInterface + { + return $this->first; + } + public function getSecond(): TimeRangeInterface + { + return $this->second; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-9131.php b/tests/PHPStan/Rules/Properties/data/bug-9131.php new file mode 100644 index 0000000000..073e27a0d7 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-9131.php @@ -0,0 +1,13 @@ += 7.4 + +namespace Bug9131; + +class A +{ + /** @var array, string> */ + public array $l = []; + + public function add(string $s): void { + $this->l[] = $s; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-9619.php b/tests/PHPStan/Rules/Properties/data/bug-9619.php new file mode 100644 index 0000000000..191eb3e589 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-9619.php @@ -0,0 +1,59 @@ += 7.4 + +namespace Bug9619; + +interface User +{ + + public function isLoggedIn(): bool; + +} + +class AdminPresenter +{ + /** @inject */ + public User $user; + + public function startup() + { + if (!$this->user->isLoggedIn()) { + // do something + } + } +} + +class AdminPresenter2 +{ + private User $user; + + public function __construct(User $user) + { + $this->user = $user; + } + + public function startup() + { + // do not report uninitialized property - it's initialized for sure + if (!$this->user->isLoggedIn()) { + // do something + } + } +} + +class AdminPresenter3 +{ + private \stdClass $user; + + public function startup() + { + $this->user = new \stdClass(); + } + + public function startup2() + { + // we cannot be sure which additional constructor gets called first + if (!$this->user->loggedIn) { + // do something + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-9831.php b/tests/PHPStan/Rules/Properties/data/bug-9831.php new file mode 100644 index 0000000000..da1161be77 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-9831.php @@ -0,0 +1,21 @@ += 8.1 + +namespace Bug9831; + +class Foo +{ + private string $bar; + + public function __construct() + { + $var = function (): void { + echo $this->bar; + }; + + $this->bar = '123'; + + $var = function (): void { + echo $this->bar; + }; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/conflicting-annotation-property.php b/tests/PHPStan/Rules/Properties/data/conflicting-annotation-property.php new file mode 100644 index 0000000000..9fd7174acb --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/conflicting-annotation-property.php @@ -0,0 +1,28 @@ +test = 1; + } + + public function doFoo2() + { + echo $this->test; + } + +} + +function (PropertyWithAnnotation $p): void { + echo $p->test; + $p->test = 1; +}; diff --git a/tests/PHPStan/Rules/Properties/data/dynamic-properties.php b/tests/PHPStan/Rules/Properties/data/dynamic-properties.php index 0a90b9ad5d..055029152e 100644 --- a/tests/PHPStan/Rules/Properties/data/dynamic-properties.php +++ b/tests/PHPStan/Rules/Properties/data/dynamic-properties.php @@ -29,3 +29,17 @@ public function doBar() { } } +final class FinalBar {} + +final class FinalFoo { + public function doBar() { + isset($this->dynamicProperty); + empty($this->dynamicProperty); + $this->dynamicProperty ?? 'test'; + + $bar = new FinalBar(); + isset($bar->dynamicProperty); + empty($bar->dynamicProperty); + $bar->dynamicProperty ?? 'test'; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/efabrica-latte-bug.php b/tests/PHPStan/Rules/Properties/data/efabrica-latte-bug.php new file mode 100644 index 0000000000..ce3c3226e2 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/efabrica-latte-bug.php @@ -0,0 +1,100 @@ += 7.4 + +namespace EfabricaLatteBug; + +use Nette\Utils\Finder; +use PHPStan\File\FileExcluder; +use SplFileInfo; + +final class AnalysedTemplatesRegistry +{ + private FileExcluder $fileExcluder; + + /** @var string[] */ + private array $analysedPaths = []; + + private bool $reportUnanalysedTemplates; + + /** @var array */ + private array $templateFiles = []; + + /** + * @param string[] $analysedPaths + */ + public function __construct(FileExcluder $fileExcluder, array $analysedPaths, bool $reportUnanalysedTemplates) + { + $this->fileExcluder = $fileExcluder; + $this->analysedPaths = $analysedPaths; + $this->reportUnanalysedTemplates = $reportUnanalysedTemplates; + foreach ($this->getExistingTemplates() as $file) { + $this->templateFiles[$file] = false; + } + } + + public function isExcludedFromAnalysing(string $path): bool + { + return $this->fileExcluder->isExcludedFromAnalysing($path); + } + + public function templateAnalysed(string $path): void + { + $path = realpath($path) ?: $path; + $this->templateFiles[$path] = true; + } + + /** + * @return string[] + */ + public function getExistingTemplates(): array + { + $files = []; + foreach ($this->analysedPaths as $analysedPath) { + if (!is_dir($analysedPath)) { + continue; + } + /** @var SplFileInfo $file */ + foreach (Finder::findFiles('*.latte')->from($analysedPath) as $file) { + $filePath = (string)$file; + if ($this->isExcludedFromAnalysing($filePath)) { + continue; + } + $files[] = $filePath; + } + } + $files = array_unique($files); + sort($files); + return $files; + } + + /** + * @return string[] + */ + public function getAnalysedTemplates(): array + { + return array_keys(array_filter($this->templateFiles, function (bool $val) { + return $val; + })); + } + + /** + * @return string[] + */ + public function getUnanalysedTemplates(): array + { + return array_keys(array_filter($this->templateFiles, function (bool $val) { + return !$val; + })); + } + + /** + * @return string[] + */ + public function getReportedUnanalysedTemplates(): array + { + if ($this->reportUnanalysedTemplates) { + return $this->getUnanalysedTemplates(); + } else { + return []; + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/invalid-callable-property-type.php b/tests/PHPStan/Rules/Properties/data/invalid-callable-property-type.php new file mode 100644 index 0000000000..053c0bfb1c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/invalid-callable-property-type.php @@ -0,0 +1,31 @@ +a = $closure; + $this->b = $closure; + $this->c = $closure; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php b/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php index 79cba0ab31..952016e860 100644 --- a/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php +++ b/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php @@ -42,6 +42,9 @@ class PrefixedTags /** @psalm-var int */ private $fooPsalm; + /** @phan-var int */ + private $fooPhan; + } /** @@ -93,3 +96,13 @@ class CallableSignature private $cb; } + +class NestedArrayInProperty +{ + + /** + * @var list|null + */ + public $args; + +} diff --git a/tests/PHPStan/Rules/Properties/data/missing-readonly-anonymous-class-property-assign.php b/tests/PHPStan/Rules/Properties/data/missing-readonly-anonymous-class-property-assign.php new file mode 100644 index 0000000000..3c5a6bfccf --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/missing-readonly-anonymous-class-property-assign.php @@ -0,0 +1,15 @@ += 8.3 + +namespace MissingReadonlyAnonymousClassPropertyAssign; + +class Foo +{ + + public function doFoo(): void + { + $c = new readonly class () { + public int $foo; + }; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign.php b/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign.php index bb474ae9bc..cded620d17 100644 --- a/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign.php +++ b/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign.php @@ -179,3 +179,124 @@ class BarClass use BarTrait; } + +class AdditionalAssignOfReadonlyPromotedProperty +{ + + public function __construct(private readonly int $x) + { + $this->x = 2; + } + +} + +class MethodCalledFromConstructorAfterAssign +{ + + + private readonly int $foo; + + public function __construct() + { + $this->foo = 1; + $this->doFoo(); + } + + public function doFoo(): void + { + echo $this->foo; + } + +} + +class MethodCalledFromConstructorBeforeAssign +{ + + + private readonly int $foo; + + public function __construct() + { + $this->doFoo(); + $this->foo = 1; + } + + public function doFoo(): void + { + echo $this->foo; + } + +} + +class MethodCalledTwice +{ + private readonly int $foo; + + public function __construct() + { + $this->doFoo(); + $this->foo = 1; + $this->doFoo(); + } + + public function doFoo(): void + { + echo $this->foo; + } +} + +class PropertyAssignedOnDifferentObject +{ + + private readonly int $foo; + + public function __construct(self $self) + { + $self->foo = 1; + $this->foo = 2; + } + +} + +class PropertyAssignedOnDifferentObjectUninitialized +{ + + private readonly int $foo; + + public function __construct(self $self) + { + $self->foo = 1; + } + +} + +class AccessToPropertyOnDifferentObject +{ + + private readonly int $foo; + + public function __construct(self $self) + { + echo $self->getFoo(); + $this->foo = 1; + } + + public function getFoo(): int + { + return $this->foo; + } + +} + +class PropertyHasInitPhpDocButIsAlsoAssignedInConstructor +{ + + /** @init */ + public readonly int $foo; + + public function __construct(int $foo) + { + $this->foo = $foo; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/nullsafe-property-fetch.php b/tests/PHPStan/Rules/Properties/data/nullsafe-property-fetch.php index 9c6bbb66c4..d2a693fe64 100644 --- a/tests/PHPStan/Rules/Properties/data/nullsafe-property-fetch.php +++ b/tests/PHPStan/Rules/Properties/data/nullsafe-property-fetch.php @@ -22,4 +22,11 @@ public function doBar(string $string, ?string $nullableString): void echo $nullableString?->bar ?? 4; } + public function doNull(): void + { + $null = null; + $null->foo; + $null?->foo; + } + } diff --git a/tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties.php b/tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties.php index dc31d5c3be..ddd7d965a6 100644 --- a/tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties.php +++ b/tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties.php @@ -73,3 +73,37 @@ function (): void { echo $hello->world; } }; + +final class FinalHelloWorld +{ + public function __get(string $attribute): mixed + { + if($attribute == "world") + { + return "Hello World"; + } + throw new \Exception("Attribute '{$attribute}' is invalid"); + } + + + public function __isset(string $attribute) + { + try { + if (!isset($this->{$attribute})) { + $x = $this->{$attribute}; + } + + return isset($this->{$attribute}); + } catch (\Exception $e) { + return false; + } + } +} + +function (): void { + $hello = new FinalHelloWorld(); + if(isset($hello->world)) + { + echo $hello->world; + } +}; diff --git a/tests/PHPStan/Rules/Properties/data/properties-in-interface.php b/tests/PHPStan/Rules/Properties/data/properties-in-interface.php new file mode 100644 index 0000000000..4e897d1998 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/properties-in-interface.php @@ -0,0 +1,10 @@ +foo; + echo $o->bar; + echo $o->baz; + + $o->foo = 1; + $o->bar = 2; + $o->baz = 3; + } + + /** + * @param object{foo: int}&\stdClass $o + * @return void + */ + public function doIntersection(object $o): void + { + echo $o->foo; + + $o->foo = 1; + } + + /** + * @param object{foo: int}|\stdClass $o + * @return void + */ + public function doUnion(object $o): void + { + echo $o->foo; + + $o->foo = 1; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/property-attributes-deprecated.php b/tests/PHPStan/Rules/Properties/data/property-attributes-deprecated.php new file mode 100644 index 0000000000..1492d7809d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-attributes-deprecated.php @@ -0,0 +1,29 @@ + */ + private $nonEmpty; + + /** @var list */ + private $listProp; + + /** @var array> */ + private $nestedListProp; + + public function doFoo(int $i, int $j) + { + unset($this->nonEmpty[$i]); + unset($this->listProp[$i]); + unset($this->nestedListProp[$i][$j]); + } + +} + +class Bar +{ + + /** @var array> */ + private $prop; + + /** + * @param int|string $key + */ + public function doFoo($key): void + { + unset($this->prop[$key]['foo']); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-allowed-private-mutation.php b/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-allowed-private-mutation.php new file mode 100644 index 0000000000..435a73706b --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc-allowed-private-mutation.php @@ -0,0 +1,30 @@ += 8.0 + +namespace ReadOnlyPropertyPhpDocAllowedPrivateMutation; + +class A +{ + + /** @phpstan-readonly */ + public array $a = []; + +} + +class B +{ + + /** + * @phpstan-readonly + * @phpstan-allow-private-mutation + */ + public array $a = []; + +} + +class C +{ + + /** @phpstan-readonly-allow-private-mutation */ + public array $a = []; + +} diff --git a/tests/PHPStan/Rules/Properties/data/reading-write-only-properties-nullsafe.php b/tests/PHPStan/Rules/Properties/data/reading-write-only-properties-nullsafe.php index 33051d7ead..2042e0005f 100644 --- a/tests/PHPStan/Rules/Properties/data/reading-write-only-properties-nullsafe.php +++ b/tests/PHPStan/Rules/Properties/data/reading-write-only-properties-nullsafe.php @@ -6,5 +6,6 @@ function (?Foo $foo): void { echo $foo?->readOnlyProperty; echo $foo?->usualProperty; + echo $foo?->asymmetricProperty; echo $foo?->writeOnlyProperty; }; diff --git a/tests/PHPStan/Rules/Properties/data/reading-write-only-properties.php b/tests/PHPStan/Rules/Properties/data/reading-write-only-properties.php index 38656a0d66..cc2b202f82 100644 --- a/tests/PHPStan/Rules/Properties/data/reading-write-only-properties.php +++ b/tests/PHPStan/Rules/Properties/data/reading-write-only-properties.php @@ -7,6 +7,8 @@ /** * @property-read int $readOnlyProperty * @property int $usualProperty + * @property-read int $asymmetricProperty + * @property-write int|string $asymmetricProperty * @property-write int $writeOnlyProperty */ #[AllowDynamicProperties] @@ -17,11 +19,13 @@ public function doFoo() { echo $this->readOnlyProperty; echo $this->usualProperty; + echo $this->asymmetricProperty; echo $this->writeOnlyProperty; $self = new self(); echo $self->readOnlyProperty; echo $self->usualProperty; + echo $self->asymmetricProperty; echo $self->writeOnlyProperty; } diff --git a/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php b/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php index 4d8c32463f..55af82fe30 100644 --- a/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php +++ b/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php @@ -29,16 +29,24 @@ class Foo */ public $psalm; + /** + * @var int + * @phan-read-only + */ + public $phan; + public function __construct(int $foo) { $this->foo = $foo; // constructor - fine $this->psalm = $foo; // constructor - fine + $this->phan = $foo; // constructor - fine } public function setFoo(int $foo): void { $this->foo = $foo; // setter - report $this->psalm = $foo; // do not report -allowed private mutation + $this->phan = $foo; // setter - report } } @@ -53,12 +61,14 @@ public function __construct(int $bar) $this->bar = $bar; // report - not in declaring class $this->baz = $baz; // report - not in declaring class $this->psalm = $bar; // report - not in declaring class + $this->phan = $bar; // report - not in declaring class } public function setBar(int $bar): void { $this->bar = $bar; // report - not in declaring class $this->psalm = $bar; // report - not in declaring class + $this->phan = $bar; // report - not in declaring class } } diff --git a/tests/PHPStan/Rules/Properties/data/require-extends.php b/tests/PHPStan/Rules/Properties/data/require-extends.php new file mode 100644 index 0000000000..14271b4ef6 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/require-extends.php @@ -0,0 +1,47 @@ += 7.4 + +namespace RequireExtends; + +/** + * Implementors are expected to use MyTrait. + * + * A base implementation is provided by MyBaseClass. + * + * @phpstan-require-extends MyBaseClass + */ +interface MyInterface +{ +} + +trait MyTrait +{ + public string $foo = 'hello'; +} + +abstract class MyBaseClass implements MyInterface +{ + use MyTrait; + + public function doSomething(): string { + return 'hallo'; + } + + static public function doSomethingStatic(): int { + return 123; + } +} + +function getFoo(MyInterface $obj): string +{ + echo $obj->bar; + return $obj->foo; +} + + +function callFoo(MyInterface $obj): string +{ + echo $obj->doesNotExist(); + echo MyInterface::doesNotExistStatic(); + echo MyInterface::doSomethingStatic(); + return $obj->doSomething(); +} diff --git a/tests/PHPStan/Rules/Properties/data/require-implements.php b/tests/PHPStan/Rules/Properties/data/require-implements.php new file mode 100644 index 0000000000..07ed308757 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/require-implements.php @@ -0,0 +1,48 @@ += 7.4 + +namespace RequireImplements; + +interface MyInterface +{ + public function doSomething(): string; + + static public function doSomethingStatic(): int; +} + +/** + * @phpstan-require-implements MyInterface + */ +trait MyTrait +{ + public string $foo = 'hello'; +} + +abstract class MyBaseClass implements MyInterface +{ + use MyTrait; + public string $bar = 'world'; + + public function doSomething(): string + { + return 'foo'; + } + + static public function doSomethingStatic(): int + { + return 1; + } +} + +function getFoo(MyBaseClass $obj): string +{ + echo $obj->bar; + return $obj->foo; +} + +function callFoo(MyBaseClass $obj): string +{ + echo $obj->doesNotExist(); + echo $obj::doesNotExistStatic(); + echo $obj::doSomethingStatic(); + return $obj->doSomething(); +} diff --git a/tests/PHPStan/Rules/Properties/data/uninitialized-property-additional-constructors.php b/tests/PHPStan/Rules/Properties/data/uninitialized-property-additional-constructors.php new file mode 100644 index 0000000000..7e3cda91a5 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/uninitialized-property-additional-constructors.php @@ -0,0 +1,22 @@ += 7.4 + +namespace TestInitializedProperty; + +class TestAdditionalConstructor +{ + public string $one; + + protected int $two; + + protected int $three; + + public function setTwo(int $value): void + { + $this->two = $value; + } + + public function setThree(int $value): void + { + $this->three = $value; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/uninitialized-property.php b/tests/PHPStan/Rules/Properties/data/uninitialized-property.php index 685efa13d4..47bc02df86 100644 --- a/tests/PHPStan/Rules/Properties/data/uninitialized-property.php +++ b/tests/PHPStan/Rules/Properties/data/uninitialized-property.php @@ -176,3 +176,297 @@ public function __construct() } } + +class ItemsCrate +{ + + /** + * @var int[] + */ + private array $items; + + /** + * @param int[] $items + */ + public function __construct( + array $items + ) + { + $this->items = $items; + $this->sortItems(); + } + + private function sortItems(): void + { + usort($this->items, static function ($a, $b): int { + return $a <=> $b; + }); + } + + public function addItem(int $i): void + { + $this->items[] = $i; + $this->sortItems(); + } + +} + +class InitializedInPrivateSetter +{ + + private int $foo; + + public function __construct() + { + $this->setFoo(); + $this->doSomething(); + } + + private function setFoo() + { + $this->foo = 1; + } + + public function doSomething() + { + echo $this->foo; + } + +} + +final class InitializedInPublicSetterFinalClass +{ + + private int $foo; + + public function __construct() + { + $this->setFoo(); + $this->doSomething(); + } + + public function setFoo() + { + $this->foo = 1; + } + + public function doSomething() + { + echo $this->foo; + } + +} + +class InitializedInPublicSetterNonFinalClass +{ + + private int $foo; + + public function __construct() + { + $this->setFoo(); + $this->doSomething(); + } + + public function setFoo() + { + $this->foo = 1; + } + + public function doSomething() + { + echo $this->foo; + } + +} + +class SometimesInitializedInPrivateSetter +{ + + private int $foo; + + public function __construct() + { + $this->setFoo(); + $this->doSomething(); + } + + private function setFoo() + { + if (rand(0, 1)) { + $this->foo = 1; + } + } + + public function doSomething() + { + echo $this->foo; + } + +} + +class ConfuseNodeScopeResolverWithAnonymousClass +{ + + private int $foo; + + public function __construct() + { + $this->setFoo(); + $this->doSomething(); + } + + private function setFoo() + { + $c = new class () { + public function setFoo() + { + } + }; + $this->foo = 1; + } + + public function doSomething() + { + echo $this->foo; + } + +} + +class ThrowInConstructor1 +{ + + private int $foo; + + public function __construct() + { + if (rand(0, 1)) { + $this->foo = 1; + return; + } + + throw new \Exception; + } + +} + +class ThrowInConstructor2 +{ + + private int $foo; + + public function __construct() + { + if (rand(0, 1)) { + throw new \Exception; + } + + $this->foo = 1; + } + +} + +class EarlyReturn +{ + + private int $foo; + + public function __construct() + { + if (rand(0, 1)) { + return; + } + + $this->foo = 1; + } + +} + +class NeverInConstructor +{ + + private int $foo; + + public function __construct() + { + if (rand(0, 1)) { + $this->foo = 1; + return; + } + + $this->returnNever(); + } + + /** + * @return never + */ + private function returnNever() + { + throw new \Exception(); + } + +} + +class InitializedInPrivateSetterWithThrow +{ + + private int $foo; + + public function __construct() + { + $this->setFoo(); + $this->doSomething(); + } + + private function setFoo() + { + if (rand(0, 1)) { + $this->foo = 1; + return; + } + + throw new \Exception(); + } + + public function doSomething() + { + echo $this->foo; + } + +} + +class InitializedInPrivateSetterWithReturnNever +{ + + private int $foo; + + public function __construct() + { + $this->setFoo(); + $this->doSomething(); + } + + private function setFoo() + { + if (rand(0, 1)) { + $this->foo = 1; + return; + } + + $this->returnNever(); + } + + public function doSomething() + { + echo $this->foo; + } + + /** + * @return never + */ + private function returnNever() + { + throw new \Exception(); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/virtual-nullsafe-property-fetch.php b/tests/PHPStan/Rules/Properties/data/virtual-nullsafe-property-fetch.php new file mode 100644 index 0000000000..a06b89d8b1 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/virtual-nullsafe-property-fetch.php @@ -0,0 +1,4 @@ += 8.0 + +$foo->regularFetch; +$foo?->nullsafeFetch; diff --git a/tests/PHPStan/Rules/Properties/data/writing-to-read-only-properties.php b/tests/PHPStan/Rules/Properties/data/writing-to-read-only-properties.php index 23cc8fcb14..2de103f0b5 100644 --- a/tests/PHPStan/Rules/Properties/data/writing-to-read-only-properties.php +++ b/tests/PHPStan/Rules/Properties/data/writing-to-read-only-properties.php @@ -7,6 +7,8 @@ /** * @property-read int $readOnlyProperty * @property int $usualProperty + * @property-read int $asymmetricProperty + * @property-write int|string $asymmetricProperty * @property-write int $writeOnlyProperty */ #[AllowDynamicProperties] @@ -31,6 +33,9 @@ public function doFoo() $self->usualProperty = 1; $self->usualProperty .= 1; + $self->asymmetricProperty = "1"; + $self->asymmetricProperty = 1; + $self->writeOnlyProperty = 1; $self->writeOnlyProperty .= 1; @@ -38,4 +43,9 @@ public function doFoo() $self->readOnlyProperty = &$s; } + public function doObjectShape() + { + + } + } diff --git a/tests/PHPStan/Rules/Properties/uninitialized-property-rule.neon b/tests/PHPStan/Rules/Properties/uninitialized-property-rule.neon new file mode 100644 index 0000000000..6362843ec8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/uninitialized-property-rule.neon @@ -0,0 +1,5 @@ +services: + - + class: PHPStan\Rules\Properties\TestInitializedProperty + tags: + - phpstan.additionalConstructorsExtension diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php new file mode 100644 index 0000000000..54a6facc7a --- /dev/null +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -0,0 +1,156 @@ + + */ +class PureFunctionRuleTest extends RuleTestCase +{ + + public function getRule(): Rule + { + return new PureFunctionRule(new FunctionPurityCheck()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/pure-function.php'], [ + [ + 'Function PureFunction\doFoo() is marked as pure but parameter $p is passed by reference.', + 8, + ], + [ + 'Impure echo in pure function PureFunction\doFoo().', + 10, + ], + [ + 'Function PureFunction\doFoo2() is marked as pure but returns void.', + 16, + ], + [ + 'Impure exit in pure function PureFunction\doFoo2().', + 18, + ], + [ + 'Impure property assignment in pure function PureFunction\doFoo3().', + 26, + ], + [ + 'Possibly impure call to a callable in pure function PureFunction\testThese().', + 60, + ], + [ + 'Possibly impure call to a callable in pure function PureFunction\testThese().', + 61, + ], + [ + 'Impure call to function PureFunction\impureFunction() in pure function PureFunction\testThese().', + 63, + ], + [ + 'Impure call to function PureFunction\voidFunction() in pure function PureFunction\testThese().', + 64, + ], + [ + 'Possibly impure call to function PureFunction\possiblyImpureFunction() in pure function PureFunction\testThese().', + 65, + ], + [ + 'Possibly impure call to unknown function in pure function PureFunction\testThese().', + 66, + ], + [ + 'Function PureFunction\actuallyPure() is marked as impure but does not have any side effects.', + 72, + ], + [ + 'Function PureFunction\emptyVoidFunction() returns void but does not have any side effects.', + 84, + ], + [ + 'Impure access to superglobal variable in pure function PureFunction\pureButAccessSuperGlobal().', + 102, + ], + [ + 'Impure access to superglobal variable in pure function PureFunction\pureButAccessSuperGlobal().', + 103, + ], + [ + 'Impure access to superglobal variable in pure function PureFunction\pureButAccessSuperGlobal().', + 105, + ], + [ + 'Impure global variable in pure function PureFunction\functionWithGlobal().', + 118, + ], + [ + 'Impure static variable in pure function PureFunction\functionWithStaticVariable().', + 128, + ], + [ + 'Possibly impure call to a Closure in pure function PureFunction\callsClosures().', + 139, + ], + [ + 'Possibly impure call to a Closure in pure function PureFunction\callsClosures().', + 140, + ], + ]); + } + + public function testFirstClassCallable(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/first-class-callable-pure-function.php'], [ + [ + 'Impure call to method FirstClassCallablePureFunction\Foo::impureFunction() in pure function FirstClassCallablePureFunction\testThese().', + 61, + ], + [ + 'Impure call to method FirstClassCallablePureFunction\Foo::voidFunction() in pure function FirstClassCallablePureFunction\testThese().', + 64, + ], + [ + 'Impure call to function FirstClassCallablePureFunction\impureFunction() in pure function FirstClassCallablePureFunction\testThese().', + 70, + ], + [ + 'Impure call to function FirstClassCallablePureFunction\voidFunction() in pure function FirstClassCallablePureFunction\testThese().', + 73, + ], + [ + 'Impure call to function FirstClassCallablePureFunction\voidFunction() in pure function FirstClassCallablePureFunction\testThese().', + 75, + ], + [ + 'Impure call to function FirstClassCallablePureFunction\impureFunction() in pure function FirstClassCallablePureFunction\testThese().', + 81, + ], + [ + 'Impure call to function FirstClassCallablePureFunction\voidFunction() in pure function FirstClassCallablePureFunction\testThese().', + 84, + ], + [ + 'Impure call to method FirstClassCallablePureFunction\Foo::impureFunction() in pure function FirstClassCallablePureFunction\testThese().', + 90, + ], + [ + 'Impure call to method FirstClassCallablePureFunction\Foo::voidFunction() in pure function FirstClassCallablePureFunction\testThese().', + 93, + ], + [ + 'Possibly impure call to a callable in pure function FirstClassCallablePureFunction\callCallbackImmediately().', + 102, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php new file mode 100644 index 0000000000..07141e4ad7 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php @@ -0,0 +1,160 @@ + + */ +class PureMethodRuleTest extends RuleTestCase +{ + + public function getRule(): Rule + { + return new PureMethodRule(new FunctionPurityCheck()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/pure-method.php'], [ + [ + 'Method PureMethod\Foo::doFoo() is marked as pure but parameter $p is passed by reference.', + 11, + ], + [ + 'Impure echo in pure method PureMethod\Foo::doFoo().', + 13, + ], + [ + 'Method PureMethod\Foo::doFoo2() is marked as pure but returns void.', + 19, + ], + [ + 'Impure die in pure method PureMethod\Foo::doFoo2().', + 21, + ], + [ + 'Impure property assignment in pure method PureMethod\Foo::doFoo3().', + 29, + ], + [ + 'Impure call to method PureMethod\Foo::voidMethod() in pure method PureMethod\Foo::doFoo4().', + 71, + ], + [ + 'Impure call to method PureMethod\Foo::impureVoidMethod() in pure method PureMethod\Foo::doFoo4().', + 72, + ], + [ + 'Possibly impure call to method PureMethod\Foo::returningMethod() in pure method PureMethod\Foo::doFoo4().', + 73, + ], + [ + 'Impure call to method PureMethod\Foo::impureReturningMethod() in pure method PureMethod\Foo::doFoo4().', + 75, + ], + [ + 'Possibly impure call to unknown method in pure method PureMethod\Foo::doFoo4().', + 76, + ], + [ + 'Impure call to method PureMethod\Foo::voidMethod() in pure method PureMethod\Foo::doFoo5().', + 84, + ], + [ + 'Impure call to method PureMethod\Foo::impureVoidMethod() in pure method PureMethod\Foo::doFoo5().', + 85, + ], + [ + 'Possibly impure call to method PureMethod\Foo::returningMethod() in pure method PureMethod\Foo::doFoo5().', + 86, + ], + [ + 'Impure call to method PureMethod\Foo::impureReturningMethod() in pure method PureMethod\Foo::doFoo5().', + 88, + ], + [ + 'Possibly impure call to unknown method in pure method PureMethod\Foo::doFoo5().', + 89, + ], + [ + 'Impure instantiation of class PureMethod\ImpureConstructor in pure method PureMethod\TestConstructors::doFoo().', + 140, + ], + [ + 'Possibly impure instantiation of class PureMethod\PossiblyImpureConstructor in pure method PureMethod\TestConstructors::doFoo().', + 141, + ], + [ + 'Possibly impure instantiation of unknown class in pure method PureMethod\TestConstructors::doFoo().', + 142, + ], + [ + 'Method PureMethod\ActuallyPure::doFoo() is marked as impure but does not have any side effects.', + 153, + ], + [ + 'Impure echo in pure method PureMethod\ExtendingClass::pure().', + 183, + ], + [ + 'Method PureMethod\ExtendingClass::impure() is marked as impure but does not have any side effects.', + 187, + ], + [ + 'Method PureMethod\ClassWithVoidMethods::privateEmptyVoidFunction() returns void but does not have any side effects.', + 214, + ], + [ + 'Impure assign to superglobal variable in pure method PureMethod\ClassWithVoidMethods::purePostGetAssign().', + 230, + ], + [ + 'Impure assign to superglobal variable in pure method PureMethod\ClassWithVoidMethods::purePostGetAssign().', + 231, + ], + [ + 'Possibly impure call to method PureMethod\MaybePureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doFoo().', + 295, + ], + [ + 'Impure call to method PureMethod\ImpureMagicMethods::__toString() in pure method PureMethod\TestMagicMethods::doFoo().', + 296, + ], + [ + 'Possibly impure call to a callable in pure method PureMethod\MaybeCallableFromUnion::doFoo().', + 330, + ], + [ + 'Possibly impure call to a callable in pure method PureMethod\MaybeCallableFromUnion::doFoo().', + 330, + ], + ]); + } + + public function testPureConstructor(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/pure-constructor.php'], [ + [ + 'Impure property assignment in pure method PureConstructor\Foo::__construct().', + 19, + ], + [ + 'Method PureConstructor\Bar::__construct() is marked as impure but does not have any side effects.', + 30, + ], + [ + 'Impure property assignment in pure method PureConstructor\AssignOtherThanThis::__construct().', + 49, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Pure/data/first-class-callable-pure-function.php b/tests/PHPStan/Rules/Pure/data/first-class-callable-pure-function.php new file mode 100644 index 0000000000..0d6e46af12 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/first-class-callable-pure-function.php @@ -0,0 +1,103 @@ += 8.1 + +namespace FirstClassCallablePureFunction; + +class Foo +{ + + /** + * @phpstan-pure + */ + function pureFunction() + { + + } + + /** + * @phpstan-impure + */ + function impureFunction() + { + echo ''; + } + + function voidFunction(): void + { + echo 'test'; + } + +} + +/** + * @phpstan-pure + */ +function pureFunction() +{ + +} + +/** + * @phpstan-impure + */ +function impureFunction() +{ + echo ''; +} + +function voidFunction(): void +{ + echo 'test'; +} + +/** + * @phpstan-pure + */ +function testThese(Foo $foo) +{ + $cb = $foo->pureFunction(...); + $cb(); + + $cb = $foo->impureFunction(...); + $cb(); + + $cb = $foo->voidFunction(...); + $cb(); + + $cb = pureFunction(...); + $cb(); + + $cb = impureFunction(...); + $cb(); + + $cb = voidFunction(...); + $cb(); + + callCallbackImmediately($cb); + + $cb = 'FirstClassCallablePureFunction\\pureFunction'; + $cb(); + + $cb = 'FirstClassCallablePureFunction\\impureFunction'; + $cb(); + + $cb = 'FirstClassCallablePureFunction\\voidFunction'; + $cb(); + + $cb = [$foo, 'pureFunction']; + $cb(); + + $cb = [$foo, 'impureFunction']; + $cb(); + + $cb = [$foo, 'voidFunction']; + $cb(); +} + +/** + * @phpstan-pure + * @return int + */ +function callCallbackImmediately(callable $cb): int +{ + return $cb(); +} diff --git a/tests/PHPStan/Rules/Pure/data/pure-constructor.php b/tests/PHPStan/Rules/Pure/data/pure-constructor.php new file mode 100644 index 0000000000..71045fd3ed --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/pure-constructor.php @@ -0,0 +1,51 @@ += 8.0 + +namespace PureConstructor; + +class Foo +{ + + private string $prop; + + public static $staticProp = 1; + + /** @phpstan-pure */ + public function __construct( + public int $test, + string $prop, + ) + { + $this->prop = $prop; + self::$staticProp++; + } + +} + +class Bar +{ + + private string $prop; + + /** @phpstan-impure */ + public function __construct( + public int $test, + string $prop, + ) + { + $this->prop = $prop; + } + +} + +class AssignOtherThanThis +{ + private int $i = 0; + + /** @phpstan-pure */ + public function __construct( + self $other, + ) + { + $other->i = 1; + } +} diff --git a/tests/PHPStan/Rules/Pure/data/pure-function.php b/tests/PHPStan/Rules/Pure/data/pure-function.php new file mode 100644 index 0000000000..2389ec9e55 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/pure-function.php @@ -0,0 +1,153 @@ +foo = 'test'; +} + +/** + * @phpstan-pure + */ +function pureFunction() +{ + +} + +/** + * @phpstan-impure + */ +function impureFunction() +{ + echo ''; +} + +function voidFunction(): void +{ + echo 'test'; +} + +function possiblyImpureFunction() +{ + +} + +/** + * @phpstan-pure + */ +function testThese(string $s, callable $cb) +{ + $s(); + $cb(); + pureFunction(); + impureFunction(); + voidFunction(); + possiblyImpureFunction(); + unknownFunction(); +} + +/** + * @phpstan-impure + */ +function actuallyPure() +{ + +} + +function voidFunctionThatThrows(): void +{ + if (rand(0, 1)) { + throw new \Exception(); + } +} + +function emptyVoidFunction(): void +{ + $a = 1 + 1; +} + +/** + * @phpstan-assert !null $a + */ +function emptyVoidFunctionWithAssertTag(?int $a): void +{ + +} + +/** + * @phpstan-pure + */ +function pureButAccessSuperGlobal(): int +{ + $a = $_POST['bla']; + $_POST['test'] = 1; + + return $_POST['test']; +} + +function emptyVoidFunctionWithByRefParameter(&$a): void +{ + +} + +/** + * @phpstan-pure + */ +function functionWithGlobal(): int +{ + global $db; + + return 1; +} + +/** + * @phpstan-pure + */ +function functionWithStaticVariable(): int +{ + static $v = 1; + + return $v; +} + +/** + * @phpstan-pure + * @param \Closure(): int $closure2 + */ +function callsClosures(\Closure $closure1, \Closure $closure2): int +{ + $closure1(); + return $closure2(); +} + +/** + * @phpstan-pure + * @param pure-callable $cb + * @param pure-Closure $closure + * @return int + */ +function callsPureCallableIdentifierTypeNode(callable $cb, \Closure $closure): int +{ + $cb(); + $closure(); +} diff --git a/tests/PHPStan/Rules/Pure/data/pure-method.php b/tests/PHPStan/Rules/Pure/data/pure-method.php new file mode 100644 index 0000000000..2b91f7ea69 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/pure-method.php @@ -0,0 +1,362 @@ +foo = 'test'; + } + + public function voidMethod(): void + { + echo '1'; + } + + /** + * @phpstan-impure + */ + public function impureVoidMethod(): void + { + echo ''; + } + + public function returningMethod(): int + { + + } + + /** + * @phpstan-pure + */ + public function pureReturningMethod(): int + { + + } + + /** + * @phpstan-impure + */ + public function impureReturningMethod(): int + { + echo ''; + } + + /** + * @phpstan-pure + */ + public function doFoo4() + { + $this->voidMethod(); + $this->impureVoidMethod(); + $this->returningMethod(); + $this->pureReturningMethod(); + $this->impureReturningMethod(); + $this->unknownMethod(); + } + + /** + * @phpstan-pure + */ + public function doFoo5() + { + self::voidMethod(); + self::impureVoidMethod(); + self::returningMethod(); + self::pureReturningMethod(); + self::impureReturningMethod(); + self::unknownMethod(); + } + + +} + +class PureConstructor +{ + + /** + * @phpstan-pure + */ + public function __construct() + { + + } + +} + +class ImpureConstructor +{ + + /** + * @phpstan-impure + */ + public function __construct() + { + echo ''; + } + +} + +class PossiblyImpureConstructor +{ + + public function __construct() + { + + } + +} + +class TestConstructors +{ + + /** + * @phpstan-pure + */ + public function doFoo(string $s) + { + new PureConstructor(); + new ImpureConstructor(); + new PossiblyImpureConstructor(); + new $s(); + } + +} + +class ActuallyPure +{ + + /** + * @phpstan-impure + */ + public function doFoo() + { + + } + +} + +class ToBeExtended +{ + + /** @phpstan-pure */ + public function pure(): int + { + + } + + /** @phpstan-impure */ + public function impure(): int + { + echo 'test'; + return 1; + } + +} + +class ExtendingClass extends ToBeExtended +{ + + public function pure(): int + { + echo 'test'; + return 1; + } + + public function impure(): int + { + return 1; + } + +} + +class ClassWithVoidMethods +{ + + public function voidFunctionThatThrows(): void + { + if (rand(0, 1)) { + throw new \Exception(); + } + } + + public function emptyVoidFunction(): void + { + + } + + protected function protectedEmptyVoidFunction(): void + { + + } + + private function privateEmptyVoidFunction(): void + { + $a = 1 + 1; + } + + private function setPostAndGet(array $post = [], array $get = []): void + { + $_POST = $post; + $_GET = $get; + } + + /** + * @phpstan-pure + */ + public function purePostGetAssign(array $post = [], array $get = []): int + { + $_POST = $post; + $_GET = $get; + + return 1; + } + +} + +class NoMagicMethods +{ + +} + +class PureMagicMethods +{ + + /** + * @phpstan-pure + */ + public function __toString(): string + { + return 'one'; + } + +} + +class MaybePureMagicMethods +{ + + public function __toString(): string + { + return 'one'; + } + +} + +class ImpureMagicMethods +{ + + /** + * @phpstan-impure + */ + public function __toString(): string + { + sleep(1); + return 'one'; + } + +} + +class TestMagicMethods +{ + + /** + * @phpstan-pure + */ + public function doFoo( + NoMagicMethods $no, + PureMagicMethods $pure, + MaybePureMagicMethods $maybe, + ImpureMagicMethods $impure + ) + { + (string) $no; + (string) $pure; + (string) $maybe; + (string) $impure; + } + +} + +class NoConstructor +{ + +} + +class TestNoConstructor +{ + + /** + * @phpstan-pure + */ + public function doFoo(): int + { + new NoConstructor(); + + return 1; + } + +} + +class MaybeCallableFromUnion +{ + + /** + * @phpstan-pure + * @param callable|string $p + */ + public function doFoo($p): int + { + $p(); + + return 1; + } + +} + +class VoidMethods +{ + + private function doFoo(): void + { + + } + + private function doBar(): void + { + \PHPStan\dumpType(1); + } + + private function doBaz(): void + { + // nop + ; + + // nop + ; + + // nop + ; + } + +} diff --git a/tests/PHPStan/Rules/RuleErrorBuilderTest.php b/tests/PHPStan/Rules/RuleErrorBuilderTest.php index 345b48f77e..59826f7172 100644 --- a/tests/PHPStan/Rules/RuleErrorBuilderTest.php +++ b/tests/PHPStan/Rules/RuleErrorBuilderTest.php @@ -20,30 +20,30 @@ public function testMessageAndLineAndBuild(): void $ruleError = $builder->build(); $this->assertSame('Foo', $ruleError->getMessage()); - $this->assertInstanceOf(LineRuleError::class, $ruleError); + $this->assertInstanceOf(LineRuleError::class, $ruleError); // @phpstan-ignore method.alreadyNarrowedType $this->assertSame(25, $ruleError->getLine()); } public function testMessageAndFileAndBuild(): void { - $builder = RuleErrorBuilder::message('Foo')->file('Bar.php'); + $builder = RuleErrorBuilder::message('Foo')->file(__FILE__); $ruleError = $builder->build(); $this->assertSame('Foo', $ruleError->getMessage()); - $this->assertInstanceOf(FileRuleError::class, $ruleError); - $this->assertSame('Bar.php', $ruleError->getFile()); + $this->assertInstanceOf(FileRuleError::class, $ruleError); // @phpstan-ignore method.alreadyNarrowedType + $this->assertSame(__FILE__, $ruleError->getFile()); } public function testMessageAndLineAndFileAndBuild(): void { - $builder = RuleErrorBuilder::message('Foo')->line(25)->file('Bar.php'); + $builder = RuleErrorBuilder::message('Foo')->line(25)->file(__FILE__); $ruleError = $builder->build(); $this->assertSame('Foo', $ruleError->getMessage()); - $this->assertInstanceOf(LineRuleError::class, $ruleError); - $this->assertInstanceOf(FileRuleError::class, $ruleError); + $this->assertInstanceOf(LineRuleError::class, $ruleError); // @phpstan-ignore method.alreadyNarrowedType + $this->assertInstanceOf(FileRuleError::class, $ruleError); // @phpstan-ignore method.alreadyNarrowedType $this->assertSame(25, $ruleError->getLine()); - $this->assertSame('Bar.php', $ruleError->getFile()); + $this->assertSame(__FILE__, $ruleError->getFile()); } } diff --git a/tests/PHPStan/Rules/ScopeFunctionCallStackRule.php b/tests/PHPStan/Rules/ScopeFunctionCallStackRule.php new file mode 100644 index 0000000000..573602c8dd --- /dev/null +++ b/tests/PHPStan/Rules/ScopeFunctionCallStackRule.php @@ -0,0 +1,38 @@ + */ +class ScopeFunctionCallStackRule implements Rule +{ + + public function getNodeType(): string + { + return Throw_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $messages = []; + foreach ($scope->getFunctionCallStack() as $reflection) { + if ($reflection instanceof FunctionReflection) { + $messages[] = $reflection->getName(); + continue; + } + + $messages[] = sprintf('%s::%s', $reflection->getDeclaringClass()->getDisplayName(), $reflection->getName()); + } + + return [ + RuleErrorBuilder::message(implode("\n", $messages))->identifier('dummy')->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/ScopeFunctionCallStackRuleTest.php b/tests/PHPStan/Rules/ScopeFunctionCallStackRuleTest.php new file mode 100644 index 0000000000..d59d22835e --- /dev/null +++ b/tests/PHPStan/Rules/ScopeFunctionCallStackRuleTest.php @@ -0,0 +1,41 @@ + + */ +class ScopeFunctionCallStackRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ScopeFunctionCallStackRule(); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/scope-function-call-stack.php'], [ + [ + "var_dump\nprint_r\nsleep", + 7, + ], + [ + "var_dump\nprint_r\nsleep", + 10, + ], + [ + "var_dump\nprint_r\nsleep", + 13, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRule.php b/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRule.php new file mode 100644 index 0000000000..b26df715b8 --- /dev/null +++ b/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRule.php @@ -0,0 +1,42 @@ + */ +class ScopeFunctionCallStackWithParametersRule implements Rule +{ + + public function getNodeType(): string + { + return Throw_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $messages = []; + foreach ($scope->getFunctionCallStackWithParameters() as [$reflection, $parameter]) { + if ($parameter === null) { + throw new ShouldNotHappenException(); + } + if ($reflection instanceof FunctionReflection) { + $messages[] = sprintf('%s ($%s)', $reflection->getName(), $parameter->getName()); + continue; + } + + $messages[] = sprintf('%s::%s ($%s)', $reflection->getDeclaringClass()->getDisplayName(), $reflection->getName(), $parameter->getName()); + } + + return [ + RuleErrorBuilder::message(implode("\n", $messages))->identifier('dummy')->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRuleTest.php b/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRuleTest.php new file mode 100644 index 0000000000..38a0aecd61 --- /dev/null +++ b/tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRuleTest.php @@ -0,0 +1,41 @@ + + */ +class ScopeFunctionCallStackWithParametersRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ScopeFunctionCallStackWithParametersRule(); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/scope-function-call-stack.php'], [ + [ + "var_dump (\$value)\nprint_r (\$value)\nsleep (\$seconds)", + 7, + ], + [ + "var_dump (\$value)\nprint_r (\$value)\nsleep (\$seconds)", + 10, + ], + [ + "var_dump (\$value)\nprint_r (\$value)\nsleep (\$seconds)", + 13, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRuleTest.php new file mode 100644 index 0000000000..a7406ad81b --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRuleTest.php @@ -0,0 +1,43 @@ + + */ +class TooWideFunctionParameterOutTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new TooWideFunctionParameterOutTypeRule(new TooWideParameterOutTypeCheck()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/too-wide-function-parameter-out.php'], [ + [ + 'Function TooWideFunctionParameterOut\doBar() never assigns null to &$p so it can be removed from the by-ref type.', + 10, + 'You can narrow the parameter out type with @param-out PHPDoc tag.', + ], + [ + 'Function TooWideFunctionParameterOut\doBaz() never assigns null to &$p so it can be removed from the @param-out type.', + 18, + ], + [ + 'Function TooWideFunctionParameterOut\doLorem() never assigns null to &$p so it can be removed from the by-ref type.', + 23, + 'You can narrow the parameter out type with @param-out PHPDoc tag.', + ], + [ + 'Function TooWideFunctionParameterOut\bug10699() never assigns 20 to &$out so it can be removed from the @param-out type.', + 48, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php index 8b6438796d..b914db3141 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php @@ -44,6 +44,10 @@ public function testRule(): void 'Function TooWideFunctionReturnType\dolor6() never returns null so it can be removed from the return type.', 79, ], + [ + 'Function TooWideFunctionReturnType\conditionalType() never returns string so it can be removed from the return type.', + 90, + ], ]); } diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRuleTest.php new file mode 100644 index 0000000000..cc2ef739c8 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRuleTest.php @@ -0,0 +1,53 @@ + + */ +class TooWideMethodParameterOutTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new TooWideMethodParameterOutTypeRule(new TooWideParameterOutTypeCheck()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/too-wide-method-parameter-out.php'], [ + [ + 'Method TooWideMethodParameterOut\Foo::doBar() never assigns null to &$p so it can be removed from the by-ref type.', + 13, + 'You can narrow the parameter out type with @param-out PHPDoc tag.', + ], + [ + 'Method TooWideMethodParameterOut\Foo::doBaz() never assigns null to &$p so it can be removed from the @param-out type.', + 21, + ], + [ + 'Method TooWideMethodParameterOut\Foo::doLorem() never assigns null to &$p so it can be removed from the by-ref type.', + 26, + 'You can narrow the parameter out type with @param-out PHPDoc tag.', + ], + [ + 'Method TooWideMethodParameterOut\Foo::bug10699() never assigns 20 to &$out so it can be removed from the @param-out type.', + 37, + ], + ]); + } + + public function testBug10684(): void + { + $this->analyse([__DIR__ . '/data/bug-10684.php'], []); + } + + public function testBug10687(): void + { + $this->analyse([__DIR__ . '/data/bug-10687.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php index 0f3ff533a1..9e9588d219 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php @@ -12,9 +12,13 @@ class TooWideMethodReturnTypehintRuleTest extends RuleTestCase { + private bool $checkProtectedAndPublicMethods = true; + + private bool $alwaysCheckFinal = false; + protected function getRule(): Rule { - return new TooWideMethodReturnTypehintRule(true); + return new TooWideMethodReturnTypehintRule($this->checkProtectedAndPublicMethods, $this->alwaysCheckFinal); } public function testPrivate(): void @@ -44,6 +48,10 @@ public function testPrivate(): void 'Method TooWideMethodReturnType\Foo::dolor6() never returns null so it can be removed from the return type.', 86, ], + [ + 'Method TooWideMethodReturnType\ConditionalTypeClass::conditionalType() never returns string so it can be removed from the return type.', + 119, + ], ]); } @@ -97,4 +105,147 @@ public function testBug6158(): void $this->analyse([__DIR__ . '/data/bug-6158.php'], []); } + public function testBug6175(): void + { + $this->analyse([__DIR__ . '/data/bug-6175.php'], []); + } + + public function dataAlwaysCheckFinal(): iterable + { + yield [ + false, + false, + [ + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\Foo::test() never returns null so it can be removed from the return type.', + 8, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test() never returns null so it can be removed from the return type.', + 28, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test() never returns null so it can be removed from the return type.', + 48, + ], + ], + ]; + + yield [ + true, + false, + [ + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\Foo::test() never returns null so it can be removed from the return type.', + 8, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test() never returns null so it can be removed from the return type.', + 28, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test2() never returns null so it can be removed from the return type.', + 33, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test3() never returns null so it can be removed from the return type.', + 38, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test() never returns null so it can be removed from the return type.', + 48, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test2() never returns null so it can be removed from the return type.', + 53, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test3() never returns null so it can be removed from the return type.', + 58, + ], + ], + ]; + + yield [ + false, + true, + [ + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\Foo::test() never returns null so it can be removed from the return type.', + 8, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test() never returns null so it can be removed from the return type.', + 28, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test2() never returns null so it can be removed from the return type.', + 33, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test3() never returns null so it can be removed from the return type.', + 38, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test() never returns null so it can be removed from the return type.', + 48, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test2() never returns null so it can be removed from the return type.', + 53, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test3() never returns null so it can be removed from the return type.', + 58, + ], + ], + ]; + + yield [ + true, + true, + [ + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\Foo::test() never returns null so it can be removed from the return type.', + 8, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test() never returns null so it can be removed from the return type.', + 28, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test2() never returns null so it can be removed from the return type.', + 33, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FinalFoo::test3() never returns null so it can be removed from the return type.', + 38, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test() never returns null so it can be removed from the return type.', + 48, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test2() never returns null so it can be removed from the return type.', + 53, + ], + [ + 'Method MethodTooWideReturnAlwaysCheckFinal\FooFinalMethods::test3() never returns null so it can be removed from the return type.', + 58, + ], + ], + ]; + } + + /** + * @dataProvider dataAlwaysCheckFinal + * @param list $expectedErrors + */ + public function testAlwaysCheckFinal(bool $checkProtectedAndPublicMethods, bool $alwaysCheckFinal, array $expectedErrors): void + { + $this->checkProtectedAndPublicMethods = $checkProtectedAndPublicMethods; + $this->alwaysCheckFinal = $alwaysCheckFinal; + $this->analyse([__DIR__ . '/data/method-too-wide-return-always-check-final.php'], $expectedErrors); + } + } diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-10684.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10684.php new file mode 100644 index 0000000000..1949d4d8b6 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10684.php @@ -0,0 +1,41 @@ += 7.4 + +namespace Bug10684; + +abstract class HookBreaker extends Exception +{ + /** + * @return mixed + */ + public function getReturnValue() + { + return 1; + } +} + +class Foo +{ + /** @var \Closure(): void */ + protected \Closure $hook; + + /** + * @return mixed + */ + public function hook(HookBreaker &$brokenBy = null) + { + $brokenBy = null; + + $return = []; + if (mt_rand() === 0) { + try { + ($this->hook)(); + } catch (HookBreaker $e) { + $brokenBy = $e; + + return $e->getReturnValue(); + } + } + + return $return; + } +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-10687.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10687.php new file mode 100644 index 0000000000..eb8f9c5f42 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-10687.php @@ -0,0 +1,20 @@ += 7.4 + +namespace Bug6175TooWide; + +trait SomeTrait { + private function sayHello(): ?string // @phpstan-ignore-line + { + return $this->value; + } +} + +class HelloWorld2 +{ + use SomeTrait; + private string $value = ''; + public function sayIt(): void + { + echo $this->sayHello(); + } +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/method-too-wide-return-always-check-final.php b/tests/PHPStan/Rules/TooWideTypehints/data/method-too-wide-return-always-check-final.php new file mode 100644 index 0000000000..9a2093ad1d --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/method-too-wide-return-always-check-final.php @@ -0,0 +1,63 @@ + + */ +class ConflictingTraitConstantsRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new ConflictingTraitConstantsRule(self::getContainer()->getByType(InitializerExprTypeResolver::class)); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/conflicting-trait-constants.php'], [ + [ + 'Protected constant ConflictingTraitConstants\Bar::PUBLIC_CONSTANT overriding public constant ConflictingTraitConstants\Foo::PUBLIC_CONSTANT should also be public.', + 23, + ], + [ + 'Private constant ConflictingTraitConstants\Bar2::PUBLIC_CONSTANT overriding public constant ConflictingTraitConstants\Foo::PUBLIC_CONSTANT should also be public.', + 32, + ], + [ + 'Public constant ConflictingTraitConstants\Bar3::PROTECTED_CONSTANT overriding protected constant ConflictingTraitConstants\Foo::PROTECTED_CONSTANT should also be protected.', + 41, + ], + [ + 'Private constant ConflictingTraitConstants\Bar4::PROTECTED_CONSTANT overriding protected constant ConflictingTraitConstants\Foo::PROTECTED_CONSTANT should also be protected.', + 50, + ], + [ + 'Protected constant ConflictingTraitConstants\Bar5::PRIVATE_CONSTANT overriding private constant ConflictingTraitConstants\Foo::PRIVATE_CONSTANT should also be private.', + 59, + ], + [ + 'Public constant ConflictingTraitConstants\Bar6::PRIVATE_CONSTANT overriding private constant ConflictingTraitConstants\Foo::PRIVATE_CONSTANT should also be private.', + 68, + ], + [ + 'Non-final constant ConflictingTraitConstants\Bar7::PUBLIC_FINAL_CONSTANT overriding final constant ConflictingTraitConstants\Foo::PUBLIC_FINAL_CONSTANT should also be final.', + 77, + ], + [ + 'Final constant ConflictingTraitConstants\Bar8::PUBLIC_CONSTANT overriding non-final constant ConflictingTraitConstants\Foo::PUBLIC_CONSTANT should also be non-final.', + 86, + ], + [ + 'Constant ConflictingTraitConstants\Bar9::PUBLIC_CONSTANT with value 2 overriding constant ConflictingTraitConstants\Foo::PUBLIC_CONSTANT with different value 1 should have the same value.', + 96, + ], + ]); + } + + public function testNativeTypes(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->analyse([__DIR__ . '/data/conflicting-trait-constants-types.php'], [ + [ + 'Constant ConflictingTraitConstantsTypes\Baz::FOO_CONST (int) overriding constant ConflictingTraitConstantsTypes\Foo::FOO_CONST (int|string) should have the same native type int|string.', + 28, + ], + [ + 'Constant ConflictingTraitConstantsTypes\Baz::BAR_CONST (int) overriding constant ConflictingTraitConstantsTypes\Foo::BAR_CONST should not have a native type.', + 30, + ], + [ + 'Constant ConflictingTraitConstantsTypes\Lorem::FOO_CONST overriding constant ConflictingTraitConstantsTypes\Foo::FOO_CONST (int|string) should also have native type int|string.', + 39, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Traits/ConstantsInTraitsRuleTest.php b/tests/PHPStan/Rules/Traits/ConstantsInTraitsRuleTest.php new file mode 100644 index 0000000000..3b6f591527 --- /dev/null +++ b/tests/PHPStan/Rules/Traits/ConstantsInTraitsRuleTest.php @@ -0,0 +1,57 @@ + + */ +class ConstantsInTraitsRuleTest extends RuleTestCase +{ + + private int $phpVersionId; + + protected function getRule(): Rule + { + return new ConstantsInTraitsRule(new PhpVersion($this->phpVersionId)); + } + + public function dataRule(): array + { + return [ + [ + 80100, + [ + [ + 'Constant is declared inside a trait but is only supported on PHP 8.2 and later.', + 7, + ], + [ + 'Constant is declared inside a trait but is only supported on PHP 8.2 and later.', + 8, + ], + ], + ], + [ + 80200, + [], + ], + ]; + } + + /** + * @dataProvider dataRule + * + * @param list $errors + */ + public function testRule(int $phpVersionId, array $errors): void + { + $this->phpVersionId = $phpVersionId; + $this->analyse([__DIR__ . '/data/constants-in-traits.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Traits/data/conflicting-trait-constants-types.php b/tests/PHPStan/Rules/Traits/data/conflicting-trait-constants-types.php new file mode 100644 index 0000000000..eaa130ffeb --- /dev/null +++ b/tests/PHPStan/Rules/Traits/data/conflicting-trait-constants-types.php @@ -0,0 +1,41 @@ += 8.2 + +namespace ConflictingTraitConstants; + +trait Foo +{ + + public const PUBLIC_CONSTANT = 1; + + protected const PROTECTED_CONSTANT = 1; + + private const PRIVATE_CONSTANT = 1; + + final public const PUBLIC_FINAL_CONSTANT = 1; + +} + +class Bar +{ + + use Foo; + + protected const PUBLIC_CONSTANT = 1; + +} + +class Bar2 +{ + + use Foo; + + private const PUBLIC_CONSTANT = 1; + +} + +class Bar3 +{ + + use Foo; + + public const PROTECTED_CONSTANT = 1; + +} + +class Bar4 +{ + + use Foo; + + private const PROTECTED_CONSTANT = 1; + +} + +class Bar5 +{ + + use Foo; + + protected const PRIVATE_CONSTANT = 1; + +} + +class Bar6 +{ + + use Foo; + + public const PRIVATE_CONSTANT = 1; + +} + +class Bar7 +{ + + use Foo; + + public const PUBLIC_FINAL_CONSTANT = 1; + +} + +class Bar8 +{ + + use Foo; + + final public const PUBLIC_CONSTANT = 1; + +} + + +class Bar9 +{ + + use Foo; + + public const PUBLIC_CONSTANT = 2; + +} + +class Bar10 +{ + use Foo; + + final public const PUBLIC_FINAL_CONSTANT = 1; +} diff --git a/tests/PHPStan/Rules/Traits/data/constants-in-traits.php b/tests/PHPStan/Rules/Traits/data/constants-in-traits.php new file mode 100644 index 0000000000..f13d2fd172 --- /dev/null +++ b/tests/PHPStan/Rules/Traits/data/constants-in-traits.php @@ -0,0 +1,14 @@ += 8.2 + +namespace ConstantsInTraits; + +trait FooBar +{ + const FOO = 'foo'; + public const BAR = 'bar', QUX = 'qux'; +} + +class Consumer +{ + use FooBar; +} diff --git a/tests/PHPStan/Rules/Types/InvalidTypesInUnionRuleTest.php b/tests/PHPStan/Rules/Types/InvalidTypesInUnionRuleTest.php new file mode 100644 index 0000000000..426f544dcd --- /dev/null +++ b/tests/PHPStan/Rules/Types/InvalidTypesInUnionRuleTest.php @@ -0,0 +1,91 @@ + + */ +class InvalidTypesInUnionRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new InvalidTypesInUnionRule(); + } + + public function testRuleOnUnionWithVoid(): void + { + $this->analyse([__DIR__ . '/data/invalid-union-with-void.php'], [ + [ + 'Type void cannot be part of a union type declaration.', + 11, + ], + [ + 'Type void cannot be part of a nullable type declaration.', + 15, + ], + ]); + } + + /** + * @requires PHP 8.0 + */ + public function testRuleOnUnionWithMixed(): void + { + $this->analyse([__DIR__ . '/data/invalid-union-with-mixed.php'], [ + [ + 'Type mixed cannot be part of a nullable type declaration.', + 9, + ], + [ + 'Type mixed cannot be part of a union type declaration.', + 12, + ], + [ + 'Type mixed cannot be part of a union type declaration.', + 16, + ], + [ + 'Type mixed cannot be part of a union type declaration.', + 17, + ], + [ + 'Type mixed cannot be part of a union type declaration.', + 22, + ], + [ + 'Type mixed cannot be part of a nullable type declaration.', + 29, + ], + [ + 'Type mixed cannot be part of a nullable type declaration.', + 29, + ], + [ + 'Type mixed cannot be part of a nullable type declaration.', + 34, + ], + ]); + } + + /** + * @requires PHP 8.1 + */ + public function testRuleOnUnionWithNever(): void + { + $this->analyse([__DIR__ . '/data/invalid-union-with-never.php'], [ + [ + 'Type never cannot be part of a nullable type declaration.', + 7, + ], + [ + 'Type never cannot be part of a union type declaration.', + 16, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Types/data/invalid-union-with-mixed.php b/tests/PHPStan/Rules/Types/data/invalid-union-with-mixed.php new file mode 100644 index 0000000000..2db7a96193 --- /dev/null +++ b/tests/PHPStan/Rules/Types/data/invalid-union-with-mixed.php @@ -0,0 +1,34 @@ + $a; diff --git a/tests/PHPStan/Rules/Types/data/invalid-union-with-never.php b/tests/PHPStan/Rules/Types/data/invalid-union-with-never.php new file mode 100644 index 0000000000..d959067f89 --- /dev/null +++ b/tests/PHPStan/Rules/Types/data/invalid-union-with-never.php @@ -0,0 +1,24 @@ + */ private $nodeType; - /** @var (callable(TNodeType, Scope): array) */ + /** @var (callable(TNodeType, Scope): list) */ private $processNodeCallback; /** * @param class-string $nodeType - * @param (callable(TNodeType, Scope): array) $processNodeCallback + * @param (callable(TNodeType, Scope): list) $processNodeCallback */ public function __construct(string $nodeType, callable $processNodeCallback) { @@ -35,7 +35,7 @@ public function getNodeType(): string /** * @param TNodeType $node - * @return array + * @return list */ public function processNode(Node $node, Scope $scope): array { diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 7445ae33f9..da7c48e022 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -66,6 +66,10 @@ public function testDefinedVariables(): void 'Undefined variable: $parseStrParameter', 34, ], + [ + 'Undefined variable: $parseStrParameter', + 36, + ], [ 'Undefined variable: $foo', 39, @@ -98,6 +102,10 @@ public function testDefinedVariables(): void 'Undefined variable: $variableInEmpty', 145, ], + [ + 'Undefined variable: $negatedVariableInEmpty', + 152, + ], [ 'Undefined variable: $variableInEmpty', 155, @@ -207,27 +215,23 @@ public function testDefinedVariables(): void 360, ], [ - 'Undefined variable: $variableInWhileIsset', - 365, - ], - [ - 'Undefined variable: $unknownVariablePassedToReset', + 'Variable $unknownVariablePassedToReset might not be defined.', 368, ], [ - 'Undefined variable: $unknownVariablePassedToReset', + 'Variable $unknownVariablePassedToReset might not be defined.', 369, ], [ - 'Undefined variable: $variableInAssign', + 'Variable $variableInAssign might not be defined.', 384, ], [ - 'Undefined variable: $undefinedArrayIndex', + 'Variable $undefinedArrayIndex might not be defined.', 409, ], [ - 'Undefined variable: $anotherUndefinedArrayIndex', + 'Variable $anotherUndefinedArrayIndex might not be defined.', 409, ], [ @@ -569,7 +573,7 @@ public function dataForeachPolluteScopeWithAlwaysIterableForeach(): array /** * @dataProvider dataForeachPolluteScopeWithAlwaysIterableForeach * - * @param mixed[] $errors + * @param list $errors */ public function testForeachPolluteScopeWithAlwaysIterableForeach(bool $polluteScopeWithAlwaysIterableForeach, array $errors): void { @@ -886,4 +890,174 @@ public function testBug8142(): void $this->analyse([__DIR__ . '/data/bug-8142.php'], []); } + public function testBug5401(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-5401.php'], []); + } + + public function testBug8212(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-8212.php'], []); + } + + public function testBug4173(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-4173.php'], [ + [ + 'Variable $value might not be defined.', // could be fixed + 30, + ], + ]); + } + + public function testBug5805(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-5805.php'], []); + } + + public function testBug8467c(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = false; + $this->analyse([__DIR__ . '/data/bug-8467c.php'], [ + [ + 'Variable $v might not be defined.', + 16, + ], + [ + 'Variable $v might not be defined.', + 18, + ], + ]); + } + + public function testBug393(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-393.php'], []); + } + + public function testBug9474(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-9474.php'], []); + } + + public function testEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/defined-variables-enum.php'], []); + } + + public function testBug5326(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-5326.php'], []); + } + + public function testBug5266(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-5266.php'], []); + } + + public function testIsStringNarrowsCertainty(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/isstring-certainty.php'], [ + [ + 'Variable $a might not be defined.', + 11, + ], + [ + 'Undefined variable: $a', + 19, + ], + ]); + } + + public function testDiscussion10252(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/discussion-10252.php'], []); + } + + public function testBug10418(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-10418.php'], []); + } + + public function testPassByReferenceIntoNotNullable(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/pass-by-reference-into-not-nullable.php'], [ + [ + 'Undefined variable: $three', + 32, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/EmptyRuleTest.php b/tests/PHPStan/Rules/Variables/EmptyRuleTest.php index 76d4b41699..6fa229669f 100644 --- a/tests/PHPStan/Rules/Variables/EmptyRuleTest.php +++ b/tests/PHPStan/Rules/Variables/EmptyRuleTest.php @@ -91,6 +91,18 @@ public function testBug6974(): void { $this->treatPhpDocTypesAsCertain = false; $this->strictUnnecessaryNullsafePropertyFetch = false; + $this->analyse([__DIR__ . '/data/bug-6974.php'], [ + [ + 'Variable $a in empty() always exists and is always falsy.', + 12, + ], + ]); + } + + public function testBug6974TreatPhpDocTypesAsCertain(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->strictUnnecessaryNullsafePropertyFetch = false; $this->analyse([__DIR__ . '/data/bug-6974.php'], [ [ 'Variable $a in empty() always exists and is always falsy.', @@ -193,4 +205,29 @@ public function testBug7199(): void $this->analyse([__DIR__ . '/data/bug-7199.php'], []); } + public function testBug9126(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->strictUnnecessaryNullsafePropertyFetch = false; + + $this->analyse([__DIR__ . '/data/bug-9126.php'], []); + } + + public function dataBug9403(): iterable + { + yield [true]; + yield [false]; + } + + /** + * @dataProvider dataBug9403 + */ + public function testBug9403(bool $treatPhpDocTypesAsCertain): void + { + $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; + $this->strictUnnecessaryNullsafePropertyFetch = false; + + $this->analyse([__DIR__ . '/data/bug-9403.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index f00ac31ad7..960dad4d31 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -281,7 +281,7 @@ public function testVariableCertaintyInIsset(): void 116, ], [ - 'Variable $variableInSecondCase in isset() always exists and is always null.', + 'Variable $variableInSecondCase in isset() is never defined.', 117, ], [ @@ -433,4 +433,54 @@ public function testBug6008(): void $this->analyse([__DIR__ . '/data/bug-6008.php'], []); } + public function testBug7292(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->strictUnnecessaryNullsafePropertyFetch = true; + + $this->analyse([__DIR__ . '/data/bug-7292.php'], []); + } + + public function testObjectShapes(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->strictUnnecessaryNullsafePropertyFetch = true; + + // could be checked but current is not + $this->analyse([__DIR__ . '/data/isset-object-shapes.php'], []); + } + + public function testBug10151(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->strictUnnecessaryNullsafePropertyFetch = true; + + $this->analyse([__DIR__ . '/data/bug-10151.php'], []); + } + + public function testBug3985(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->strictUnnecessaryNullsafePropertyFetch = true; + + $this->analyse([__DIR__ . '/../../Analyser/data/bug-3985.php'], [ + [ + 'Variable $foo in isset() is never defined.', + 13, + ], + [ + 'Variable $foo in isset() is never defined.', + 21, + ], + ]); + } + + public function testBug10064(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->strictUnnecessaryNullsafePropertyFetch = true; + + $this->analyse([__DIR__ . '/data/bug-10064.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php index 33ca615df9..4781fa1f18 100644 --- a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php @@ -360,4 +360,28 @@ public function testBug7968(): void $this->analyse([__DIR__ . '/data/bug-7968.php'], []); } + public function testBug8084(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->strictUnnecessaryNullsafePropertyFetch = true; + + $this->analyse([__DIR__ . '/data/bug-8084.php'], []); + } + + public function testBug10577(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->strictUnnecessaryNullsafePropertyFetch = true; + + $this->analyse([__DIR__ . '/data/bug-10577.php'], []); + } + + public function testBug10610(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->strictUnnecessaryNullsafePropertyFetch = true; + + $this->analyse([__DIR__ . '/data/bug-10610.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php new file mode 100644 index 0000000000..77bdcc2c1f --- /dev/null +++ b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php @@ -0,0 +1,67 @@ + + */ +class ParameterOutAssignedTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new ParameterOutAssignedTypeRule( + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, true, false, true, false), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/parameter-out-assigned-type.php'], [ + [ + 'Parameter &$p @param-out type of function ParameterOutAssignedType\foo() expects int, string given.', + 10, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutAssignedType\Foo::doFoo() expects int, string given.', + 21, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutAssignedType\Foo::doBar() expects string, int given.', + 29, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutAssignedType\Foo::doBaz() expects list, array<0|int<2, max>, int> given.', + 38, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutAssignedType\Foo::doBaz2() expects list, non-empty-list<\'str\'|int> given.', + 47, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutAssignedType\Foo::doBaz3() expects list>, array, array, int>> given.', + 56, + ], + [ + 'Parameter &$p by-ref type of method ParameterOutAssignedType\Foo::doNoParamOut() expects string, int given.', + 61, + 'You can change the parameter out type with @param-out PHPDoc tag.', + ], + ]); + } + + public function testBug10699(): void + { + $this->analyse([__DIR__ . '/../../Analyser/data/bug-10699.php'], []); + } + + public function testBenevolentArrayKey(): void + { + $this->analyse([__DIR__ . '/data/benevolent-array-key.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Variables/ParameterOutExecutionEndTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ParameterOutExecutionEndTypeRuleTest.php new file mode 100644 index 0000000000..96ff882d8c --- /dev/null +++ b/tests/PHPStan/Rules/Variables/ParameterOutExecutionEndTypeRuleTest.php @@ -0,0 +1,48 @@ + + */ +class ParameterOutExecutionEndTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ParameterOutExecutionEndTypeRule( + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, true, false, true, false), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/parameter-out-execution-end.php'], [ + [ + 'Parameter &$p @param-out type of method ParameterOutExecutionEnd\Foo::foo2() expects string, string|null given.', + 21, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutExecutionEnd\Foo::foo3() expects string, string|null given.', + 34, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutExecutionEnd\Foo::foo4() expects string, string|null given.', + 45, + ], + [ + 'Parameter &$p @param-out type of method ParameterOutExecutionEnd\Foo::foo6() expects int, string given.', + 69, + ], + [ + 'Parameter &$p @param-out type of function ParameterOutExecutionEnd\foo2() expects string, string|null given.', + 80, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Variables/ThrowTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ThrowTypeRuleTest.php index ed2168609c..020f713708 100644 --- a/tests/PHPStan/Rules/Variables/ThrowTypeRuleTest.php +++ b/tests/PHPStan/Rules/Variables/ThrowTypeRuleTest.php @@ -15,7 +15,7 @@ class ThrowTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ThrowTypeRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false)); + return new ThrowTypeRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false)); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Variables/UnsetRuleTest.php b/tests/PHPStan/Rules/Variables/UnsetRuleTest.php index e2e31cac2b..f6897a5b9c 100644 --- a/tests/PHPStan/Rules/Variables/UnsetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/UnsetRuleTest.php @@ -81,4 +81,14 @@ public function testBug7417(): void $this->analyse([__DIR__ . '/data/bug-7417.php'], []); } + public function testBug8113(): void + { + $this->analyse([__DIR__ . '/data/bug-8113.php'], []); + } + + public function testBug4565(): void + { + $this->analyse([__DIR__ . '/../../Analyser/data/bug-4565.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php b/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php index d76738b388..801d2b31f4 100644 --- a/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php +++ b/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php @@ -15,7 +15,7 @@ class VariableCloningRuleTest extends RuleTestCase protected function getRule(): Rule { - return new VariableCloningRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false)); + return new VariableCloningRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false, false, true, false)); } public function testClone(): void @@ -38,8 +38,12 @@ public function testClone(): void 19, ], [ - 'Cloning object of an unknown class VariableCloning\Bar.', + 'Cannot clone non-object variable $baz of type VariableCloning\Bar|VariableCloning\Foo|null.', 23, + ], + [ + 'Cloning object of an unknown class VariableCloning\Bar.', + 35, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], ]); diff --git a/tests/PHPStan/Rules/Variables/data/benevolent-array-key.php b/tests/PHPStan/Rules/Variables/data/benevolent-array-key.php new file mode 100644 index 0000000000..83be0c39f6 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/benevolent-array-key.php @@ -0,0 +1,53 @@ + $matches + * @param-out array> $matches + */ + public static function matchAllStrictGroups(array &$matches): int + { + $result = self::matchAll($matches); + + return $result; + } + + /** + * @param array $matches + * @param-out array> $matches + */ + public static function matchAll(array &$matches): int + { + $matches = [['foo']]; + + return 1; + } +} + +class HelloWorld2 +{ + /** + * @param array $matches + * @param-out array> $matches + */ + public static function matchAllStrictGroups(array &$matches): int + { + $result = self::matchAll($matches); + + return $result; + } + + /** + * @param array $matches + * @param-out array> $matches + */ + public static function matchAll(array &$matches): int + { + $matches = [['foo']]; + + return 1; + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-10064.php b/tests/PHPStan/Rules/Variables/data/bug-10064.php new file mode 100644 index 0000000000..4f8808c669 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-10064.php @@ -0,0 +1,23 @@ + 5 ? 42: null; // a possibly null var + $b = random_int(0, 10) > 6 ? 47: null; // a possibly null var + if (isset($a, $b)) { + return $check > $a && $check < $b; + } + if (isset($a)) { + return $check > $a; + } + if (isset($b)) { + return $check < $b; + } + + return false; + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-10151.php b/tests/PHPStan/Rules/Variables/data/bug-10151.php new file mode 100644 index 0000000000..f93e860eb8 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-10151.php @@ -0,0 +1,25 @@ += 7.4 + +namespace Bug10151; + +class Test +{ + /** + * @var array + */ + protected array $cache = []; + + public function getCachedItemId (string $keyName): void + { + $result = $this->cache[$keyName] ??= ($newIndex = count($this->cache) + 1); + + // WRONG ERROR: Variable $newIndex in isset() always exists and is not nullable. + if (isset($newIndex)) { + $this->recordNewCacheItem($keyName); + } + } + + protected function recordNewCacheItem (string $keyName): void { + // ... + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-10418.php b/tests/PHPStan/Rules/Variables/data/bug-10418.php new file mode 100644 index 0000000000..d193c95e69 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-10418.php @@ -0,0 +1,12 @@ += 8.1 + +namespace Bug10418; + +function (): void { + $text = '123'; + $result = match(1){ + preg_match('/(\d+)/', $text, $match) => 'matched number: ' . $match[1], + preg_match('/(\w+)/', $text, $match) => 'matched word: ' . json_encode($match), + default => 'no matches!' + }; +}; diff --git a/tests/PHPStan/Rules/Variables/data/bug-10577.php b/tests/PHPStan/Rules/Variables/data/bug-10577.php new file mode 100644 index 0000000000..c84a6897ff --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-10577.php @@ -0,0 +1,29 @@ + 'Test1', + '20' => 'Test2', + ]; + + + public function validate(string $value): void + { + $value = trim($value); + + if ($value === '') { + throw new \RuntimeException(); + } + + $value = self::MAP[$value] ?? $value; + + assertType("'Test1'|'Test2'", self::MAP[$value]); + + // ... + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-10610.php b/tests/PHPStan/Rules/Variables/data/bug-10610.php new file mode 100644 index 0000000000..d56a2a8b07 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-10610.php @@ -0,0 +1,87 @@ + [ + '19' => '582', + '26' => '689', + '56' => '817', + '52' => '1050', + '67' => '2923', + '78' => '4057', + '75' => '4078', + '54' => '4078', + '76' => '4079', + '77' => '4080', + '9' => '4080', + '46' => '4091', + '22' => '4111', + '48' => '4112', + '70' => '4113', + '42' => '4117', + '43' => '4118', + '6' => '4126', + '36' => '4129', + '13' => '4309', + '14' => '4904', + '5' => '5222', + '71' => '5223', + '73' => '5242', + '74' => '5250', + '24' => '5252', + '58' => '5255', + '35' => '5261', + '1' => '5264', + '20' => '5268', + '21' => '5269', + '31' => '5270', + '51' => '5271', + '55' => '5271', + '39' => '5274', + '50' => '5277', + '49' => '5278', + '11' => '5279', + '41' => '5279', + '44' => '5280', + '59' => '5281', + '60' => '5281', + '23' => '5281', + '72' => '5283', + '32' => '5283', + '8' => '5285', + '40' => '5285', + '12' => '5298', + '37' => '5305', + '65' => '5310', + '64' => '5310', + '57' => '5352', + '33' => '5364', + '25' => '5375', + '34' => '5460', + '45' => '7581', + '3' => '7624', + '53' => '7672', + '999' => '7953', + '69' => '7953', + '2' => '8206', + '7' => '9697', + ], + 'bar' => [ + '30' => 'Test3', + ], + ]; + + public function validate(string $k, string $value): void + { + $res = self::MAP[$k][$value] ?? ''; + + assertType("'1050'|'2923'|'4057'|'4078'|'4079'|'4080'|'4091'|'4111'|'4112'|'4113'|'4117'|'4118'|'4126'|'4129'|'4309'|'4904'|'5222'|'5223'|'5242'|'5250'|'5252'|'5255'|'5261'|'5264'|'5268'|'5269'|'5270'|'5271'|'5274'|'5277'|'5278'|'5279'|'5280'|'5281'|'5283'|'5285'|'5298'|'5305'|'5310'|'5352'|'5364'|'5375'|'5460'|'582'|'689'|'7581'|'7624'|'7672'|'7953'|'817'|'8206'|'9697'|'Test3'", self::MAP[$k][$value]); + + // ... + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-393.php b/tests/PHPStan/Rules/Variables/data/bug-393.php new file mode 100644 index 0000000000..492ce244e7 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-393.php @@ -0,0 +1,31 @@ +privateProperty = 123; + }, + new Foo(), + Foo::class + ))(); + + (\Closure::bind( + function () { + $this->privateProperty = 123; + }, + new Bar(), + Foo::class + ))(); +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-4173.php b/tests/PHPStan/Rules/Variables/data/bug-4173.php new file mode 100644 index 0000000000..9257376c96 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-4173.php @@ -0,0 +1,34 @@ += 7.4 + +namespace Bug7292; + +class MyMetadata { + /** @var class-string */ + public string $fqcn; +} + +class MyClass { + /** @var array> */ + private $myMap = []; + + public function doSomething(MyMetadata $class): void + { + unset($this->myMap[$class->fqcn]['foo']); + + if (isset($this->myMap[$class->fqcn]) && ! $this->myMap[$class->fqcn]) { + unset($this->myMap[$class->fqcn]); + } + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-8084.php b/tests/PHPStan/Rules/Variables/data/bug-8084.php new file mode 100644 index 0000000000..8c55e7ad53 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-8084.php @@ -0,0 +1,19 @@ += 8.0 + +namespace Bug8084a; + +use Exception; +use function array_shift; +use function PHPStan\Testing\assertType; + +class Bug8084 +{ + /** + * @param string[] $params + */ + public function run(array $params): void + { + $a = array_shift($params) ?? throw new Exception(); + $b = array_shift($params) ?? "default_b"; + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-8113.php b/tests/PHPStan/Rules/Variables/data/bug-8113.php new file mode 100644 index 0000000000..2c2807e48f --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-8113.php @@ -0,0 +1,48 @@ + array('id' => 23, + 'User' => array( + 'first_name' => 'x', + ), + ), + 'SurveyInvitation' => array( + 'is_too_old_to_follow' => 'yes', + ), + 'User' => array( + 'first_name' => 'x', + ), + ); + + assertType('array', $review); + + if ( + array_key_exists('review', $review['SurveyInvitation']) && + $review['SurveyInvitation']['review'] === null + ) { + assertType("array&hasOffsetValue('SurveyInvitation', array&hasOffsetValue('review', null))", $review); + $review['Review'] = [ + 'id' => null, + 'text' => null, + 'answer' => null, + ]; + assertType("non-empty-array&hasOffsetValue('Review', array{id: null, text: null, answer: null})&hasOffsetValue('SurveyInvitation', array&hasOffsetValue('review', null))", $review); + unset($review['SurveyInvitation']['review']); + assertType("non-empty-array&hasOffsetValue('Review', array)&hasOffsetValue('SurveyInvitation', array)", $review); + } + assertType('array', $review); + if (array_key_exists('User', $review['Review'])) { + assertType("array&hasOffsetValue('Review', array&hasOffset('User'))", $review); + $review['User'] = $review['Review']['User']; + assertType("hasOffsetValue('Review', array&hasOffset('User'))&hasOffsetValue('User', mixed)&non-empty-array", $review); + unset($review['Review']['User']); + assertType("hasOffsetValue('Review', array)&hasOffsetValue('User', array)&non-empty-array", $review); + } + assertType("array&hasOffsetValue('Review', array)", $review); +}; diff --git a/tests/PHPStan/Rules/Variables/data/bug-8212.php b/tests/PHPStan/Rules/Variables/data/bug-8212.php new file mode 100644 index 0000000000..384ce1b97e --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-8212.php @@ -0,0 +1,14 @@ +owner; + } +} + +function (): void { + $resume = new Resume(); + $owner = $resume->getOwner(); + if (!empty($owner)) { + echo "not empty"; + } +}; diff --git a/tests/PHPStan/Rules/Variables/data/bug-9403.php b/tests/PHPStan/Rules/Variables/data/bug-9403.php new file mode 100644 index 0000000000..0889949c57 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-9403.php @@ -0,0 +1,31 @@ +>', $result); + assertNativeType('list>', $result); + + if (!empty($result)) { + rsort($result); + } + return $result; + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-9474.php b/tests/PHPStan/Rules/Variables/data/bug-9474.php new file mode 100644 index 0000000000..5ea6ec4104 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-9474.php @@ -0,0 +1,22 @@ += 8.1 + +namespace Bug9474; + +class GlazedTerracotta{ + public function getColor() : int{ return 1; } +} + +class HelloWorld +{ + public function sayHello(): void + { + var_dump((function(GlazedTerracotta $block) : int{ + $i = match($color = $block->getColor()){ + 1 => 1, + default => throw new \Exception("Unhandled dye colour " . $color) + }; + echo $color; + return $i; + })(new GlazedTerracotta)); + } +} diff --git a/tests/PHPStan/Rules/Variables/data/defined-variables-enum.php b/tests/PHPStan/Rules/Variables/data/defined-variables-enum.php new file mode 100644 index 0000000000..b72fafb522 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/defined-variables-enum.php @@ -0,0 +1,28 @@ += 8.1 + +declare(strict_types=1); + +namespace DefinedVariablesEnum; + +enum Foo +{ + case A; + case B; +} + +class HelloWorld +{ + public function sayHello(Foo $f): void + { + switch ($f) { + case Foo::A: + $i = 5; + break; + case Foo::B: + $i = 6; + break; + } + + var_dump($i); + } +} diff --git a/tests/PHPStan/Rules/Variables/data/discussion-10252.php b/tests/PHPStan/Rules/Variables/data/discussion-10252.php new file mode 100644 index 0000000000..00f3d90429 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/discussion-10252.php @@ -0,0 +1,11 @@ +foo)) { + + } + + if (isset($o->bar)) { + + } + + if (isset($o->baz)) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/isstring-certainty.php b/tests/PHPStan/Rules/Variables/data/isstring-certainty.php new file mode 100644 index 0000000000..270e978e68 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/isstring-certainty.php @@ -0,0 +1,22 @@ +doFoo($p); + } + + /** + * @param list $p + * @param-out list $p + */ + function doBaz(&$p): void + { + unset($p[1]); + } + + /** + * @param list $p + * @param-out list $p + */ + function doBaz2(&$p): void + { + $p[] = 'str'; + } + + /** + * @param list> $p + * @param-out list> $p + */ + function doBaz3(&$p): void + { + unset($p[1][2]); + } + + function doNoParamOut(string &$p): void + { + $p = 1; + } + + function doNoParamOut2(string &$p): void + { + $p = 'foo'; + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/parameter-out-execution-end.php b/tests/PHPStan/Rules/Variables/data/parameter-out-execution-end.php new file mode 100644 index 0000000000..6f55e987cc --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/parameter-out-execution-end.php @@ -0,0 +1,106 @@ += 8.0 + +namespace PassByReferenceIntoNotNullable; + +class Foo +{ + + public function doFooNoType(&$test) + { + + } + + public function doFooMixedType(mixed &$test) + { + + } + + public function doFooIntType(int &$test) + { + + } + + public function doFooNullableType(?int &$test) + { + + } + + public function test() + { + $this->doFooNoType($one); + $this->doFooMixedType($two); + $this->doFooIntType($three); + $this->doFooNullableType($four); + } + +} + +class FooPhpDocs +{ + + /** + * @param mixed $test + */ + public function doFooMixedType(&$test) + { + + } + + /** + * @param int $test + */ + public function doFooIntType(&$test) + { + + } + + /** + * @param int|null $test + */ + public function doFooNullableType(&$test) + { + + } + + public function test() + { + $this->doFooMixedType($two); + $this->doFooIntType($three); + $this->doFooNullableType($four); + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/variable-cloning.php b/tests/PHPStan/Rules/Variables/data/variable-cloning.php index ae58c62b9b..f40a61658a 100644 --- a/tests/PHPStan/Rules/Variables/data/variable-cloning.php +++ b/tests/PHPStan/Rules/Variables/data/variable-cloning.php @@ -29,4 +29,8 @@ class Foo {}; /** @var object $object */ $object = doFoo(); clone $object; + + /** @var Bar $unknownObject */ + $unknownObject = doBaz(); + clone $unknownObject; }; diff --git a/tests/PHPStan/Rules/WarningEmittingRuleTest.php b/tests/PHPStan/Rules/WarningEmittingRuleTest.php new file mode 100644 index 0000000000..6b0ac62cb8 --- /dev/null +++ b/tests/PHPStan/Rules/WarningEmittingRuleTest.php @@ -0,0 +1,53 @@ + + */ +class WarningEmittingRuleTest extends RuleTestCase +{ + + /** + * @return Rule + */ + protected function getRule(): Rule + { + return new class implements Rule { + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + echo $undefined; // @phpstan-ignore variable.undefined + return []; + } + + }; + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 70300) { + self::markTestSkipped('For some reason this test does not work on PHP 7.2 with old PHPUnit'); + } + + try { + $this->analyse([__DIR__ . '/data/empty-file.php'], []); + self::fail('Should throw an exception'); + + } catch (AssertionFailedError $e) { + self::assertStringContainsString('Undefined variable', $e->getMessage()); // exact message differs between PHPStan versions + } + } + +} diff --git a/tests/PHPStan/Rules/data/datetime-instantiation.php b/tests/PHPStan/Rules/data/datetime-instantiation.php index dada942a30..47bb5deb95 100644 --- a/tests/PHPStan/Rules/data/datetime-instantiation.php +++ b/tests/PHPStan/Rules/data/datetime-instantiation.php @@ -18,3 +18,6 @@ function foo(string $date, string $date2): void { } new \DateTime('2020-04-31'); + +new \dateTime('2020.11.17'); +new \dateTimeImmutablE('2020.11.17'); diff --git a/tests/PHPStan/Rules/data/empty-file.php b/tests/PHPStan/Rules/data/empty-file.php new file mode 100644 index 0000000000..60ac8c38d2 --- /dev/null +++ b/tests/PHPStan/Rules/data/empty-file.php @@ -0,0 +1,3 @@ += 8.0 + +namespace ScopeFunctionCallStack; + +function (): void +{ + var_dump(print_r(sleep(throw new \Exception()))); + + var_dump(print_r(function () { + sleep(throw new \Exception()); + })); + + var_dump(print_r(fn () => sleep(throw new \Exception()))); +}; diff --git a/tests/PHPStan/Testing/TypeInferenceTestCaseTest.php b/tests/PHPStan/Testing/TypeInferenceTestCaseTest.php new file mode 100644 index 0000000000..407ca5c94b --- /dev/null +++ b/tests/PHPStan/Testing/TypeInferenceTestCaseTest.php @@ -0,0 +1,61 @@ +expectException(AssertionFailedError::class); + $this->expectExceptionMessage($errorMessage); + + $this->gatherAssertTypes($filePath); + } + +} diff --git a/tests/PHPStan/Testing/data/assert-certainty-case-insensitive.php b/tests/PHPStan/Testing/data/assert-certainty-case-insensitive.php new file mode 100644 index 0000000000..79ea770cc4 --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-certainty-case-insensitive.php @@ -0,0 +1,9 @@ += 8.0 + +namespace MissingAssertCertaintyCaseSensitive; + +use PHPStan\TrinaryLogic; + +function doFoo(string $s) { + assertvariablecertainty(TrinaryLogic::createMaybe(), $s); +} diff --git a/tests/PHPStan/Testing/data/assert-certainty-missing-namespace.php b/tests/PHPStan/Testing/data/assert-certainty-missing-namespace.php new file mode 100644 index 0000000000..228bac1208 --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-certainty-missing-namespace.php @@ -0,0 +1,9 @@ += 8.0 + +namespace MissingAssertCertaintyNamespace; + +use PHPStan\TrinaryLogic; + +function doFoo(string $s) { + assertVariableCertainty(TrinaryLogic::createMaybe(), $s); +} diff --git a/tests/PHPStan/Testing/data/assert-certainty-wrong-namespace.php b/tests/PHPStan/Testing/data/assert-certainty-wrong-namespace.php new file mode 100644 index 0000000000..0ed04b3ef0 --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-certainty-wrong-namespace.php @@ -0,0 +1,10 @@ += 8.0 + +namespace WrongAssertCertaintyNamespace; + +use PHPStan\TrinaryLogic; +use function SomeWrong\Namespace\assertVariableCertainty; + +function doFoo(string $s) { + assertVariableCertainty(TrinaryLogic::createMaybe(), $s); +} diff --git a/tests/PHPStan/Testing/data/assert-native-type-case-insensitive.php b/tests/PHPStan/Testing/data/assert-native-type-case-insensitive.php new file mode 100644 index 0000000000..effd8b777a --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-native-type-case-insensitive.php @@ -0,0 +1,7 @@ += 8.0 + +namespace MissingAssertNativeCaseSensitive; + +function doFoo(string $s) { + assertNATIVEType('string', $s); +} diff --git a/tests/PHPStan/Testing/data/assert-native-type-missing-namespace.php b/tests/PHPStan/Testing/data/assert-native-type-missing-namespace.php new file mode 100644 index 0000000000..7df11d72fc --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-native-type-missing-namespace.php @@ -0,0 +1,7 @@ += 8.0 + +namespace MissingAssertNativeNamespace; + +function doFoo(string $s) { + assertNativeType('string', $s); +} diff --git a/tests/PHPStan/Testing/data/assert-native-type-wrong-namespace.php b/tests/PHPStan/Testing/data/assert-native-type-wrong-namespace.php new file mode 100644 index 0000000000..fb08c4829f --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-native-type-wrong-namespace.php @@ -0,0 +1,9 @@ += 8.0 + +namespace WrongAssertNativeNamespace; + +use function SomeWrong\Namespace\assertNativeType; + +function doFoo(string $s) { + assertNativeType('string', $s); +} diff --git a/tests/PHPStan/Testing/data/assert-type-case-insensitive.php b/tests/PHPStan/Testing/data/assert-type-case-insensitive.php new file mode 100644 index 0000000000..7738ae6f38 --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-type-case-insensitive.php @@ -0,0 +1,8 @@ += 8.0 + +namespace MissingTypeCaseSensitive; + +function doFoo(string $s) { + assertTYPe('string', $s); +} + diff --git a/tests/PHPStan/Testing/data/assert-type-missing-namespace.php b/tests/PHPStan/Testing/data/assert-type-missing-namespace.php new file mode 100644 index 0000000000..d0f9018efa --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-type-missing-namespace.php @@ -0,0 +1,8 @@ += 8.0 + +namespace MissingAssertTypeNamespace; + +function doFoo(string $s) { + assertType('string', $s); +} + diff --git a/tests/PHPStan/Testing/data/assert-type-wrong-namespace.php b/tests/PHPStan/Testing/data/assert-type-wrong-namespace.php new file mode 100644 index 0000000000..67715fd548 --- /dev/null +++ b/tests/PHPStan/Testing/data/assert-type-wrong-namespace.php @@ -0,0 +1,10 @@ += 8.0 + +namespace WrongAssertTypeNamespace; + +use function SomeWrong\Namespace\assertType; + +function doFoo(string $s) { + assertType('string', $s); +} + diff --git a/tests/PHPStan/Type/ArrayTypeTest.php b/tests/PHPStan/Type/ArrayTypeTest.php index 0390892aef..ed3af63507 100644 --- a/tests/PHPStan/Type/ArrayTypeTest.php +++ b/tests/PHPStan/Type/ArrayTypeTest.php @@ -5,6 +5,7 @@ use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\OversizedArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; @@ -65,6 +66,11 @@ public function dataIsSuperTypeOf(): array new ConstantArrayType([], []), TrinaryLogic::createYes(), ], + [ + new ArrayType(new IntegerType(), new StringType()), + new IntersectionType([new ArrayType(new IntegerType(), new StringType()), new OversizedArrayType()]), + TrinaryLogic::createYes(), + ], ]; } diff --git a/tests/PHPStan/Type/BitwiseFlagHelperTest.php b/tests/PHPStan/Type/BitwiseFlagHelperTest.php index 597e367f3b..49381b899b 100644 --- a/tests/PHPStan/Type/BitwiseFlagHelperTest.php +++ b/tests/PHPStan/Type/BitwiseFlagHelperTest.php @@ -125,13 +125,13 @@ public function testExprContainsConst(Expr $expr, string $constName, TrinaryLogi /** @var ScopeFactory $scopeFactory */ $scopeFactory = self::getContainer()->getByType(ScopeFactory::class); $scope = $scopeFactory->create(ScopeContext::create('file.php')) - ->assignVariable('mixedVar', new MixedType()) - ->assignVariable('stringVar', new StringType()) - ->assignVariable('integerVar', new IntegerType()) - ->assignVariable('booleanVar', new BooleanType()) - ->assignVariable('floatVar', new FloatType()) - ->assignVariable('unionIntFloatVar', new UnionType([new IntegerType(), new FloatType()])) - ->assignVariable('unionStringFloatVar', new UnionType([new StringType(), new FloatType()])); + ->assignVariable('mixedVar', new MixedType(), new MixedType()) + ->assignVariable('stringVar', new StringType(), new StringType()) + ->assignVariable('integerVar', new IntegerType(), new IntegerType()) + ->assignVariable('booleanVar', new BooleanType(), new BooleanType()) + ->assignVariable('floatVar', new FloatType(), new FloatType()) + ->assignVariable('unionIntFloatVar', new UnionType([new IntegerType(), new FloatType()]), new UnionType([new IntegerType(), new FloatType()])) + ->assignVariable('unionStringFloatVar', new UnionType([new StringType(), new FloatType()]), new UnionType([new StringType(), new FloatType()])); $analyser = new BitwiseFlagHelper($this->createReflectionProvider()); $actual = $analyser->bitwiseOrContainsConstant($expr, $scope, $constName); diff --git a/tests/PHPStan/Type/CallableTypeTest.php b/tests/PHPStan/Type/CallableTypeTest.php index 1f6e0dc379..dd5c46acff 100644 --- a/tests/PHPStan/Type/CallableTypeTest.php +++ b/tests/PHPStan/Type/CallableTypeTest.php @@ -50,6 +50,14 @@ public function dataIsSuperTypeOf(): array new CallableType([new NativeParameterReflection('foo', false, new MixedType(), PassedByReference::createNo(), false, null)], new MixedType(), false), TrinaryLogic::createYes(), ], + [ + new CallableType([ + new NativeParameterReflection('foo', false, new StringType(), PassedByReference::createNo(), false, null), + new NativeParameterReflection('bar', false, new StringType(), PassedByReference::createNo(), false, null), + ], new MixedType(), false), + new CallableType([new NativeParameterReflection('foo', false, new StringType(), PassedByReference::createNo(), false, null)], new MixedType(), false), + TrinaryLogic::createMaybe(), + ], ]; } @@ -338,6 +346,69 @@ public function dataAccepts(): array ]), TrinaryLogic::createYes(), ], + [ + new CallableType([ + new NativeParameterReflection('foo', false, new StringType(), PassedByReference::createNo(), false, null), + new NativeParameterReflection('bar', false, new StringType(), PassedByReference::createNo(), false, null), + ], new MixedType(), false), + new CallableType([new NativeParameterReflection('foo', false, new StringType(), PassedByReference::createNo(), false, null)], new MixedType(), false), + TrinaryLogic::createYes(), + ], + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), + TrinaryLogic::createYes(), + ], + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createNo()), + TrinaryLogic::createNo(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + TrinaryLogic::createNo(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createMaybe()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createMaybe()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createMaybe()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createMaybe()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + TrinaryLogic::createYes(), + ], + [ + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createYes()), + new CallableType([], new VoidType(), false, null, null, [], TrinaryLogic::createNo()), + TrinaryLogic::createNo(), + ], ]; } diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php index 315f06abd5..0b8bff2efb 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php @@ -4,6 +4,7 @@ use PHPStan\Type\BooleanType; use PHPStan\Type\NullType; +use PHPStan\Type\StringType; use PHPStan\Type\VerbosityLevel; use PHPUnit\Framework\TestCase; @@ -98,4 +99,33 @@ public function testAppendingOptionalKeys(): void $this->assertSame('array{0: 17|bool|null, 1?: 17|null, 2?: 17}', $builder->getArray()->describe(VerbosityLevel::precise())); } + public function testDegradedArrayIsNotAlwaysOversized(): void + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->degradeToGeneralArray(); + for ($i = 0; $i < 300; $i++) { + $builder->setOffsetValueType(new StringType(), new StringType()); + } + + $array = $builder->getArray(); + $this->assertSame('non-empty-array', $array->describe(VerbosityLevel::precise())); + } + + public function testIsList(): void + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + $builder->setOffsetValueType(null, new ConstantIntegerType(0)); + $this->assertTrue($builder->isList()); + + $builder->setOffsetValueType(new ConstantIntegerType(0), new NullType()); + $this->assertTrue($builder->isList()); + + $builder->setOffsetValueType(new ConstantIntegerType(1), new NullType(), true); + $this->assertTrue($builder->isList()); + + $builder->setOffsetValueType(new ConstantIntegerType(2), new NullType(), true); + $this->assertFalse($builder->isList()); + } + } diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php index 429911c157..049139818e 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -18,6 +18,7 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function array_map; use function sprintf; @@ -486,9 +487,73 @@ public function dataIsSuperTypeOf(): iterable new IntegerType(), ], 2, [0, 1]), new ConstantArrayType([], []), + TrinaryLogic::createYes(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], 2, [0, 1]), + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ], 1, [0]), + TrinaryLogic::createYes(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ]), + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], 2, [0, 1]), TrinaryLogic::createMaybe(), ]; + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new StringType(), + ]), + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], 2, [0, 1]), + TrinaryLogic::createNo(), + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], 2, [0, 1]), + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new StringType(), + ]), + TrinaryLogic::createNo(), + ]; + yield [ new ConstantArrayType([], []), new ConstantArrayType([ @@ -746,4 +811,146 @@ public function dataIsCallable(): iterable ]; } + public function dataValuesArray(): iterable + { + yield 'empty' => [ + new ConstantArrayType([], []), + new ConstantArrayType([], []), + ]; + + yield 'non-optional' => [ + new ConstantArrayType([ + new ConstantIntegerType(10), + new ConstantIntegerType(11), + ], [ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [20], [], false), + new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ], [ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [2], [], true), + ]; + + yield 'optional-1' => [ + new ConstantArrayType([ + new ConstantIntegerType(10), + new ConstantIntegerType(11), + new ConstantIntegerType(12), + new ConstantIntegerType(13), + new ConstantIntegerType(14), + ], [ + new ConstantStringType('a'), + new ConstantStringType('b'), + new ConstantStringType('c'), + new ConstantStringType('d'), + new ConstantStringType('e'), + ], [15], [1, 3], false), + new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + new ConstantIntegerType(4), + ], [ + new ConstantStringType('a'), + new UnionType([new ConstantStringType('b'), new ConstantStringType('c')]), + new UnionType([new ConstantStringType('c'), new ConstantStringType('d'), new ConstantStringType('e')]), + new UnionType([new ConstantStringType('d'), new ConstantStringType('e')]), + new ConstantStringType('e'), + ], [3, 4, 5], [3, 4], true), + ]; + + yield 'optional-2' => [ + new ConstantArrayType([ + new ConstantIntegerType(10), + new ConstantIntegerType(11), + new ConstantIntegerType(12), + new ConstantIntegerType(13), + new ConstantIntegerType(14), + ], [ + new ConstantStringType('a'), + new ConstantStringType('b'), + new ConstantStringType('c'), + new ConstantStringType('d'), + new ConstantStringType('e'), + ], [15], [0, 2, 4], false), + new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + new ConstantIntegerType(4), + ], [ + new UnionType([new ConstantStringType('a'), new ConstantStringType('b')]), + new UnionType([new ConstantStringType('b'), new ConstantStringType('c'), new ConstantStringType('d')]), + new UnionType([new ConstantStringType('c'), new ConstantStringType('d'), new ConstantStringType('e')]), + new UnionType([new ConstantStringType('d'), new ConstantStringType('e')]), + new ConstantStringType('e'), + ], [2, 3, 4, 5], [2, 3, 4], true), + ]; + + yield 'optional-at-end-and-list' => [ + new ConstantArrayType([ + new ConstantIntegerType(10), + new ConstantIntegerType(11), + new ConstantIntegerType(12), + ], [ + new ConstantStringType('a'), + new ConstantStringType('b'), + new ConstantStringType('c'), + ], [11, 12, 13], [1, 2], true), + new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + ], [ + new ConstantStringType('a'), + new ConstantStringType('b'), + new ConstantStringType('c'), + ], [1, 2, 3], [1, 2], true), + ]; + + yield 'optional-at-end-but-not-list' => [ + new ConstantArrayType([ + new ConstantIntegerType(10), + new ConstantIntegerType(11), + new ConstantIntegerType(12), + ], [ + new ConstantStringType('a'), + new ConstantStringType('b'), + new ConstantStringType('c'), + ], [11, 12, 13], [1, 2], false), + new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + ], [ + new ConstantStringType('a'), + new UnionType([new ConstantStringType('b'), new ConstantStringType('c')]), + new ConstantStringType('c'), + ], [1, 2, 3], [1, 2], true), + ]; + } + + /** + * @dataProvider dataValuesArray + */ + public function testValuesArray(ConstantArrayType $type, ConstantArrayType $expectedType): void + { + $actualType = $type->getValuesArray(); + $message = sprintf( + 'Values array of %s is %s, but should be %s', + $type->describe(VerbosityLevel::precise()), + $actualType->describe(VerbosityLevel::precise()), + $expectedType->describe(VerbosityLevel::precise()), + ); + $this->assertTrue($expectedType->equals($actualType), $message); + $this->assertSame($expectedType->isList(), $actualType->isList()); + $this->assertSame($expectedType->getNextAutoIndexes(), $actualType->getNextAutoIndexes()); + } + } diff --git a/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php b/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php index 00ec37a8b6..2122e3c829 100644 --- a/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php @@ -23,6 +23,14 @@ public function dataDescribe(): array new ConstantFloatType(1.2000000992884E-10), '1.2000000992884E-10', ], + [ + new ConstantFloatType(-1.200000099288476E+10), + '-12000000992.88476', + ], + [ + new ConstantFloatType(-1.200000099288476E+20), + '-1.200000099288476E+20', + ], [ new ConstantFloatType(1.2 * 1.4), '1.68', diff --git a/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php b/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php new file mode 100644 index 0000000000..e083852496 --- /dev/null +++ b/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php @@ -0,0 +1,82 @@ +&oversized-array', + ]; + + yield [ + '[1, 2, 3, ...[1, 2, 3]]', + 'non-empty-list&oversized-array', + ]; + + yield [ + '[1, 2, 3, ...[1, \'foo\' => 2, 3]]', + 'non-empty-array&oversized-array', + ]; + + yield [ + '[1, 2, 2 => 3]', + 'non-empty-list&oversized-array', + ]; + yield [ + '[1, 2, 3 => 3]', + 'non-empty-array&oversized-array', + ]; + yield [ + '[1, 1 => 2, 3]', + 'non-empty-list&oversized-array', + ]; + yield [ + '[1, 2 => 2, 3]', + 'non-empty-array&oversized-array', + ]; + yield [ + '[1, \'foo\' => 2, 3]', + 'non-empty-array&oversized-array', + ]; + } + + /** + * @dataProvider dataBuild + */ + public function testBuild(string $sourceCode, string $expectedTypeDescription): void + { + $parser = self::getParser(); + $ast = $parser->parseString('assertInstanceOf(Expression::class, $expr); + + $array = $expr->expr; + $this->assertInstanceOf(Array_::class, $array); + + $builder = new OversizedArrayBuilder(); + $initializerExprTypeResolver = self::getContainer()->getByType(InitializerExprTypeResolver::class); + $arrayType = $builder->build($array, static fn (Expr $expr): Type => $initializerExprTypeResolver->getType($expr, InitializerExprContext::createEmpty())); + $this->assertSame($expectedTypeDescription, $arrayType->describe(VerbosityLevel::precise())); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../../conf/bleedingEdge.neon', + ]; + } + +} diff --git a/tests/PHPStan/Type/FileTypeMapperTest.php b/tests/PHPStan/Type/FileTypeMapperTest.php index cc8a6632b6..a5be40c268 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -18,7 +18,7 @@ public function testGetResolvedPhpDoc(): void /** @var FileTypeMapper $fileTypeMapper */ $fileTypeMapper = self::getContainer()->getByType(FileTypeMapper::class); - $resolvedA = $fileTypeMapper->getResolvedPhpDoc(__DIR__ . '/data/annotations.php', 'Foo', null, null, '/** + $resolvedA = $fileTypeMapper->getResolvedPhpDoc(__DIR__ . '/data/annotations.php', 'TestAnnotations\\Foo', null, null, '/** * @property int | float $numericBazBazProperty * @property X $singleLetterObjectName * @@ -34,8 +34,14 @@ public function testGetResolvedPhpDoc(): void $this->assertCount(0, $resolvedA->getParamTags()); $this->assertCount(2, $resolvedA->getPropertyTags()); $this->assertNull($resolvedA->getReturnTag()); - $this->assertSame('float|int', $resolvedA->getPropertyTags()['numericBazBazProperty']->getType()->describe(VerbosityLevel::precise())); - $this->assertSame('X', $resolvedA->getPropertyTags()['singleLetterObjectName']->getType()->describe(VerbosityLevel::precise())); + $this->assertNotNull($resolvedA->getPropertyTags()['numericBazBazProperty']->getReadableType()); + $this->assertNotNull($resolvedA->getPropertyTags()['numericBazBazProperty']->getWritableType()); + $this->assertSame('float|int', $resolvedA->getPropertyTags()['numericBazBazProperty']->getReadableType()->describe(VerbosityLevel::precise())); + $this->assertSame('float|int', $resolvedA->getPropertyTags()['numericBazBazProperty']->getWritableType()->describe(VerbosityLevel::precise())); + $this->assertNotNull($resolvedA->getPropertyTags()['singleLetterObjectName']->getReadableType()); + $this->assertNotNull($resolvedA->getPropertyTags()['singleLetterObjectName']->getWritableType()); + $this->assertSame('TestAnnotations\\X', $resolvedA->getPropertyTags()['singleLetterObjectName']->getReadableType()->describe(VerbosityLevel::precise())); + $this->assertSame('TestAnnotations\\X', $resolvedA->getPropertyTags()['singleLetterObjectName']->getWritableType()->describe(VerbosityLevel::precise())); $this->assertCount(6, $resolvedA->getMethodTags()); $this->assertArrayNotHasKey('complicatedParameters', $resolvedA->getMethodTags()); // ambiguous parameter types @@ -60,7 +66,7 @@ public function testGetResolvedPhpDoc(): void $this->assertCount(0, $returningNullableObject->getParameters()); $rotate = $resolvedA->getMethodTags()['rotate']; - $this->assertSame('Image', $rotate->getReturnType()->describe(VerbosityLevel::precise())); + $this->assertSame('TestAnnotations\\Image', $rotate->getReturnType()->describe(VerbosityLevel::precise())); $this->assertFalse($rotate->isStatic()); $this->assertCount(2, $rotate->getParameters()); $this->assertSame('float', $rotate->getParameters()['angle']->getType()->describe(VerbosityLevel::precise())); @@ -80,7 +86,7 @@ public function testGetResolvedPhpDoc(): void $this->assertTrue($paramMultipleTypesWithExtraSpaces->getParameters()['string']->passedByReference()->no()); $this->assertFalse($paramMultipleTypesWithExtraSpaces->getParameters()['string']->isOptional()); $this->assertFalse($paramMultipleTypesWithExtraSpaces->getParameters()['string']->isVariadic()); - $this->assertSame('stdClass|null', $paramMultipleTypesWithExtraSpaces->getParameters()['object']->getType()->describe(VerbosityLevel::precise())); + $this->assertSame('TestAnnotations\\stdClass|null', $paramMultipleTypesWithExtraSpaces->getParameters()['object']->getType()->describe(VerbosityLevel::precise())); $this->assertTrue($paramMultipleTypesWithExtraSpaces->getParameters()['object']->passedByReference()->no()); $this->assertFalse($paramMultipleTypesWithExtraSpaces->getParameters()['object']->isOptional()); $this->assertFalse($paramMultipleTypesWithExtraSpaces->getParameters()['object']->isVariadic()); diff --git a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php index ac6c6e9ad0..929ec087dc 100644 --- a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php +++ b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php @@ -17,6 +17,7 @@ use PHPStan\Type\Test\B; use PHPStan\Type\Test\C; use PHPStan\Type\Test\D; +use PHPStan\Type\Test\E; use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -63,7 +64,7 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createYes(), ], 'implementation with @extends with different type args' => [ - new GenericObjectType(B\I::class, [new ObjectType('DateTimeInteface')]), + new GenericObjectType(B\I::class, [new ObjectType('DateTimeInterface')]), new GenericObjectType(B\IImpl::class, [new ObjectType('DateTime')]), TrinaryLogic::createNo(), ], @@ -97,6 +98,21 @@ public function dataIsSuperTypeOf(): array new GenericObjectType(C\Covariant::class, [new ObjectType('DateTimeInterface')]), TrinaryLogic::createMaybe(), ], + 'contravariant with equal types' => [ + new GenericObjectType(C\Contravariant::class, [new ObjectType('DateTime')]), + new GenericObjectType(C\Contravariant::class, [new ObjectType('DateTime')]), + TrinaryLogic::createYes(), + ], + 'contravariant with sub type' => [ + new GenericObjectType(C\Contravariant::class, [new ObjectType('DateTimeInterface')]), + new GenericObjectType(C\Contravariant::class, [new ObjectType('DateTime')]), + TrinaryLogic::createMaybe(), + ], + 'contravariant with super type' => [ + new GenericObjectType(C\Contravariant::class, [new ObjectType('DateTime')]), + new GenericObjectType(C\Contravariant::class, [new ObjectType('DateTimeInterface')]), + TrinaryLogic::createYes(), + ], [ new ObjectType(ReflectionClass::class), new GenericObjectType(ReflectionClass::class, [ @@ -138,11 +154,115 @@ public function dataIsSuperTypeOf(): array ]), TrinaryLogic::createNo(), ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createInvariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createCovariant()]), + TrinaryLogic::createNo(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createInvariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createContravariant()]), + TrinaryLogic::createNo(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createCovariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createInvariant()]), + TrinaryLogic::createYes(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createCovariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createCovariant()]), + TrinaryLogic::createYes(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createCovariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createContravariant()]), + TrinaryLogic::createNo(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createContravariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createInvariant()]), + TrinaryLogic::createYes(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createContravariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createInvariant()]), + TrinaryLogic::createYes(), + ], + [ + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTime')], null, null, [TemplateTypeVariance::createContravariant()]), + new GenericObjectType(C\Invariant::class, [new ObjectType('DateTimeInterface')], null, null, [TemplateTypeVariance::createCovariant()]), + TrinaryLogic::createNo(), + ], + ]; + } + + public function dataTypeProjections(): array + { + $invariantA = new GenericObjectType(E\Foo::class, [new ObjectType(E\A::class)], null, null, [TemplateTypeVariance::createInvariant()]); + $invariantB = new GenericObjectType(E\Foo::class, [new ObjectType(E\B::class)], null, null, [TemplateTypeVariance::createInvariant()]); + $invariantC = new GenericObjectType(E\Foo::class, [new ObjectType(E\C::class)], null, null, [TemplateTypeVariance::createInvariant()]); + + $covariantA = new GenericObjectType(E\Foo::class, [new ObjectType(E\A::class)], null, null, [TemplateTypeVariance::createCovariant()]); + $covariantB = new GenericObjectType(E\Foo::class, [new ObjectType(E\B::class)], null, null, [TemplateTypeVariance::createCovariant()]); + $covariantC = new GenericObjectType(E\Foo::class, [new ObjectType(E\C::class)], null, null, [TemplateTypeVariance::createCovariant()]); + + $contravariantA = new GenericObjectType(E\Foo::class, [new ObjectType(E\A::class)], null, null, [TemplateTypeVariance::createContravariant()]); + $contravariantB = new GenericObjectType(E\Foo::class, [new ObjectType(E\B::class)], null, null, [TemplateTypeVariance::createContravariant()]); + $contravariantC = new GenericObjectType(E\Foo::class, [new ObjectType(E\C::class)], null, null, [TemplateTypeVariance::createContravariant()]); + + $bivariant = new GenericObjectType(E\Foo::class, [new MixedType(true)], null, null, [TemplateTypeVariance::createBivariant()]); + + return [ + [$invariantB, $invariantA, TrinaryLogic::createNo()], + [$invariantB, $invariantB, TrinaryLogic::createYes()], + [$invariantB, $invariantC, TrinaryLogic::createNo()], + [$invariantB, $covariantA, TrinaryLogic::createNo()], + [$invariantB, $covariantB, TrinaryLogic::createNo()], + [$invariantB, $covariantC, TrinaryLogic::createNo()], + [$invariantB, $contravariantA, TrinaryLogic::createNo()], + [$invariantB, $contravariantB, TrinaryLogic::createNo()], + [$invariantB, $contravariantC, TrinaryLogic::createNo()], + [$invariantB, $bivariant, TrinaryLogic::createNo()], + + [$covariantB, $invariantA, TrinaryLogic::createMaybe()], + [$covariantB, $invariantB, TrinaryLogic::createYes()], + [$covariantB, $invariantC, TrinaryLogic::createYes()], + [$covariantB, $covariantA, TrinaryLogic::createMaybe()], + [$covariantB, $covariantB, TrinaryLogic::createYes()], + [$covariantB, $covariantC, TrinaryLogic::createYes()], + [$covariantB, $contravariantA, TrinaryLogic::createNo()], + [$covariantB, $contravariantB, TrinaryLogic::createNo()], + [$covariantB, $contravariantC, TrinaryLogic::createNo()], + [$covariantB, $bivariant, TrinaryLogic::createNo()], + + [$contravariantB, $invariantA, TrinaryLogic::createYes()], + [$contravariantB, $invariantB, TrinaryLogic::createYes()], + [$contravariantB, $invariantC, TrinaryLogic::createMaybe()], + [$contravariantB, $covariantA, TrinaryLogic::createNo()], + [$contravariantB, $covariantB, TrinaryLogic::createNo()], + [$contravariantB, $covariantC, TrinaryLogic::createNo()], + [$contravariantB, $contravariantA, TrinaryLogic::createYes()], + [$contravariantB, $contravariantB, TrinaryLogic::createYes()], + [$contravariantB, $contravariantC, TrinaryLogic::createMaybe()], + [$contravariantB, $bivariant, TrinaryLogic::createNo()], + + [$bivariant, $invariantA, TrinaryLogic::createYes()], + [$bivariant, $invariantB, TrinaryLogic::createYes()], + [$bivariant, $invariantC, TrinaryLogic::createYes()], + [$bivariant, $covariantA, TrinaryLogic::createYes()], + [$bivariant, $covariantB, TrinaryLogic::createYes()], + [$bivariant, $covariantC, TrinaryLogic::createYes()], + [$bivariant, $contravariantA, TrinaryLogic::createYes()], + [$bivariant, $contravariantB, TrinaryLogic::createYes()], + [$bivariant, $contravariantC, TrinaryLogic::createYes()], + [$bivariant, $bivariant, TrinaryLogic::createYes()], ]; } /** * @dataProvider dataIsSuperTypeOf + * @dataProvider dataTypeProjections */ public function testIsSuperTypeOf(Type $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -188,12 +308,12 @@ public function dataAccepts(): array TrinaryLogic::createYes(), ], 'implementation with @extends with different type args' => [ - new GenericObjectType(B\I::class, [new ObjectType('DateTimeInteface')]), + new GenericObjectType(B\I::class, [new ObjectType('DateTimeInterface')]), new GenericObjectType(B\IImpl::class, [new ObjectType('DateTime')]), TrinaryLogic::createNo(), ], 'generic object accepts normal object of same type' => [ - new GenericObjectType(Traversable::class, [new MixedType(true), new ObjectType('DateTimeInteface')]), + new GenericObjectType(Traversable::class, [new MixedType(true), new ObjectType('DateTimeInterface')]), new ObjectType(Traversable::class), TrinaryLogic::createYes(), ], @@ -212,6 +332,7 @@ public function dataAccepts(): array /** * @dataProvider dataAccepts + * @dataProvider dataTypeProjections */ public function testAccepts( Type $acceptingType, @@ -340,7 +461,7 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ ); } - /** @return array}> */ + /** @return array}> */ public function dataGetReferencedTypeArguments(): array { $templateType = static fn (string $name, ?Type $bound = null): TemplateType => TemplateTypeFactory::create( @@ -356,6 +477,7 @@ public function dataGetReferencedTypeArguments(): array new GenericObjectType(D\Invariant::class, [ $templateType('T'), ]), + false, [ new TemplateTypeReference( $templateType('T'), @@ -363,11 +485,42 @@ public function dataGetReferencedTypeArguments(): array ), ], ], + 'param: Invariant' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createCovariant(), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'param: Invariant' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createContravariant(), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], 'param: Out' => [ TemplateTypeVariance::createContravariant(), new GenericObjectType(D\Out::class, [ $templateType('T'), ]), + false, [ new TemplateTypeReference( $templateType('T'), @@ -382,6 +535,7 @@ public function dataGetReferencedTypeArguments(): array $templateType('T'), ]), ]), + false, [ new TemplateTypeReference( $templateType('T'), @@ -398,6 +552,176 @@ public function dataGetReferencedTypeArguments(): array ]), ]), ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'param: In' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], + 'param: In>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'param: In>>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], + 'param: In>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\Out::class, [ + $templateType('T'), + ]), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], + 'param: Out>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Out::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], + 'param: Out>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Out::class, [ + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ]), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'param: In>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ]), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'param: Invariant>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\Out::class, [ + $templateType('T'), + ]), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], + 'param: Invariant>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'param: In>>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\Out::class, [ + $templateType('T'), + ]), + ]), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], + 'param: Out>>' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Out::class, [ + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + ]), + false, [ new TemplateTypeReference( $templateType('T'), @@ -410,6 +734,7 @@ public function dataGetReferencedTypeArguments(): array new GenericObjectType(D\Invariant::class, [ $templateType('T'), ]), + false, [ new TemplateTypeReference( $templateType('T'), @@ -417,11 +742,42 @@ public function dataGetReferencedTypeArguments(): array ), ], ], + 'return: Invariant' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createCovariant(), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], + 'return: Invariant' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createContravariant(), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], 'return: Out' => [ TemplateTypeVariance::createCovariant(), new GenericObjectType(D\Out::class, [ $templateType('T'), ]), + false, [ new TemplateTypeReference( $templateType('T'), @@ -436,6 +792,7 @@ public function dataGetReferencedTypeArguments(): array $templateType('T'), ]), ]), + false, [ new TemplateTypeReference( $templateType('T'), @@ -452,6 +809,7 @@ public function dataGetReferencedTypeArguments(): array ]), ]), ]), + false, [ new TemplateTypeReference( $templateType('T'), @@ -459,30 +817,435 @@ public function dataGetReferencedTypeArguments(): array ), ], ], - 'return: Out>' => [ + 'return: In' => [ TemplateTypeVariance::createCovariant(), - new GenericObjectType(D\Out::class, [ - new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'return: In>' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\In::class, [ $templateType('T'), ]), ]), + false, [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createInvariant(), + TemplateTypeVariance::createCovariant(), ), ], ], - ]; - } - - /** - * @dataProvider dataGetReferencedTypeArguments - * - * @param array $expectedReferences - */ - public function testGetReferencedTypeArguments(TemplateTypeVariance $positionVariance, Type $type, array $expectedReferences): void - { + 'return: In>>' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'return: In>' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\Out::class, [ + $templateType('T'), + ]), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'return: Out>' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Out::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'return: Out>' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Out::class, [ + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ]), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'return: In>' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ]), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'return: Invariant>' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\Out::class, [ + $templateType('T'), + ]), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], + 'return: Invariant>' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'return: In>>' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\Out::class, [ + $templateType('T'), + ]), + ]), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], + 'return: Out>>' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Out::class, [ + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + ]), + false, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'param: Out> (with invariance composition)' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Out::class, [ + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ]), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'param: In> (with invariance composition)' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ]), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'param: Invariant> (with invariance composition)' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\Out::class, [ + $templateType('T'), + ]), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'param: Invariant> (with invariance composition)' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'param: In>> (with invariance composition)' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\Out::class, [ + $templateType('T'), + ]), + ]), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'param: Out>> (with invariance composition)' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Out::class, [ + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'param: Invariant (with invariance composition)' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createCovariant(), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + 'param: Invariant (with invariance composition)' => [ + TemplateTypeVariance::createContravariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createContravariant(), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], + 'return: Out> (with invariance composition)' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Out::class, [ + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ]), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'return: In> (with invariance composition)' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ]), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'return: Invariant> (with invariance composition)' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\Out::class, [ + $templateType('T'), + ]), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'return: Invariant> (with invariance composition)' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'return: In>> (with invariance composition)' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\In::class, [ + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\Out::class, [ + $templateType('T'), + ]), + ]), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'return: Out>> (with invariance composition)' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Out::class, [ + new GenericObjectType(D\Invariant::class, [ + new GenericObjectType(D\In::class, [ + $templateType('T'), + ]), + ]), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createInvariant(), + ), + ], + ], + 'return: Invariant (with invariance composition)' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createCovariant(), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createCovariant(), + ), + ], + ], + 'return: Invariant (with invariance composition)' => [ + TemplateTypeVariance::createCovariant(), + new GenericObjectType(D\Invariant::class, [ + $templateType('T'), + ], null, null, [ + TemplateTypeVariance::createContravariant(), + ]), + true, + [ + new TemplateTypeReference( + $templateType('T'), + TemplateTypeVariance::createContravariant(), + ), + ], + ], + ]; + } + + /** + * @dataProvider dataGetReferencedTypeArguments + * + * @param array $expectedReferences + */ + public function testGetReferencedTypeArguments(TemplateTypeVariance $positionVariance, Type $type, bool $invarianceComposition, array $expectedReferences): void + { + TemplateTypeVariance::setInvarianceCompositionEnabled($invarianceComposition); + $result = []; foreach ($type->getReferencedTemplateTypes($positionVariance) as $r) { $result[] = $r; diff --git a/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php b/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php index a83addb405..2bcde9560a 100644 --- a/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php +++ b/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php @@ -25,6 +25,8 @@ public function testIssue2512(): void new TemplateTypeMap([ 'T' => $templateType, ]), + TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createInvariant(), ); $this->assertEquals( @@ -40,6 +42,8 @@ public function testIssue2512(): void $templateType, ]), ]), + TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createInvariant(), ); $this->assertEquals( diff --git a/tests/PHPStan/Type/Generic/data/generic-classes-c.php b/tests/PHPStan/Type/Generic/data/generic-classes-c.php index cd17cc9261..417797587b 100644 --- a/tests/PHPStan/Type/Generic/data/generic-classes-c.php +++ b/tests/PHPStan/Type/Generic/data/generic-classes-c.php @@ -9,3 +9,7 @@ interface Invariant { /** @template-covariant T */ interface Covariant { } + +/** @template-contravariant T */ +interface Contravariant { +} diff --git a/tests/PHPStan/Type/Generic/data/generic-classes-d.php b/tests/PHPStan/Type/Generic/data/generic-classes-d.php index b25dbc6bac..69621f8e08 100644 --- a/tests/PHPStan/Type/Generic/data/generic-classes-d.php +++ b/tests/PHPStan/Type/Generic/data/generic-classes-d.php @@ -13,3 +13,9 @@ interface Out { /** @return T */ public function get(); } + +/** @template-contravariant T */ +interface In { + /** @return T */ + public function get(); +} diff --git a/tests/PHPStan/Type/Generic/data/generic-classes-e.php b/tests/PHPStan/Type/Generic/data/generic-classes-e.php new file mode 100644 index 0000000000..ed60625429 --- /dev/null +++ b/tests/PHPStan/Type/Generic/data/generic-classes-e.php @@ -0,0 +1,10 @@ +assertSame('true', $type->toBoolean()->describe(VerbosityLevel::precise())); } + public function dataGetEnumCases(): iterable + { + if (PHP_VERSION_ID < 80100) { + return []; + } + + $reflectionProvider = $this->createReflectionProvider(); + $classReflection = $reflectionProvider->getClass(FooEnum::class); + + yield [ + new IntersectionType([ + new ThisType($classReflection), + new EnumCaseObjectType(FooEnum::class, 'FOO'), + ]), + [ + new EnumCaseObjectType(FooEnum::class, 'FOO'), + ], + ]; + } + + /** + * @dataProvider dataGetEnumCases + * @param list $expectedEnumCases + */ + public function testGetEnumCases( + IntersectionType $type, + array $expectedEnumCases, + ): void + { + $enumCases = $type->getEnumCases(); + $this->assertCount(count($expectedEnumCases), $enumCases); + foreach ($enumCases as $i => $enumCase) { + $expectedEnumCase = $expectedEnumCases[$i]; + $this->assertTrue($expectedEnumCase->equals($enumCase), sprintf('%s->equals(%s)', $expectedEnumCase->describe(VerbosityLevel::precise()), $enumCase->describe(VerbosityLevel::precise()))); + } + } + } diff --git a/tests/PHPStan/Type/MixedTypeTest.php b/tests/PHPStan/Type/MixedTypeTest.php index 32af9c48e0..6ee710a8c1 100644 --- a/tests/PHPStan/Type/MixedTypeTest.php +++ b/tests/PHPStan/Type/MixedTypeTest.php @@ -10,6 +10,7 @@ use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use function sprintf; @@ -240,6 +241,80 @@ public function testSubstractedIsArray(MixedType $mixedType, Type $typeToSubtrac ); } + public function dataSubstractedIsConstantArray(): array + { + return [ + [ + new MixedType(), + new ArrayType(new IntegerType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new MixedType(), new MixedType()), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new ConstantArrayType( + [new ConstantIntegerType(1)], + [new ConstantStringType('hello')], + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ConstantArrayType([], []), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new UnionType([new FloatType(), new ArrayType(new MixedType(), new MixedType())]), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new UnionType([new FloatType(), new ArrayType(new StringType(), new MixedType())]), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new UnionType([new FloatType(), new IntegerType()]), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new FloatType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(true), + new FloatType(), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsConstantArray + */ + public function testSubstractedIsConstantArray(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isConstantArray(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isConstantArray()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + public function dataSubstractedIsString(): array { return [ @@ -517,6 +592,108 @@ public function dataSubstractedIsLiteralString(): array ]; } + /** + * @dataProvider dataSubstractedIsClassString + */ + public function testSubstractedIsClassString(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isClassStringType(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isClassStringType()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsClassString(): array + { + return [ + [ + new MixedType(), + new StringType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new ClassStringType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new IntersectionType([ + new StringType(), + new AccessoryLiteralStringType(), + ]), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** @dataProvider dataSubtractedIsVoid */ + public function testSubtractedIsVoid(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isVoid(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isVoid()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubtractedIsVoid(): array + { + return [ + [ + new MixedType(), + new VoidType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new StringType(), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** @dataProvider dataSubtractedIsScalar */ + public function testSubtractedIsScalar(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isScalar(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isScalar()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubtractedIsScalar(): array + { + return [ + [ + new MixedType(), + new UnionType([new BooleanType(), new FloatType(), new IntegerType(), new StringType()]), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new StringType(), + TrinaryLogic::createMaybe(), + ], + ]; + } + /** * @dataProvider dataSubstractedIsLiteralString */ @@ -571,6 +748,247 @@ public function dataSubstractedIsIterable(): array ]; } + /** + * @dataProvider dataSubstractedIsBoolean + */ + public function testSubstractedIsBoolean(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isBoolean(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isBoolean()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsBoolean(): array + { + return [ + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ConstantBooleanType(true), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ConstantBooleanType(false), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new BooleanType(), + TrinaryLogic::createNo(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsFalse + */ + public function testSubstractedIsFalse(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isFalse(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isFalse()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsFalse(): array + { + return [ + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ConstantBooleanType(true), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ConstantBooleanType(false), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new BooleanType(), + TrinaryLogic::createNo(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsNull + */ + public function testSubstractedIsNull(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isNull(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isNull()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsNull(): array + { + return [ + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ConstantBooleanType(true), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ConstantBooleanType(false), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new BooleanType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new NullType(), + TrinaryLogic::createNo(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsTrue + */ + public function testSubstractedIsTrue(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isTrue(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isTrue()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsTrue(): array + { + return [ + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ConstantBooleanType(true), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new ConstantBooleanType(false), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new BooleanType(), + TrinaryLogic::createNo(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsFloat + */ + public function testSubstractedIsFloat(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isFloat(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isFloat()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsFloat(): array + { + return [ + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + IntegerRangeType::fromInterval(-5, 5), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new FloatType(), + TrinaryLogic::createNo(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsInteger + */ + public function testSubstractedIsInteger(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isInteger(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isInteger()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsInteger(): array + { + return [ + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + IntegerRangeType::fromInterval(-5, 5), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new StringType(), + TrinaryLogic::createMaybe(), + ], + ]; + } + /** * @dataProvider dataSubstractedIsIterable */ @@ -641,4 +1059,62 @@ public function testSubstractedIsOffsetAccessible(MixedType $mixedType, Type $ty ); } + public function dataSubtractedHasOffsetValueType(): array + { + return [ + [ + new MixedType(), + new ArrayType(new MixedType(), new MixedType()), + new StringType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new StringType(), + new StringType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ObjectType(ArrayAccess::class), + new StringType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new StringType(), + new ObjectType(ArrayAccess::class), + ]), + new StringType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new StringType(), + new ObjectType(ArrayAccess::class), + new FloatType(), + ]), + new StringType(), + TrinaryLogic::createNo(), + ], + ]; + } + + /** @dataProvider dataSubtractedHasOffsetValueType */ + public function testSubtractedHasOffsetValueType(MixedType $mixedType, Type $typeToSubtract, Type $offsetType, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->hasOffsetValueType($offsetType); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> hasOffsetValueType()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + } diff --git a/tests/PHPStan/Type/ObjectTypeTest.php b/tests/PHPStan/Type/ObjectTypeTest.php index 3cb3212860..6d8cfb28ee 100644 --- a/tests/PHPStan/Type/ObjectTypeTest.php +++ b/tests/PHPStan/Type/ObjectTypeTest.php @@ -4,6 +4,12 @@ use ArrayAccess; use ArrayObject; +use Bug4008\BaseModel; +use Bug4008\ChildGenericGenericClass; +use Bug4008\GenericClass; +use Bug4008\Model; +use Bug8850\UserInSessionInRoleEndpointExtension; +use Bug9006\TestInterface; use Closure; use Countable; use DateInterval; @@ -16,12 +22,14 @@ use InvalidArgumentException; use Iterator; use LogicException; +use ObjectTypeEnums\FooEnum; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; @@ -32,6 +40,7 @@ use Throwable; use ThrowPoints\TryCatch\MyInvalidArgumentException; use Traversable; +use function count; use function sprintf; use const PHP_VERSION_ID; @@ -429,6 +438,21 @@ public function dataIsSuperTypeOf(): array new ObjectType(DateTime::class), TrinaryLogic::createNo(), ], + 61 => [ + new ObjectType(UserInSessionInRoleEndpointExtension::class), + new ThisType($reflectionProvider->getClass(UserInSessionInRoleEndpointExtension::class)), + TrinaryLogic::createYes(), + ], + 62 => [ + new ObjectType(TestInterface::class), + new ClosureType([], new MixedType(), false), + TrinaryLogic::createNo(), + ], + 63 => [ + new ObjectType(TestInterface::class), + new ObjectType(Closure::class), + TrinaryLogic::createNo(), + ], ]; } @@ -460,7 +484,7 @@ public function dataAccepts(): array ], [ new ObjectType(Traversable::class), - new GenericObjectType(Traversable::class, [new MixedType(true), new ObjectType('DateTimeInteface')]), + new GenericObjectType(Traversable::class, [new MixedType(true), new ObjectType('DateTimeInterface')]), TrinaryLogic::createYes(), ], [ @@ -483,6 +507,16 @@ public function dataAccepts(): array ), TrinaryLogic::createNo(), ], + [ + new ObjectType(TestInterface::class), + new ClosureType([], new MixedType(), false), + TrinaryLogic::createNo(), + ], + 63 => [ + new ObjectType(TestInterface::class), + new ObjectType(Closure::class), + TrinaryLogic::createNo(), + ], ]; } @@ -582,4 +616,79 @@ public function testHasOffsetValueType( ); } + public function dataGetEnumCases(): iterable + { + yield [ + new ObjectType(stdClass::class), + [], + ]; + + yield [ + new ObjectType(FooEnum::class), + [ + new EnumCaseObjectType(FooEnum::class, 'FOO'), + new EnumCaseObjectType(FooEnum::class, 'BAR'), + new EnumCaseObjectType(FooEnum::class, 'BAZ'), + ], + ]; + + yield [ + new ObjectType(FooEnum::class, new EnumCaseObjectType(FooEnum::class, 'FOO')), + [ + new EnumCaseObjectType(FooEnum::class, 'BAR'), + new EnumCaseObjectType(FooEnum::class, 'BAZ'), + ], + ]; + + yield [ + new ObjectType(FooEnum::class, new UnionType([new EnumCaseObjectType(FooEnum::class, 'FOO'), new EnumCaseObjectType(FooEnum::class, 'BAR')])), + [ + new EnumCaseObjectType(FooEnum::class, 'BAZ'), + ], + ]; + } + + /** + * @dataProvider dataGetEnumCases + * @param list $expectedEnumCases + */ + public function testGetEnumCases( + ObjectType $type, + array $expectedEnumCases, + ): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $enumCases = $type->getEnumCases(); + $this->assertCount(count($expectedEnumCases), $enumCases); + foreach ($enumCases as $i => $enumCase) { + $expectedEnumCase = $expectedEnumCases[$i]; + $this->assertTrue($expectedEnumCase->equals($enumCase), sprintf('%s->equals(%s)', $expectedEnumCase->describe(VerbosityLevel::precise()), $enumCase->describe(VerbosityLevel::precise()))); + } + } + + public function testClassReflectionWithTemplateBound(): void + { + $type = new ObjectType(GenericClass::class); + $classReflection = $type->getClassReflection(); + $this->assertNotNull($classReflection); + $tModlel = $classReflection->getActiveTemplateTypeMap()->getType('TModlel'); + $this->assertNotNull($tModlel); + $this->assertSame(BaseModel::class, $tModlel->describe(VerbosityLevel::precise())); + } + + public function testClassReflectionParentWithTemplateBound(): void + { + $type = new ObjectType(ChildGenericGenericClass::class); + $classReflection = $type->getClassReflection(); + $this->assertNotNull($classReflection); + $ancestor = $classReflection->getAncestorWithClassName(GenericClass::class); + $this->assertNotNull($ancestor); + $tModlel = $ancestor->getActiveTemplateTypeMap()->getType('TModlel'); + $this->assertNotNull($tModlel); + $this->assertSame(Model::class, $tModlel->describe(VerbosityLevel::precise())); + } + } diff --git a/tests/PHPStan/Type/SimultaneousTypeTraverserTest.php b/tests/PHPStan/Type/SimultaneousTypeTraverserTest.php new file mode 100644 index 0000000000..e9cb8ada70 --- /dev/null +++ b/tests/PHPStan/Type/SimultaneousTypeTraverserTest.php @@ -0,0 +1,92 @@ +', + ]; + yield [ + new ArrayType(new MixedType(), new IntegerType()), + new ConstantArrayType( + [new ConstantIntegerType(0)], + [new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()])], + 1, + ), + 'array', + ]; + yield [ + new ArrayType(new MixedType(), new StringType()), + new ConstantArrayType( + [new ConstantIntegerType(0)], + [new IntegerType()], + 1, + ), + 'array', + ]; + + yield [ + new BenevolentUnionType([ + new StringType(), + new IntegerType(), + ]), + new UnionType([ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntegerType(), + new FloatType(), + ]), + '(int|string)', + ]; + + yield [ + new BenevolentUnionType([ + new StringType(), + new IntegerType(), + ]), + new UnionType([ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntegerType(), + ]), + '(int|non-empty-string)', + ]; + } + + /** + * @dataProvider dataChangeStringIntoNonEmptyString + */ + public function testChangeIntegerIntoString(Type $left, Type $right, string $expectedTypeDescription): void + { + $cb = static function (Type $left, Type $right, callable $traverse): Type { + if (!$left->isString()->yes()) { + return $traverse($left, $right); + } + if (!$right->isNonEmptyString()->yes()) { + return $traverse($left, $right); + } + return $right; + }; + $actualType = SimultaneousTypeTraverser::map($left, $right, $cb); + $this->assertSame($expectedTypeDescription, $actualType->describe(VerbosityLevel::precise())); + } + +} diff --git a/tests/PHPStan/Type/StringTypeTest.php b/tests/PHPStan/Type/StringTypeTest.php index d31be3e1fd..8f22550446 100644 --- a/tests/PHPStan/Type/StringTypeTest.php +++ b/tests/PHPStan/Type/StringTypeTest.php @@ -86,6 +86,11 @@ public function dataIsSuperTypeOf(): array new GenericClassStringType(new ObjectType(stdClass::class)), TrinaryLogic::createMaybe(), ], + [ + new StringAlwaysAcceptingObjectWithToStringType(), + new ObjectType(ClassWithToString::class), + TrinaryLogic::createYes(), + ], ]; } diff --git a/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtension.php b/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtension.php index 41db098905..5d34d6f911 100644 --- a/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtension.php +++ b/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtension.php @@ -10,7 +10,7 @@ final class TestDecimalOperatorTypeSpecifyingExtension implements OperatorTypeSp public function isOperatorSupported(string $operatorSigil, Type $leftSide, Type $rightSide): bool { - return in_array($operatorSigil, ['-', '+', '*', '/'], true) + return in_array($operatorSigil, ['-', '+', '*', '/', '^', '**'], true) && $leftSide->isSuperTypeOf(new ObjectType(TestDecimal::class))->yes() && $rightSide->isSuperTypeOf(new ObjectType(TestDecimal::class))->yes(); } diff --git a/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php b/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php index 3f64bd6221..55f44ec4fe 100644 --- a/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php +++ b/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php @@ -46,6 +46,18 @@ public function dataSigilAndSidesProvider(): iterable new ObjectType(TestDecimal::class), new ObjectType(TestDecimal::class), ]; + + yield '^' => [ + '^', + new ObjectType(TestDecimal::class), + new ObjectType(TestDecimal::class), + ]; + + yield '**' => [ + '**', + new ObjectType(TestDecimal::class), + new ObjectType(TestDecimal::class), + ]; } /** diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index e91065e062..f17eb54265 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -2,17 +2,22 @@ namespace PHPStan\Type; +use Bug9006\TestInterface; use CheckTypeFunctionCall\FinalClassWithMethodExists; use CheckTypeFunctionCall\FinalClassWithPropertyExists; use Closure; use DateTime; use DateTimeImmutable; use DateTimeInterface; +use DynamicProperties\FinalFoo; use Exception; use InvalidArgumentException; use Iterator; +use ObjectShapesAcceptance\ClassWithFooIntProperty; use PHPStan\Fixture\FinalClass; +use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; @@ -22,6 +27,7 @@ use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Accessory\NonEmptyArrayType; +use PHPStan\Type\Accessory\OversizedArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; @@ -1522,7 +1528,7 @@ public function dataUnion(): iterable new ConstantStringType('test_function'), ], UnionType::class, - '\'test_function\'|(callable(): mixed&string)', + '\'test_function\'|callable-string', ], [ [ @@ -1530,7 +1536,7 @@ public function dataUnion(): iterable new IntegerType(), ], UnionType::class, - '(callable(): mixed&string)|int', + 'callable-string|int', ], [ [ @@ -2145,6 +2151,18 @@ public function dataUnion(): iterable '$this(stdClass)|stdClass::foo', ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\ManyCasesTestEnum', new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'B'), + ])), + new ObjectType('PHPStan\Fixture\ManyCasesTestEnum', new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A')), + ], + ObjectType::class, + 'PHPStan\Fixture\ManyCasesTestEnum~PHPStan\Fixture\ManyCasesTestEnum::A', + ]; + yield [ [ new ThisType( @@ -2280,6 +2298,276 @@ public function dataUnion(): iterable IntersectionType::class, "array&hasOffsetValue(0, array&hasOffsetValue('code', mixed))", ]; + + yield [ + [ + new ConstantArrayType( + [new ConstantStringType('default'), new ConstantStringType('range')], + [new ObjectType(Foo::class), new ObjectType(Foo::class)], + [0], + [0, 1], + ), + new ConstantArrayType( + [new ConstantStringType('range')], + [new ObjectType(Foo::class)], + [0], + [0], + ), + ], + ConstantArrayType::class, + 'array{default?: RecursionCallable\Foo, range?: RecursionCallable\Foo}', + ]; + + yield [ + [ + new IntersectionType([ + new ConstantArrayType( + [new ConstantStringType('default'), new ConstantStringType('range')], + [new ObjectType(Foo::class), new ObjectType(Foo::class)], + [0], + [0, 1], + ), + new NonEmptyArrayType(), + ]), + new ConstantArrayType( + [new ConstantStringType('range')], + [new ObjectType(Foo::class)], + [0], + [0], + ), + ], + ConstantArrayType::class, + 'array{default?: RecursionCallable\Foo, range?: RecursionCallable\Foo}', + ]; + yield [ + [ + new IntersectionType([new ArrayType(new IntegerType(), new StringType()), new OversizedArrayType()]), + new ArrayType(new IntegerType(), new StringType()), + ], + IntersectionType::class, + 'array&oversized-array', + ]; + yield [ + [ + new ObjectType(TestInterface::class), + new ClosureType([], new MixedType(), false), + ], + UnionType::class, + 'Bug9006\TestInterface|(Closure(): mixed)', + ]; + yield [ + [ + new ObjectType(TestInterface::class), + new ObjectType(Closure::class), + ], + UnionType::class, + 'Bug9006\TestInterface|Closure', + ]; + yield [ + [ + new ObjectShapeType([], []), + new ObjectWithoutClassType(), + ], + ObjectWithoutClassType::class, + 'object', + ]; + yield [ + [ + new ObjectShapeType([], []), + new ObjectType(stdClass::class), + ], + UnionType::class, + 'object{}|stdClass', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['foo' => new IntegerType()], []), + ], + ObjectShapeType::class, + 'object{foo: int}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['foo' => new ConstantIntegerType(1)], []), + ], + ObjectShapeType::class, + 'object{foo: int}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['foo' => new StringType()], []), + ], + UnionType::class, + 'object{foo: int}|object{foo: string}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['bar' => new StringType()], []), + ], + UnionType::class, + 'object{bar: string}|object{foo: int}', + ]; + + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(Traversable::class), + ], + UnionType::class, + 'object{foo: int}|Traversable', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(\ObjectShape\Foo::class), + ], + UnionType::class, + 'ObjectShape\Foo|object{foo: int}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(ClassWithFooIntProperty::class), + ], + ObjectShapeType::class, + 'object{foo: int}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(\ObjectShapesAcceptance\FinalClass::class), + ], + UnionType::class, + 'ObjectShapesAcceptance\FinalClass|object{foo: int}', + ]; + yield [ + [ + new NeverType(), + new NonAcceptingNeverType(), + ], + NeverType::class, + '*NEVER*', + ]; + yield [ + [ + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0]), + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('c'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0, 1]), + ], + UnionType::class, + 'array{a?: true, b: true}|array{a?: true, c?: true}', + ]; + + yield [ + [ + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0]), + new IntersectionType([ + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('c'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0, 1]), + new NonEmptyArrayType(), + ]), + ], + UnionType::class, + 'array{a?: true, b: true}|(array{a?: true, c?: true}&non-empty-array)', + ]; + + yield [ + [ + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'TWO'), + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + ], + UnionType::class, + 'PHPStan\Fixture\AnotherTestEnum::ONE|PHPStan\Fixture\AnotherTestEnum::TWO|PHPStan\Fixture\TestEnumInterface', + ]; + yield [ + [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + ], + ObjectType::class, + 'PHPStan\Fixture\TestEnumInterface', + ]; + yield [ + [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + new ObjectWithoutClassType(), + ], + ObjectWithoutClassType::class, + 'object', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(), + ], + CallableType::class, + 'callable(): mixed', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new ClosureType(), + ], + CallableType::class, + 'pure-callable(): mixed', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createMaybe()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + ], + CallableType::class, + 'callable(): mixed', + ]; + yield [ + [ + new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new SimpleImpurePoint('functionCall', 'foo', true), + ]), + new ClosureType(), + ], + UnionType::class, + '(Closure(): mixed)|(pure-Closure)', + ]; + yield [ + [ + new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new SimpleImpurePoint('functionCall', 'foo', false), + ]), + new ClosureType(), + ], + ClosureType::class, + 'Closure(): mixed', + ]; } /** @@ -2475,7 +2763,7 @@ public function dataIntersect(): iterable StaticTypeFactory::truthy(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -2483,7 +2771,7 @@ public function dataIntersect(): iterable new NeverType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -2542,7 +2830,7 @@ public function dataIntersect(): iterable new IterableType(new StringType(), new MixedType()), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -2550,7 +2838,7 @@ public function dataIntersect(): iterable new IterableType(new MixedType(), new StringType()), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -2638,7 +2926,7 @@ public function dataIntersect(): iterable new HasMethodType('doBar'), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -2654,7 +2942,7 @@ public function dataIntersect(): iterable new HasMethodType('__toString'), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -2694,8 +2982,16 @@ public function dataIntersect(): iterable new ObjectType(\Test\Foo::class), new HasPropertyType('fooProperty'), ], + IntersectionType::class, + 'Test\Foo&hasProperty(fooProperty)', + ], + [ + [ + new ObjectType(FinalFoo::class), + new HasPropertyType('fooProperty'), + ], PHP_VERSION_ID < 80200 ? IntersectionType::class : NeverType::class, - PHP_VERSION_ID < 80200 ? 'Test\Foo&hasProperty(fooProperty)' : '*NEVER*', + PHP_VERSION_ID < 80200 ? 'DynamicProperties\FinalFoo&hasProperty(fooProperty)' : '*NEVER*=implicit', ], [ [ @@ -2727,7 +3023,7 @@ public function dataIntersect(): iterable new HasPropertyType('fooProperty'), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -2759,8 +3055,19 @@ public function dataIntersect(): iterable ]), new HasPropertyType('fooProperty'), ], - PHP_VERSION_ID < 80200 ? UnionType::class : NeverType::class, - PHP_VERSION_ID < 80200 ? '(Test\FirstInterface&hasProperty(fooProperty))|(Test\Foo&hasProperty(fooProperty))' : '*NEVER*', + UnionType::class, + '(Test\FirstInterface&hasProperty(fooProperty))|(Test\Foo&hasProperty(fooProperty))', + ], + [ + [ + new UnionType([ + new ObjectType(FinalFoo::class), + new ObjectType(FirstInterface::class), + ]), + new HasPropertyType('fooProperty'), + ], + PHP_VERSION_ID < 80200 ? UnionType::class : IntersectionType::class, + PHP_VERSION_ID < 80200 ? '(DynamicProperties\FinalFoo&hasProperty(fooProperty))|(Test\FirstInterface&hasProperty(fooProperty))' : 'Test\FirstInterface&hasProperty(fooProperty)', ], [ [ @@ -2817,7 +3124,7 @@ public function dataIntersect(): iterable new HasOffsetType(new ConstantStringType('b')), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -2825,7 +3132,7 @@ public function dataIntersect(): iterable new HasOffsetType(new ConstantStringType('a')), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -2907,7 +3214,7 @@ public function dataIntersect(): iterable new NonEmptyArrayType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -2941,7 +3248,7 @@ public function dataIntersect(): iterable new NonEmptyArrayType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -2963,7 +3270,7 @@ public function dataIntersect(): iterable new IntegerType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -2971,7 +3278,7 @@ public function dataIntersect(): iterable new StringType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -2979,7 +3286,7 @@ public function dataIntersect(): iterable new ConstantStringType('foo'), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -3094,7 +3401,7 @@ public function dataIntersect(): iterable new ConstantStringType('Nonexistent'), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -3102,7 +3409,7 @@ public function dataIntersect(): iterable new IntegerType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -3158,7 +3465,7 @@ public function dataIntersect(): iterable new GenericClassStringType(new ObjectType(stdClass::class)), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -3190,7 +3497,7 @@ public function dataIntersect(): iterable new ConstantStringType(stdClass::class), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -3214,7 +3521,7 @@ public function dataIntersect(): iterable IntegerRangeType::fromInterval(7, 9), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -3230,7 +3537,7 @@ public function dataIntersect(): iterable new ConstantIntegerType(4), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -3361,7 +3668,7 @@ public function dataIntersect(): iterable new ClassStringType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -3423,7 +3730,7 @@ public function dataIntersect(): iterable new AccessoryNumericStringType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -3447,7 +3754,7 @@ public function dataIntersect(): iterable new AccessoryNumericStringType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -3458,7 +3765,7 @@ public function dataIntersect(): iterable new NeverType(), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ [ @@ -3507,6 +3814,22 @@ public function dataIntersect(): iterable StrictMixedType::class, 'mixed', ], + [ + [ + new NeverType(true), + new IntegerType(), + ], + NeverType::class, + '*NEVER*=explicit', + ], + [ + [ + new NeverType(), + new IntegerType(), + ], + NeverType::class, + '*NEVER*=implicit', + ], ]; if (PHP_VERSION_ID < 80100) { @@ -3527,7 +3850,7 @@ public function dataIntersect(): iterable new EnumCaseObjectType(stdClass::class, 'ONE'), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ]; yield [ [ @@ -3551,7 +3874,7 @@ public function dataIntersect(): iterable new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ]; yield [ [ @@ -3575,7 +3898,7 @@ public function dataIntersect(): iterable new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'ONE'), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ]; yield [ [ @@ -3583,7 +3906,7 @@ public function dataIntersect(): iterable new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ]; yield [ [ @@ -3599,7 +3922,7 @@ public function dataIntersect(): iterable new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ]; yield [ [ @@ -3660,7 +3983,7 @@ public function dataIntersect(): iterable new ConstantStringType('0'), ], NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ]; yield [ @@ -3689,6 +4012,246 @@ public function dataIntersect(): iterable IntersectionType::class, 'array&hasOffsetValue(\'a\', 1)', ]; + yield [ + [ + new IntersectionType([new ArrayType(new IntegerType(), new StringType()), new OversizedArrayType()]), + new ArrayType(new IntegerType(), new StringType()), + ], + IntersectionType::class, + 'array&oversized-array', + ]; + yield [ + [ + new ObjectType(TestInterface::class), + new ClosureType([], new MixedType(), false), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ObjectType(TestInterface::class), + new ObjectType(Closure::class), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ObjectShapeType([], []), + new ObjectWithoutClassType(), + ], + ObjectShapeType::class, + 'object{}', + ]; + yield [ + [ + new ObjectShapeType([], []), + new ObjectType(stdClass::class), + ], + IntersectionType::class, + 'object{}&stdClass', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new HasPropertyType('foo'), + ], + ObjectShapeType::class, + 'object{foo: int}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], ['foo']), + new HasPropertyType('foo'), + ], + ObjectShapeType::class, + 'object{foo: int}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new HasPropertyType('bar'), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['foo' => new IntegerType()], []), + ], + ObjectShapeType::class, + 'object{foo: int}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['foo' => new ConstantIntegerType(1)], []), + ], + ObjectShapeType::class, + 'object{foo: 1}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['foo' => new StringType()], []), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['bar' => new StringType()], []), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(Traversable::class), + ], + IntersectionType::class, + 'object{foo: int}&Traversable', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(\ObjectShapesAcceptance\Foo::class), + ], + IntersectionType::class, + 'ObjectShapesAcceptance\Foo&object{foo: int}', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(ClassWithFooIntProperty::class), + ], + ObjectType::class, + 'ObjectShapesAcceptance\ClassWithFooIntProperty', + ]; + yield [ + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(\ObjectShapesAcceptance\FinalClass::class), + ], + PHP_VERSION_ID < 80200 ? IntersectionType::class : NeverType::class, + PHP_VERSION_ID < 80200 ? 'ObjectShapesAcceptance\FinalClass&object{foo: int}' : '*NEVER*=implicit', + ]; + yield [ + [ + new NeverType(true), + new NonAcceptingNeverType(), + ], + NonAcceptingNeverType::class, + 'never=explicit', + ]; + yield [ + [ + new UnionType([ + new ConstantArrayType([], []), + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0]), + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('c'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0, 1]), + ]), + new NonEmptyArrayType(), + ], + UnionType::class, + 'array{a?: true, b: true}|(array{a?: true, c?: true}&non-empty-array)', + ]; + yield [ + [ + new ConstantArrayType([], []), + new NonEmptyArrayType(), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0]), + new NonEmptyArrayType(), + ], + ConstantArrayType::class, + 'array{a?: true, b: true}', + ]; + yield [ + [ + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('c'), + ], [ + new ConstantBooleanType(true), + new ConstantBooleanType(true), + ], [0], [0, 1]), + new NonEmptyArrayType(), + ], + IntersectionType::class, + 'array{a?: true, c?: true}&non-empty-array', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new CallableType(), + ], + CallableType::class, + 'pure-callable(): mixed', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + new ClosureType(), + ], + ClosureType::class, + 'pure-Closure', + ]; + yield [ + [ + new CallableType(null, null, true, null, null, [], TrinaryLogic::createMaybe()), + new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes()), + ], + CallableType::class, + 'pure-callable(): mixed', + ]; + yield [ + [ + new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new SimpleImpurePoint('functionCall', 'foo', true), + ]), + new ClosureType(), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ClosureType([], new MixedType(), true, null, null, null, [], [], [ + new SimpleImpurePoint('functionCall', 'foo', false), + ]), + new ClosureType(), + ], + ClosureType::class, + 'pure-Closure', + ]; } /** @@ -3711,6 +4274,13 @@ public function testIntersect( $actualTypeDescription .= '=implicit'; } } + if ($actualType instanceof NeverType) { + if ($actualType->isExplicit()) { + $actualTypeDescription .= '=explicit'; + } else { + $actualTypeDescription .= '=implicit'; + } + } $this->assertSame($expectedTypeDescription, $actualTypeDescription); $this->assertInstanceOf($expectedTypeClass, $actualType); } @@ -3735,6 +4305,13 @@ public function testIntersectInversed( $actualTypeDescription .= '=implicit'; } } + if ($actualType instanceof NeverType) { + if ($actualType->isExplicit()) { + $actualTypeDescription .= '=explicit'; + } else { + $actualTypeDescription .= '=implicit'; + } + } $this->assertSame($expectedTypeDescription, $actualTypeDescription); $this->assertInstanceOf($expectedTypeClass, $actualType); } @@ -3746,7 +4323,7 @@ public function dataRemove(): array new ConstantBooleanType(true), new ConstantBooleanType(true), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new UnionType([ @@ -3802,13 +4379,13 @@ public function dataRemove(): array new ConstantBooleanType(true), new BooleanType(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new ConstantBooleanType(false), new BooleanType(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new BooleanType(), @@ -3826,19 +4403,19 @@ public function dataRemove(): array new BooleanType(), new BooleanType(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ StaticTypeFactory::falsey(), StaticTypeFactory::falsey(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ StaticTypeFactory::truthy(), StaticTypeFactory::truthy(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ StaticTypeFactory::truthy(), @@ -3959,7 +4536,7 @@ public function dataRemove(): array new BenevolentUnionType([new IntegerType(), new StringType()]), new UnionType([new IntegerType(), new StringType()]), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new ArrayType(new MixedType(), new MixedType()), @@ -3987,7 +4564,7 @@ public function dataRemove(): array ]), new NonEmptyArrayType(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new ArrayType(new MixedType(), new MixedType()), @@ -4026,13 +4603,13 @@ public function dataRemove(): array new MixedType(false), new MixedType(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new MixedType(false, new StringType()), new MixedType(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new MixedType(false), @@ -4062,7 +4639,7 @@ public function dataRemove(): array new ObjectType('Exception'), new ObjectType('Throwable'), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new ObjectType('Exception', new ObjectType('InvalidArgumentException')), @@ -4104,25 +4681,25 @@ public function dataRemove(): array IntegerRangeType::fromInterval(0, 2), IntegerRangeType::fromInterval(-1, 3), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ IntegerRangeType::fromInterval(0, 2), IntegerRangeType::fromInterval(0, 3), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ IntegerRangeType::fromInterval(0, 2), IntegerRangeType::fromInterval(-1, 2), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ IntegerRangeType::fromInterval(0, 2), new IntegerType(), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ IntegerRangeType::fromInterval(null, 1), @@ -4155,7 +4732,7 @@ public function dataRemove(): array ], 2), new HasOffsetType(new ConstantIntegerType(1)), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new ConstantArrayType([ @@ -4179,7 +4756,7 @@ public function dataRemove(): array ], 2, [1]), new HasOffsetType(new ConstantIntegerType(0)), NeverType::class, - '*NEVER*', + '*NEVER*=implicit', ], [ new MixedType(), @@ -4210,6 +4787,30 @@ public function dataRemove(): array TemplateMixedType::class, // should be TemplateConstantBooleanType 'T (class Foo, parameter)', // should be T of true ], + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new HasPropertyType('foo'), + NeverType::class, + '*NEVER*=implicit', + ], + [ + new ObjectShapeType(['foo' => new IntegerType()], ['foo']), + new HasPropertyType('foo'), + ObjectShapeType::class, + 'object{}', + ], + [ + new ObjectShapeType(['foo' => new IntegerType()], []), + new HasPropertyType('bar'), + ObjectShapeType::class, + 'object{foo: int}', + ], + [ + new ObjectShapeType(['foo' => new IntegerType()], ['foo']), + new HasPropertyType('bar'), + ObjectShapeType::class, + 'object{foo?: int}', + ], ]; } @@ -4225,7 +4826,15 @@ public function testRemove( ): void { $result = TypeCombinator::remove($fromType, $type); - $this->assertSame($expectedTypeDescription, $result->describe(VerbosityLevel::precise())); + $actualTypeDescription = $result->describe(VerbosityLevel::precise()); + if ($result instanceof NeverType) { + if ($result->isExplicit()) { + $actualTypeDescription .= '=explicit'; + } else { + $actualTypeDescription .= '=implicit'; + } + } + $this->assertSame($expectedTypeDescription, $actualTypeDescription); $this->assertInstanceOf($expectedTypeClass, $result); } diff --git a/tests/PHPStan/Type/TypeGetFiniteTypesTest.php b/tests/PHPStan/Type/TypeGetFiniteTypesTest.php new file mode 100644 index 0000000000..ce605f3d5a --- /dev/null +++ b/tests/PHPStan/Type/TypeGetFiniteTypesTest.php @@ -0,0 +1,144 @@ + $expectedTypes + */ + public function testGetFiniteTypes( + Type $type, + array $expectedTypes, + ): void + { + $this->assertEquals($expectedTypes, $type->getFiniteTypes()); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + ]; + } + +} diff --git a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php new file mode 100644 index 0000000000..592271bdda --- /dev/null +++ b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php @@ -0,0 +1,437 @@ +', + ]; + + yield [ + new ArrayType(new IntegerType(), new IntegerType()), + 'array', + ]; + + yield [ + new MixedType(), + 'mixed', + ]; + + yield [ + new ObjectType(stdClass::class), + 'stdClass', + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + new ConstantStringType('baz'), + new ConstantStringType('$ref'), + ], [ + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + new ConstantIntegerType(4), + ], [0], [2]), + 'array{foo: 1, bar: 2, baz?: 3, \'$ref\': 4}', + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('1100-RB'), + ], [ + new ConstantIntegerType(1), + ], [0]), + "array{'1100-RB': 1}", + ]; + + yield [ + new ConstantArrayType([ + new ConstantStringType('Karlovy Vary'), + ], [ + new ConstantIntegerType(1), + ], [0]), + "array{'Karlovy Vary': 1}", + ]; + + yield [ + new ObjectShapeType([ + '1100-RB' => new ConstantIntegerType(1), + ], []), + "object{'1100-RB': 1}", + ]; + + yield [ + new ConstantArrayType([ + new ConstantIntegerType(1), + new ConstantIntegerType(2), + new ConstantIntegerType(3), + ], [ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + new ConstantStringType('baz'), + ], [0], [2]), + 'array{1: \'foo\', 2: \'bar\', 3?: \'baz\'}', + ]; + + yield [ + new ConstantIntegerType(42), + '42', + ]; + + yield [ + new ConstantFloatType(2.5), + '2.5', + ]; + + yield [ + new ConstantBooleanType(true), + 'true', + ]; + + yield [ + new ConstantBooleanType(false), + 'false', + ]; + + yield [ + new ConstantStringType('foo'), + "'foo'", + ]; + + yield [ + new GenericClassStringType(new ObjectType('stdClass')), + 'class-string', + ]; + + yield [ + new GenericObjectType('stdClass', [ + new ConstantIntegerType(1), + new ConstantIntegerType(2), + ]), + 'stdClass<1, 2>', + ]; + + yield [ + new GenericObjectType('stdClass', [ + new StringType(), + new IntegerType(), + new MixedType(), + ], null, null, [ + TemplateTypeVariance::createInvariant(), + TemplateTypeVariance::createContravariant(), + TemplateTypeVariance::createBivariant(), + ]), + 'stdClass', + ]; + + yield [ + new IterableType(new MixedType(), new MixedType()), + 'iterable', + ]; + + yield [ + new IterableType(new MixedType(), new IntegerType()), + 'iterable', + ]; + + yield [ + new IterableType(new IntegerType(), new IntegerType()), + 'iterable', + ]; + + yield [ + new UnionType([new StringType(), new IntegerType()]), + '(int | string)', + ]; + + yield [ + new UnionType([new IntegerType(), new StringType()]), + '(int | string)', + ]; + + yield [ + new ObjectShapeType([ + 'foo' => new ConstantIntegerType(1), + 'bar' => new StringType(), + 'baz' => new ConstantIntegerType(2), + ], ['baz']), + 'object{foo: 1, bar: string, baz?: 2}', + ]; + + yield [ + new ConditionalType( + new ObjectWithoutClassType(), + new ObjectType('stdClass'), + new IntegerType(), + new StringType(), + false, + ), + '(object is stdClass ? int : string)', + ]; + + yield [ + new ConditionalType( + new ObjectWithoutClassType(), + new ObjectType('stdClass'), + new IntegerType(), + new StringType(), + true, + ), + '(object is not stdClass ? int : string)', + ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + 'literal-string', + ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + 'non-empty-string', + ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + 'numeric-string', + ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryLiteralStringType(), new AccessoryNonEmptyStringType()]), + '(literal-string & non-empty-string)', + ]; + + yield [ + new IntersectionType([new ArrayType(new IntegerType(), new StringType()), new NonEmptyArrayType()]), + 'non-empty-array', + ]; + + yield [ + new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new StringType()), new AccessoryArrayListType()]), + 'list', + ]; + + yield [ + new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new StringType()), new NonEmptyArrayType(), new AccessoryArrayListType()]), + 'non-empty-list', + ]; + + yield [ + new IntersectionType([new ClassStringType(), new AccessoryLiteralStringType()]), + '(class-string & literal-string)', + ]; + + yield [ + new IntersectionType([new GenericClassStringType(new ObjectType('Foo')), new AccessoryLiteralStringType()]), + '(class-string & literal-string)', + ]; + + yield [ + new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType()), new AccessoryArrayListType()]), + 'list', + ]; + + yield [ + new IntersectionType([ + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType()), + new NonEmptyArrayType(), + new AccessoryArrayListType(), + ]), + 'non-empty-list', + ]; + + yield [ + new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ], [ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ]), + "array{'foo', 'bar'}", + ]; + + yield [ + new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(2), + ], [ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ]), + "array{0: 'foo', 2: 'bar'}", + ]; + + yield [ + new ConstantArrayType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ], [ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [2], [1]), + "array{0: 'foo', 1?: 'bar'}", + ]; + } + + /** + * @dataProvider dataToPhpDocNode + */ + public function testToPhpDocNode(Type $type, string $expected): void + { + $phpDocNode = $type->toPhpDocNode(); + + $typeString = (string) $phpDocNode; + $this->assertSame($expected, $typeString); + + $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); + $parsedType = $typeStringResolver->resolve($typeString); + $this->assertTrue($type->equals($parsedType), sprintf('%s->equals(%s)', $type->describe(VerbosityLevel::precise()), $parsedType->describe(VerbosityLevel::precise()))); + } + + public function dataToPhpDocNodeWithoutCheckingEquals(): iterable + { + yield [ + new ConstantStringType("foo\nbar\nbaz"), + '(literal-string & non-falsy-string)', + ]; + + yield [ + new ConstantIntegerType(PHP_INT_MIN), + (string) PHP_INT_MIN, + ]; + + yield [ + new ConstantIntegerType(PHP_INT_MAX), + (string) PHP_INT_MAX, + ]; + + yield [ + new ConstantFloatType(9223372036854775807), + '9.223372036854776E+18', + ]; + + yield [ + new ConstantFloatType(-9223372036854775808), + '-9.223372036854776E+18', + ]; + + yield [ + new ConstantFloatType(2.35), + '2.35', + ]; + + yield [ + new ConstantFloatType(100), + '100.0', + ]; + + yield [ + new ConstantFloatType(8.202343767574732), + '8.202343767574732', + ]; + + yield [ + new ConstantFloatType(1e80), + '1.0E+80', + ]; + + yield [ + new ConstantFloatType(-5e-80), + '-5.0E-80', + ]; + + yield [ + new ConstantFloatType(0.0), + '0.0', + ]; + + yield [ + new ConstantFloatType(-0.0), + '-0.0', + ]; + } + + /** + * @dataProvider dataToPhpDocNodeWithoutCheckingEquals + */ + public function testToPhpDocNodeWithoutCheckingEquals(Type $type, string $expected): void + { + $phpDocNode = $type->toPhpDocNode(); + + $typeString = (string) $phpDocNode; + $this->assertSame($expected, $typeString); + + $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); + $typeStringResolver->resolve($typeString); + } + + public function dataFromTypeStringToPhpDocNode(): iterable + { + foreach ($this->dataToPhpDocNode() as [, $typeString]) { + yield [$typeString]; + } + + yield ['callable']; + yield ['callable(Foo): Bar']; + yield ['callable(Foo=, Bar=): Bar']; + yield ['Closure(Foo=, Bar=): Bar']; + + yield ['callable(Foo $foo): Bar']; + yield ['callable(Foo $foo=, Bar $bar=): Bar']; + yield ['Closure(Foo $foo=, Bar $bar=): Bar']; + yield ['Closure(Foo $foo=, Bar $bar=): (Closure(Foo): Bar)']; + } + + /** + * @dataProvider dataFromTypeStringToPhpDocNode + */ + public function testFromTypeStringToPhpDocNode(string $typeString): void + { + $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); + $type = $typeStringResolver->resolve($typeString); + $this->assertSame($typeString, (string) $type->toPhpDocNode()); + + $typeAgain = $typeStringResolver->resolve((string) $type->toPhpDocNode()); + $this->assertTrue($type->equals($typeAgain)); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + ]; + } + +} diff --git a/tests/PHPStan/Type/UnionTypeTest.php b/tests/PHPStan/Type/UnionTypeTest.php index b26e9a00ad..cd0ee180cc 100644 --- a/tests/PHPStan/Type/UnionTypeTest.php +++ b/tests/PHPStan/Type/UnionTypeTest.php @@ -10,6 +10,7 @@ use PHPStan\Reflection\PassedByReference; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\Accessory\HasOffsetType; @@ -20,16 +21,19 @@ use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use RecursionCallable\Foo; use stdClass; use function array_merge; use function array_reverse; use function get_class; use function sprintf; +use const PHP_VERSION_ID; class UnionTypeTest extends PHPStanTestCase { @@ -643,6 +647,69 @@ public function testIsSubTypeOfInversed(UnionType $type, Type $otherType, Trinar ); } + public function dataIsScalar(): array + { + return [ + [ + TypeCombinator::union( + new BooleanType(), + new IntegerType(), + new FloatType(), + new StringType(), + ), + TrinaryLogic::createYes(), + ], + [ + new UnionType([ + new BooleanType(), + new ObjectType(DateTimeImmutable::class), + ]), + TrinaryLogic::createMaybe(), + ], + [ + new UnionType([ + new IntegerType(), + new NullType(), + ]), + TrinaryLogic::createMaybe(), + ], + [ + new UnionType([ + new FloatType(), + new MixedType(), + ]), + TrinaryLogic::createMaybe(), + ], + [ + new UnionType([ + new ArrayType(new IntegerType(), new StringType()), + new StringType(), + ]), + TrinaryLogic::createMaybe(), + ], + [ + new UnionType([ + new ArrayType(new IntegerType(), new StringType()), + new NullType(), + new ObjectType(DateTimeImmutable::class), + new ResourceType(), + ]), + TrinaryLogic::createNo(), + ], + ]; + } + + /** @dataProvider dataIsScalar */ + public function testIsScalar(UnionType $type, TrinaryLogic $expectedResult): void + { + $actualResult = $type->isScalar(); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isScalar()', $type->describe(VerbosityLevel::precise())), + ); + } + public function dataDescribe(): array { return [ @@ -650,11 +717,13 @@ public function dataDescribe(): array new UnionType([new IntegerType(), new StringType()]), 'int|string', 'int|string', + 'int|string', ], [ new UnionType([new IntegerType(), new StringType(), new NullType()]), 'int|string|null', 'int|string|null', + 'int|string|null', ], [ new UnionType([ @@ -675,6 +744,7 @@ public function dataDescribe(): array new ConstantStringType('1'), ]), "1|2|2.2|10|'1'|'10'|'10aaa'|'11aaa'|'1aaa'|'2'|'2aaa'|'foo'|stdClass|true|null", + "1|2|2.2|10|'1'|'10'|'10aaa'|'11aaa'|'1aaa'|'2'|'2aaa'|'foo'|stdClass|true|null", 'float|int|stdClass|string|true|null', ], [ @@ -696,6 +766,7 @@ public function dataDescribe(): array new ConstantStringType('aaa'), ), '\'aaa\'|array{a: int, b: float}|array{a: string, b: bool}', + '\'aaa\'|array{a: int, b: float}|array{a: string, b: bool}', 'array|string', ], [ @@ -717,6 +788,7 @@ public function dataDescribe(): array new ConstantStringType('aaa'), ), '\'aaa\'|array{a: string, b: bool}|array{b: int, c: float}', + '\'aaa\'|array{a: string, b: bool}|array{b: int, c: float}', 'array|string', ], [ @@ -738,6 +810,7 @@ public function dataDescribe(): array new ConstantStringType('aaa'), ), '\'aaa\'|array{a: string, b: bool}|array{c: int, d: float}', + '\'aaa\'|array{a: string, b: bool}|array{c: int, d: float}', 'array|string', ], [ @@ -758,6 +831,7 @@ public function dataDescribe(): array ]), ), 'array{int, bool, float}|array{string}', + 'array{int, bool, float}|array{string}', 'array', ], [ @@ -770,6 +844,7 @@ public function dataDescribe(): array ]), ), 'array{}|array{foooo: \'barrr\'}', + 'array{}|array{foooo: \'barrr\'}', 'array', ], [ @@ -781,6 +856,7 @@ public function dataDescribe(): array ]), ), 'int|numeric-string', + 'int|numeric-string', 'int|string', ], [ @@ -790,6 +866,7 @@ public function dataDescribe(): array ), 'int<0, 4>|int<6, 10>', 'int<0, 4>|int<6, 10>', + 'int<0, 4>|int<6, 10>', ], [ TypeCombinator::union( @@ -801,6 +878,7 @@ public function dataDescribe(): array ), new NullType(), ), + 'TFoo of int (class foo, parameter)|null', '(TFoo of int)|null', '(TFoo of int)|null', ], @@ -814,6 +892,7 @@ public function dataDescribe(): array ), new GenericClassStringType(new ObjectType('Abc')), ), + 'class-string|TFoo of int (class foo, parameter)', 'class-string|TFoo of int', 'class-string|TFoo of int', ], @@ -827,6 +906,7 @@ public function dataDescribe(): array ), new NullType(), ), + 'TFoo (class foo, parameter)|null', 'TFoo|null', 'TFoo|null', ], @@ -845,9 +925,16 @@ public function dataDescribe(): array ), new NullType(), ), + 'TFoo of TBar (class foo, parameter) (class foo, parameter)|null', '(TFoo of TBar)|null', '(TFoo of TBar)|null', ], + [ + new UnionType([new ObjectType('Foo'), new ObjectType('Foo')]), + 'Foo#1|Foo#2', + 'Foo', + 'Foo', + ], ]; } @@ -856,17 +943,19 @@ public function dataDescribe(): array */ public function testDescribe( Type $type, + string $expectedPreciseDescription, string $expectedValueDescription, string $expectedTypeOnlyDescription, ): void { + $this->assertSame($expectedPreciseDescription, $type->describe(VerbosityLevel::precise())); $this->assertSame($expectedValueDescription, $type->describe(VerbosityLevel::value())); $this->assertSame($expectedTypeOnlyDescription, $type->describe(VerbosityLevel::typeOnly())); } - public function dataAccepts(): array + public function dataAccepts(): iterable { - return [ + yield from [ [ new UnionType([new CallableType(), new NullType()]), new ClosureType([], new StringType(), false), @@ -934,6 +1023,89 @@ public function dataAccepts(): array new ClosureType([], new MixedType(), false), TrinaryLogic::createYes(), ], + + ]; + + if (PHP_VERSION_ID >= 80100) { + yield [ + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'B'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'C'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'D'), + ]), + new ObjectType( + 'PHPStan\Fixture\ManyCasesTestEnum', + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'E'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'F'), + ]), + ), + TrinaryLogic::createYes(), + ]; + + yield [ + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'A'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'B'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'C'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'D'), + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'E'), + ]), + new ObjectType( + 'PHPStan\Fixture\ManyCasesTestEnum', + new EnumCaseObjectType('PHPStan\Fixture\ManyCasesTestEnum', 'F'), + ), + TrinaryLogic::createYes(), + ]; + + yield [ + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + ]), + new ObjectType('PHPStan\Fixture\TestEnum'), + TrinaryLogic::createYes(), + ]; + + yield [ + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'TWO'), + ]), + new ObjectType('PHPStan\Fixture\TestEnum'), + TrinaryLogic::createMaybe(), + ]; + + yield [ + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new NullType(), + ]), + new ObjectType( + 'PHPStan\Fixture\TestEnum', + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + ), + TrinaryLogic::createYes(), + ]; + + yield [ + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new NullType(), + ]), + new UnionType([ + new ObjectType( + 'PHPStan\Fixture\TestEnum', + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + ), + new NullType(), + ]), + TrinaryLogic::createYes(), + ]; + } + + yield from [ 'accepts template-of-union with same members' => [ new UnionType([ new IntegerType(), @@ -1119,7 +1291,6 @@ public function dataAccepts(): array ]), TrinaryLogic::createYes(), ], - ]; } @@ -1217,4 +1388,256 @@ public function testSorting(): void ); } + /** + * @dataProvider dataGetConstantArrays + * @param Type[] $types + * @param list $expectedDescriptions + */ + public function testGetConstantArrays( + array $types, + array $expectedDescriptions, + ): void + { + $unionType = TypeCombinator::union(...$types); + $constantArrays = $unionType->getConstantArrays(); + + $actualDescriptions = []; + foreach ($constantArrays as $constantArray) { + $actualDescriptions[] = $constantArray->describe(VerbosityLevel::precise()); + } + + $this->assertSame($expectedDescriptions, $actualDescriptions); + } + + public function dataGetConstantArrays(): iterable + { + yield from [ + [ + [ + TypeCombinator::intersect( + new ConstantArrayType( + [new ConstantIntegerType(1), new ConstantIntegerType(2)], + [new IntegerType(), new StringType()], + 2, + [0, 1], + ), + new NonEmptyArrayType(), + ), + new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [new ObjectType(Foo::class), new ObjectType(stdClass::class)], + 2, + ), + ], + [ + 'array{1?: int, 2?: string}', + 'array{RecursionCallable\Foo, stdClass}', + ], + ], + [ + [ + TypeCombinator::intersect( + new ConstantArrayType( + [new ConstantIntegerType(1), new ConstantIntegerType(2)], + [new IntegerType(), new StringType()], + 2, + [0, 1], + ), + ), + new IntegerType(), + ], + [], + ], + ]; + } + + /** + * @dataProvider dataGetConstantStrings + * @param list $expectedDescriptions + */ + public function testGetConstantStrings( + Type $unionType, + array $expectedDescriptions, + ): void + { + $constantStrings = $unionType->getConstantStrings(); + + $actualDescriptions = []; + foreach ($constantStrings as $constantString) { + $actualDescriptions[] = $constantString->describe(VerbosityLevel::precise()); + } + + $this->assertSame($expectedDescriptions, $actualDescriptions); + } + + public function dataGetConstantStrings(): iterable + { + yield from [ + [ + TypeCombinator::union( + new ConstantStringType('hello'), + new ConstantStringType('world'), + ), + [ + "'hello'", + "'world'", + ], + ], + [ + TypeCombinator::union( + new ConstantStringType(''), + TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType(), + ), + ), + [], + ], + [ + new UnionType([ + new IntersectionType( + [ + new ConstantStringType('foo'), + new AccessoryLiteralStringType(), + ], + ), + new IntersectionType( + [ + new ConstantStringType('bar'), + new AccessoryLiteralStringType(), + ], + ), + ]), + [ + "'foo'", + "'bar'", + ], + ], + [ + new BenevolentUnionType([ + new ConstantStringType('foo'), + new NullType(), + ]), + [ + "'foo'", + ], + ], + ]; + } + + /** + * @dataProvider dataGetObjectClassNames + * @param list $expectedObjectClassNames + */ + public function testGetObjectClassNames( + Type $unionType, + array $expectedObjectClassNames, + ): void + { + $this->assertSame($expectedObjectClassNames, $unionType->getObjectClassNames()); + } + + public function dataGetObjectClassNames(): iterable + { + yield from [ + [ + TypeCombinator::union( + new ObjectType(stdClass::class), + new ObjectType(DateTimeImmutable::class), + ), + [ + 'stdClass', + 'DateTimeImmutable', + ], + ], + [ + TypeCombinator::union( + new ObjectType(stdClass::class), + new NullType(), + ), + [], + ], + [ + TypeCombinator::union( + new StringType(), + new NullType(), + ), + [], + ], + ]; + } + + /** + * @dataProvider dataGetArrays + * @param list $expectedDescriptions + */ + public function testGetArrays( + Type $unionType, + array $expectedDescriptions, + ): void + { + $arrays = $unionType->getArrays(); + + $actualDescriptions = []; + foreach ($arrays as $arrayType) { + $actualDescriptions[] = $arrayType->describe(VerbosityLevel::precise()); + } + + $this->assertSame($expectedDescriptions, $actualDescriptions); + } + + public function dataGetArrays(): iterable + { + yield from [ + [ + TypeCombinator::union( + new ConstantStringType('hello'), + new ConstantStringType('world'), + ), + [], + ], + [ + TypeCombinator::union( + TypeCombinator::intersect( + new ConstantArrayType( + [new ConstantIntegerType(1), new ConstantIntegerType(2)], + [new IntegerType(), new StringType()], + 2, + [0, 1], + ), + new NonEmptyArrayType(), + ), + new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [new ObjectType(Foo::class), new ObjectType(stdClass::class)], + 2, + ), + ), + [ + 'array{1?: int, 2?: string}', + 'array{RecursionCallable\Foo, stdClass}', + ], + ], + [ + TypeCombinator::union( + new ArrayType(new IntegerType(), new StringType()), + new ConstantArrayType( + [new ConstantIntegerType(1), new ConstantIntegerType(2)], + [new IntegerType(), new StringType()], + 2, + [0, 1], + ), + new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [new ObjectType(Foo::class), new ObjectType(stdClass::class)], + 2, + ), + ), + [ + 'array', + ], + ], + ]; + } + } diff --git a/tests/PHPStan/Type/data/ObjectTypeEnums.php b/tests/PHPStan/Type/data/ObjectTypeEnums.php new file mode 100644 index 0000000000..b1f9843c7a --- /dev/null +++ b/tests/PHPStan/Type/data/ObjectTypeEnums.php @@ -0,0 +1,12 @@ += 8.1 + +namespace ObjectTypeEnums; + +enum FooEnum +{ + + case FOO; + case BAR; + case BAZ; + +} diff --git a/tests/PHPStan/Type/data/annotations.php b/tests/PHPStan/Type/data/annotations.php index 0732fd31c4..bf157ab365 100644 --- a/tests/PHPStan/Type/data/annotations.php +++ b/tests/PHPStan/Type/data/annotations.php @@ -1,5 +1,7 @@ \\|null but return statement is missing\\.$#" + message: "#^Access to an undefined property PhpParser\\\\Node\\:\\:\\$name\\.$#" count: 1 path: PHP-Parser/lib/PhpParser/NodeVisitor/NameResolver.php - - message: "#^Method PhpParser\\\\NodeVisitor\\\\NameResolver\\:\\:enterNode\\(\\) should return int\\|PhpParser\\\\Node\\|null but return statement is missing\\.$#" + message: "#^Access to an undefined property PhpParser\\\\Node\\:\\:\\$namespacedName\\.$#" count: 1 path: PHP-Parser/lib/PhpParser/NodeVisitor/NameResolver.php - - message: "#^Access to an undefined property PhpParser\\\\Node\\:\\:\\$name\\.$#" + message: "#^Method PhpParser\\\\NodeVisitor\\\\NameResolver\\:\\:beforeTraverse\\(\\) should return array\\\\|null but return statement is missing\\.$#" count: 1 path: PHP-Parser/lib/PhpParser/NodeVisitor/NameResolver.php - - message: "#^Access to an undefined property PhpParser\\\\Node\\:\\:\\$namespacedName\\.$#" + message: "#^Method PhpParser\\\\NodeVisitor\\\\NameResolver\\:\\:enterNode\\(\\) should return int\\|PhpParser\\\\Node\\|null but return statement is missing\\.$#" count: 1 path: PHP-Parser/lib/PhpParser/NodeVisitor/NameResolver.php - - message: "#^Method PhpParser\\\\NodeVisitorAbstract\\:\\:beforeTraverse\\(\\) should return array\\\\|null but return statement is missing\\.$#" + message: "#^Method PhpParser\\\\NodeVisitorAbstract\\:\\:afterTraverse\\(\\) should return array\\\\|null but return statement is missing\\.$#" count: 1 path: PHP-Parser/lib/PhpParser/NodeVisitorAbstract.php - - message: "#^Method PhpParser\\\\NodeVisitorAbstract\\:\\:enterNode\\(\\) should return int\\|PhpParser\\\\Node\\|null but return statement is missing\\.$#" + message: "#^Method PhpParser\\\\NodeVisitorAbstract\\:\\:beforeTraverse\\(\\) should return array\\\\|null but return statement is missing\\.$#" count: 1 path: PHP-Parser/lib/PhpParser/NodeVisitorAbstract.php - - message: "#^Method PhpParser\\\\NodeVisitorAbstract\\:\\:leaveNode\\(\\) should return array\\\\|int\\|PhpParser\\\\Node\\|false\\|null but return statement is missing\\.$#" + message: "#^Method PhpParser\\\\NodeVisitorAbstract\\:\\:enterNode\\(\\) should return int\\|PhpParser\\\\Node\\|null but return statement is missing\\.$#" count: 1 path: PHP-Parser/lib/PhpParser/NodeVisitorAbstract.php - - message: "#^Method PhpParser\\\\NodeVisitorAbstract\\:\\:afterTraverse\\(\\) should return array\\\\|null but return statement is missing\\.$#" + message: "#^Method PhpParser\\\\NodeVisitorAbstract\\:\\:leaveNode\\(\\) should return array\\\\|int\\|PhpParser\\\\Node\\|false\\|null but return statement is missing\\.$#" count: 1 path: PHP-Parser/lib/PhpParser/NodeVisitorAbstract.php @@ -145,16 +135,6 @@ parameters: count: 3 path: PHP-Parser/lib/PhpParser/Parser/Php7.php - - - message: "#^Property PhpParser\\\\ParserAbstract\\:\\:\\$endAttributes \\(array\\) does not accept string\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/ParserAbstract.php - - - - message: "#^Property PhpParser\\\\ParserAbstract\\:\\:\\$endAttributeStack \\(array\\\\) does not accept array\\\\.$#" - count: 1 - path: PHP-Parser/lib/PhpParser/ParserAbstract.php - - message: "#^Comparison operation \"\\<\" between \\(array\\|float\\|int\\<0, max\\>\\) and int results in an error\\.$#" count: 3 @@ -166,24 +146,24 @@ parameters: path: PHP-Parser/lib/PhpParser/ParserAbstract.php - - message: "#^Variable \\$tokenValue might not be defined\\.$#" + message: "#^Property PhpParser\\\\ParserAbstract\\:\\:\\$endAttributeStack \\(array\\\\) does not accept array\\\\.$#" count: 1 path: PHP-Parser/lib/PhpParser/ParserAbstract.php - - message: "#^Variable \\$action might not be defined\\.$#" + message: "#^Property PhpParser\\\\ParserAbstract\\:\\:\\$endAttributes \\(array\\) does not accept string\\.$#" count: 1 path: PHP-Parser/lib/PhpParser/ParserAbstract.php - - message: "#^Strict comparison using \\=\\=\\= between null and PhpParser\\\\Node will always evaluate to false\\.$#" + message: "#^Variable \\$action might not be defined\\.$#" count: 1 - path: PHP-Parser/lib/PhpParser/PrettyPrinterAbstract.php + path: PHP-Parser/lib/PhpParser/ParserAbstract.php - - message: "#^Else branch is unreachable because previous condition is always true\\.$#" + message: "#^Variable \\$tokenValue might not be defined\\.$#" count: 1 - path: PHP-Parser/lib/PhpParser/PrettyPrinterAbstract.php + path: PHP-Parser/lib/PhpParser/ParserAbstract.php - message: "#^Argument of an invalid type PhpParser\\\\Node supplied for foreach, only iterables are supported\\.$#" diff --git a/tests/e2e/data/empty.neon b/tests/e2e/data/empty.neon new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e/data/soap.php b/tests/e2e/data/soap.php index e57afe1481..d75edf768e 100644 --- a/tests/e2e/data/soap.php +++ b/tests/e2e/data/soap.php @@ -14,9 +14,9 @@ class MySoapClient2 extends \SoapClient /** * @param string|null $wsdl - * @param mixed[]|null $options + * @param mixed[] $options */ - public function __construct($wsdl, array $options = null) + public function __construct($wsdl, array $options = []) { parent::__construct($wsdl, $options); } @@ -46,7 +46,7 @@ class MySoapHeader extends \SoapHeader public function __construct(string $username, string $password) { - parent::SoapHeader($username, $password); + parent::__construct($username, $password); } } diff --git a/tests/e2e/data/timecop.php b/tests/e2e/data/timecop.php index 3a0ee354a1..0c7ad015cb 100644 --- a/tests/e2e/data/timecop.php +++ b/tests/e2e/data/timecop.php @@ -20,4 +20,9 @@ public static function create(): self return new self(new DateTimeImmutable()); } + public function getBar(): DateTimeImmutable + { + return $this->bar; + } + } diff --git a/tests/e2e/phpstan.neon b/tests/e2e/phpstan.neon index 8b96fff56d..b9fe1cd9c1 100644 --- a/tests/e2e/phpstan.neon +++ b/tests/e2e/phpstan.neon @@ -4,3 +4,4 @@ includes: parameters: phpVersion: 80000 tmpDir: tmp + treatPhpDocTypesAsCertain: false diff --git a/tests/e2e/phpstan_resultcachepath.neon b/tests/e2e/phpstan_resultcachepath.neon index 5aaf1200bd..3ad1d1a3b1 100644 --- a/tests/e2e/phpstan_resultcachepath.neon +++ b/tests/e2e/phpstan_resultcachepath.neon @@ -4,3 +4,4 @@ includes: parameters: phpVersion: 80000 resultCachePath: %tmpDir%/myResultCacheFile.php + treatPhpDocTypesAsCertain: false diff --git a/tests/generate-reflection-test.php b/tests/generate-reflection-test.php new file mode 100644 index 0000000000..eba37b5454 --- /dev/null +++ b/tests/generate-reflection-test.php @@ -0,0 +1,10 @@ +